AbenzaFran commited on
Commit
b02dba2
·
1 Parent(s): 959e7c7
Files changed (1) hide show
  1. app.py +205 -176
app.py CHANGED
@@ -2,214 +2,243 @@ import os
2
  import re
3
  import streamlit as st
4
  from dotenv import load_dotenv
5
- from langsmith import traceable
6
- from langsmith.wrappers import wrap_openai
7
- import openai
8
- import asyncio
9
- import threading
10
- from PIL import Image
11
  import io
 
12
  import json
13
  import queue
14
  import logging
15
- import time
16
-
17
- # Load environment variables
18
- load_dotenv()
19
- openai.api_key = os.getenv("openai.api_key")
20
- LANGSMITH_API_KEY = os.getenv("LANGSMITH_API_KEY")
21
- ASSISTANT_ID = os.getenv("ASSISTANT_ID_SOLUTION_SPECIFIER_A")
22
 
23
- # if not all([openai.api_key, LANGSMITH_API_KEY, ASSISTANT_ID]):
24
- # raise ValueError("Please set openai.api_key, LANGSMITH_API_KEY, and ASSISTANT_ID in your .env file.")
 
 
 
 
25
 
26
- # Initialize logging
27
- logging.basicConfig(format="[%(asctime)s] %(levelname)s: %(message)s", level=logging.INFO)
28
- logger = logging.getLogger(__name__)
 
 
 
 
 
 
29
 
30
- # Initialize Langsmith's traceable OpenAI client
31
- wrapped_openai = wrap_openai(openai.Client(api_key=openai.api_key, api_base="https://api.openai.com"))
32
 
33
- # Initialize Langsmith client (ensure you have configured Langsmith correctly)
34
- # Assuming Langsmith uses environment variables or configuration files for setup
35
- # If not, initialize it here accordingly
 
 
 
36
 
37
- # Define a traceable function to handle Assistant interactions
38
- @traceable
39
- def create_run(thread_id: str, assistant_id: str) -> openai.beta.RunsStream:
40
- """
41
- Creates a streaming run with the Assistant.
42
- """
43
- return wrapped_openai.beta.threads.runs.stream(
44
- thread_id=thread_id,
45
- assistant_id=assistant_id,
46
- model="gpt-4o", # Replace with your desired model
47
- stream=True
48
- )
49
 
50
- # Function to remove citations as per your original code
51
- def remove_citation(text: str) -> str:
52
- pattern = r"【\d+†\w+】"
53
- return re.sub(pattern, "📚", text)
 
54
 
55
- # Initialize session state for messages, thread_id, and tool_requests
 
 
56
  if "messages" not in st.session_state:
57
  st.session_state["messages"] = []
58
- if "thread_id" not in st.session_state:
59
- st.session_state["thread_id"] = None
 
 
60
  if "tool_requests" not in st.session_state:
61
  st.session_state["tool_requests"] = queue.Queue()
62
 
63
  tool_requests = st.session_state["tool_requests"]
64
 
65
- # Initialize Streamlit page
66
- st.set_page_config(page_title="Solution Specifier A", layout="centered")
67
- st.title("Solution Specifier A")
 
 
 
 
68
 
69
- # Display existing messages
70
- for msg in st.session_state["messages"]:
71
- if msg["role"] == "user":
72
- with st.chat_message("user"):
73
- st.write(msg["content"])
74
- else:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  with st.chat_message("assistant"):
76
- if isinstance(msg["content"], Image.Image):
77
- st.image(msg["content"])
78
- else:
79
- st.write(msg["content"])
80
 
81
- # Chat input widget
82
- user_input = st.chat_input("Type your message here...")
83
 
84
- # Function to handle tool requests (function calls)
85
- def handle_requires_action(tool_request):
86
- st.toast("Running a function", icon=":material/function:")
 
 
 
 
 
 
 
 
 
87
  tool_outputs = []
88
- data = tool_request.data
89
- for tool in data.required_action.submit_tool_outputs.tool_calls:
90
- if tool.function.arguments:
91
- function_arguments = json.loads(tool.function.arguments)
92
  else:
93
- function_arguments = {}
94
- match tool.function.name:
 
95
  case "hello_world":
96
- logger.info("Calling hello_world function")
97
- answer = hello_world(**function_arguments)
98
- tool_outputs.append({"tool_call_id": tool.id, "output": answer})
 
 
99
  case _:
100
- logger.error(f"Unrecognized function name: {tool.function.name}. Tool: {tool}")
101
- ret_val = {
102
- "status": "error",
103
- "message": f"Function name is not recognized. Ensure the correct function name and try again."
104
- }
105
- tool_outputs.append({"tool_call_id": tool.id, "output": json.dumps(ret_val)})
106
- st.toast("Function completed", icon=":material/function:")
107
  return tool_outputs, data.thread_id, data.id
108
 
109
- # Example function that could be called by the Assistant
110
- def hello_world(name: str) -> str:
111
- time.sleep(2) # Simulate a long-running task
112
- return f"Hello {name}!"
113
-
114
- # Function to add assistant messages to session state
115
- def add_message_to_state_session(message):
116
- if len(message) > 0:
117
- st.session_state["messages"].append({"role": "assistant", "content": message})
118
-
119
- # Function to process streamed data
120
- def data_streamer(stream):
121
  """
122
- Stream data from the assistant. Text messages are yielded. Images and tool requests are put in the queue.
 
 
 
123
  """
124
- logger.info("Starting data streamer")
125
- st.toast("Thinking...", icon=":material/emoji_objects:")
126
- content_produced = False
127
- try:
128
- for response in stream:
129
- event = response.event
130
- if event == "thread.message.delta":
131
- content = response.data.delta.content[0]
132
- if content.type == "text":
133
- value = content.text.value
134
- content_produced = True
135
- yield value
136
- elif content.type == "image_file":
137
- logger.info("Image file received")
138
- image_content = io.BytesIO(wrapped_openai.files.content(content.image_file.file_id).read())
139
- img = Image.open(image_content)
140
- content_produced = True
141
- yield img
142
- elif event == "thread.run.requires_action":
143
- logger.info("Run requires action")
144
- tool_requests.put(response)
145
- if not content_produced:
146
- yield "[LLM requires a function call]"
147
- break
148
- elif event == "thread.run.failed":
149
- logger.error("Run failed")
150
- yield "[Run failed]"
151
- break
152
- finally:
153
- st.toast("Completed", icon=":material/emoji_objects:")
154
- logger.info("Finished data streamer")
155
-
156
- # Function to display the streamed response
157
- def display_stream(stream):
158
- with st.chat_message("assistant"):
159
- for content in data_streamer(stream):
160
- if isinstance(content, Image.Image):
161
- st.image(content)
162
- add_message_to_state_session(content)
163
- else:
164
- st.write(content)
165
- add_message_to_state_session(content)
166
-
167
- # Main function to handle user input and assistant response
168
  def main():
 
 
 
 
 
 
 
 
 
169
  if user_input:
170
- # Add user message to session state
171
- st.session_state["messages"].append({"role": "user", "content": user_input})
172
-
173
- # Display the user's message
174
  with st.chat_message("user"):
175
  st.write(user_input)
176
-
177
- # Create a new thread if it doesn't exist
178
- if st.session_state["thread_id"] is None:
179
- logger.info("Creating new thread")
180
- thread = wrapped_openai.beta.threads.create()
181
- st.session_state["thread_id"] = thread.id
182
- else:
183
- thread = wrapped_openai.beta.threads.retrieve(st.session_state["thread_id"])
184
-
185
- # Add user message to the thread
186
- wrapped_openai.beta.threads.messages.create(
187
- thread_id=thread.id,
188
- role="user",
189
- content=user_input
190
  )
191
-
192
- # Create a new run with streaming
193
- logger.info("Creating a new run with streaming")
194
- stream = create_run(thread.id, ASSISTANT_ID)
195
-
196
- # Start a separate thread to handle streaming to avoid blocking Streamlit
197
- stream_thread = threading.Thread(target=display_stream, args=(stream,))
198
- stream_thread.start()
199
-
200
- # Handle tool requests if any
201
- while not tool_requests.empty():
202
- logger.info("Handling tool requests")
203
- tool_request = tool_requests.get()
204
- tool_outputs, thread_id, run_id = handle_requires_action(tool_request)
205
- wrapped_openai.beta.threads.runs.submit_tool_outputs_stream(
206
- thread_id=thread_id,
207
- run_id=run_id,
208
- tool_outputs=tool_outputs
209
- )
210
- # After handling, create a new stream to continue the conversation
211
- new_stream = create_run(thread_id, ASSISTANT_ID)
212
- display_stream(new_stream)
213
-
214
- # Run the main function
215
- main()
 
2
  import re
3
  import streamlit as st
4
  from dotenv import load_dotenv
5
+
 
 
 
 
 
6
  import io
7
+ import time
8
  import json
9
  import queue
10
  import logging
11
+ from PIL import Image
 
 
 
 
 
 
12
 
13
+ # ------------------------
14
+ # LangSmith imports
15
+ # ------------------------
16
+ import openai
17
+ from langsmith.wrappers import wrap_openai
18
+ from langsmith import traceable
19
 
20
+ # ------------------------
21
+ # Configure logging (optional but recommended)
22
+ # ------------------------
23
+ def init_logging():
24
+ logging.basicConfig(
25
+ format="[%(asctime)s] %(levelname)+8s: %(message)s",
26
+ level=logging.INFO,
27
+ )
28
+ return logging.getLogger()
29
 
30
+ logger = init_logging()
 
31
 
32
+ # ------------------------
33
+ # Load environment variables
34
+ # ------------------------
35
+ load_dotenv()
36
+ api_key = os.getenv("OPENAI_API_KEY")
37
+ assistant_id = os.getenv("ASSISTANT_ID_SOLUTION_SPECIFIER_A") # The assistant we want to call
38
 
39
+ if not api_key or not assistant_id:
40
+ raise RuntimeError("Please set OPENAI_API_KEY and ASSISTANT_ID_SOLUTION_SPECIFIER_A in your environment")
 
 
 
 
 
 
 
 
 
 
41
 
42
+ # ------------------------
43
+ # Wrap the OpenAI client for LangSmith traceability
44
+ # ------------------------
45
+ openai_client = openai.Client(api_key=api_key)
46
+ client = wrap_openai(openai_client)
47
 
48
+ # ------------------------
49
+ # Streamlit session state
50
+ # ------------------------
51
  if "messages" not in st.session_state:
52
  st.session_state["messages"] = []
53
+
54
+ if "thread" not in st.session_state:
55
+ st.session_state["thread"] = None
56
+
57
  if "tool_requests" not in st.session_state:
58
  st.session_state["tool_requests"] = queue.Queue()
59
 
60
  tool_requests = st.session_state["tool_requests"]
61
 
62
+ # ------------------------
63
+ # Utility to remove citations like: 【12†somefile】
64
+ # You can adapt to your own "annotations" handling if needed
65
+ # ------------------------
66
+ def remove_citation(text: str) -> str:
67
+ pattern = r"【\d+†\w+】"
68
+ return re.sub(pattern, "📚", text)
69
 
70
+ # ------------------------
71
+ # Helper: data streamer for text & images
72
+ # Adapted from the Medium article approach
73
+ # to handle text deltas, images, or function calls
74
+ # ------------------------
75
+ def data_streamer():
76
+ """
77
+ Streams data from the assistant run. Yields text or images
78
+ and enqueues tool requests (function calls) to tool_requests.
79
+ """
80
+ st.toast("Thinking...", icon=":material/emoji_objects:")
81
+ content_produced = False
82
+
83
+ for event in st.session_state["run_stream"]:
84
+ match event.event:
85
+ case "thread.message.delta":
86
+ # A chunk of text or an image
87
+ content = event.data.delta.content[0]
88
+ match content.type:
89
+ case "text":
90
+ text_value = content.text.value
91
+ content_produced = True
92
+ # Optionally remove citations, etc.
93
+ yield remove_citation(text_value)
94
+
95
+ case "image_file":
96
+ # If the assistant returns an image
97
+ file_id = content.image_file.file_id
98
+ content_produced = True
99
+ image_content = io.BytesIO(client.files.content(file_id).read())
100
+ yield Image.open(image_content)
101
+
102
+ case "thread.run.requires_action":
103
+ # The assistant is requesting a function call
104
+ logger.info(f"[Tool Request] {event}")
105
+ tool_requests.put(event)
106
+ if not content_produced:
107
+ # We can yield a placeholder if the model hasn't said anything yet
108
+ yield "[LLM is requesting a function call]"
109
+ return
110
+
111
+ case "thread.run.failed":
112
+ # The run failed for some reason
113
+ logger.error(f"Run failed: {event}")
114
+ return
115
+
116
+ # If we successfully streamed everything
117
+ st.toast("Completed", icon=":material/emoji_objects:")
118
+
119
+ # ------------------------
120
+ # Helper: display the streaming content
121
+ # This wraps data_streamer in st.write_stream
122
+ # so you can see partial tokens in real-time
123
+ # ------------------------
124
+ def display_stream(run_stream, create_context=True):
125
+ """
126
+ Grabs tokens from data_streamer() and displays them in real-time.
127
+ If `create_context=True`, messages are displayed as an assistant block.
128
+ """
129
+ st.session_state["run_stream"] = run_stream
130
+ if create_context:
131
  with st.chat_message("assistant"):
132
+ streamed_result = st.write_stream(data_streamer)
133
+ else:
134
+ streamed_result = st.write_stream(data_streamer)
 
135
 
136
+ # Return whatever the final token stream is
137
+ return streamed_result
138
 
139
+ # ------------------------
140
+ # Example of handling a function call (requires_action)
141
+ # If your Assistant uses function calling (e.g. code interpreter),
142
+ # you'd parse arguments, run the function, and return output here.
143
+ # ------------------------
144
+ def handle_tool_request(event):
145
+ """
146
+ Demonstrates how you might handle a function call.
147
+ In practice, you'd parse the arguments from the event
148
+ and run your custom logic. Then return outputs as JSON.
149
+ """
150
+ st.toast("Running a function (this is user-defined code)", icon=":material/function:")
151
  tool_outputs = []
152
+ data = event.data
153
+ for tool_call in data.required_action.submit_tool_outputs.tool_calls:
154
+ if tool_call.function.arguments:
155
+ function_args = json.loads(tool_call.function.arguments)
156
  else:
157
+ function_args = {}
158
+
159
+ match tool_call.function.name:
160
  case "hello_world":
161
+ # Example: implement a user-defined function
162
+ name = function_args.get("name", "anonymous")
163
+ time.sleep(2) # Simulate a long-running function
164
+ output_val = f"Hello, {name}! This was from a local function."
165
+ tool_outputs.append({"tool_call_id": tool_call.id, "output": output_val})
166
  case _:
167
+ # If unknown function name
168
+ msg = {"status": "error", "message": "Unknown function request."}
169
+ tool_outputs.append({"tool_call_id": tool_call.id, "output": json.dumps(msg)})
 
 
 
 
170
  return tool_outputs, data.thread_id, data.id
171
 
172
+ # ------------------------
173
+ # Main chat logic
174
+ # ------------------------
175
+ @traceable # Make this function traceable via LangSmith
176
+ def generate_assistant_reply(user_input: str):
 
 
 
 
 
 
 
177
  """
178
+ 1. If no thread exists, create a new one.
179
+ 2. Insert user message into the thread.
180
+ 3. Use the Assistants API to create a run + stream the response.
181
+ 4. If the assistant requests a function call, handle it and stream again.
182
  """
183
+ # Create or retrieve thread
184
+ if not st.session_state["thread"]:
185
+ st.session_state["thread"] = client.beta.threads.create()
186
+ thread = st.session_state["thread"]
187
+
188
+ # Add user message to the thread
189
+ client.beta.threads.messages.create(
190
+ thread_id=thread.id,
191
+ role="user",
192
+ content=user_input
193
+ )
194
+
195
+ # Start streaming assistant response
196
+ with client.beta.threads.runs.stream(
197
+ thread_id=thread.id,
198
+ assistant_id=assistant_id,
199
+ ) as run_stream:
200
+ display_stream(run_stream)
201
+
202
+ # If the assistant requested any tool calls, handle them now
203
+ while not tool_requests.empty():
204
+ event = tool_requests.get()
205
+ tool_outputs, t_id, run_id = handle_tool_request(event)
206
+ # Submit tool outputs
207
+ with client.beta.threads.runs.submit_tool_outputs_stream(
208
+ thread_id=t_id, run_id=run_id, tool_outputs=tool_outputs
209
+ ) as next_stream:
210
+ display_stream(next_stream, create_context=False)
211
+
212
+ # ------------------------
213
+ # Streamlit UI
214
+ # ------------------------
 
 
 
 
 
 
 
 
 
 
 
 
215
  def main():
216
+ st.set_page_config(page_title="Solution Specifier A", layout="centered")
217
+ st.title("Solution Specifier A")
218
+
219
+ # Display existing conversation
220
+ for msg in st.session_state["messages"]:
221
+ with st.chat_message(msg["role"]):
222
+ st.write(msg["content"])
223
+
224
+ user_input = st.chat_input("Type your message here...")
225
  if user_input:
226
+ # Show user's message
 
 
 
227
  with st.chat_message("user"):
228
  st.write(user_input)
229
+
230
+ # Keep in session state
231
+ st.session_state["messages"].append({"role": "user", "content": user_input})
232
+
233
+ # Generate assistant reply
234
+ generate_assistant_reply(user_input)
235
+
236
+ # In a real app, you might keep track of the final text
237
+ # from the streamed tokens. For simplicity, we store
238
+ # the entire streamed result as one block in session state:
239
+ st.session_state["messages"].append(
240
+ {"role": "assistant", "content": "[assistant reply streamed above]"}
 
 
241
  )
242
+
243
+ if __name__ == "__main__":
244
+ main()