Commit
·
b02dba2
1
Parent(s):
959e7c7
refactor
Browse files
app.py
CHANGED
@@ -2,214 +2,243 @@ import os
|
|
2 |
import re
|
3 |
import streamlit as st
|
4 |
from dotenv import load_dotenv
|
5 |
-
|
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
|
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 |
-
#
|
24 |
-
#
|
|
|
|
|
|
|
|
|
25 |
|
26 |
-
#
|
27 |
-
logging
|
28 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
-
|
31 |
-
wrapped_openai = wrap_openai(openai.Client(api_key=openai.api_key, api_base="https://api.openai.com"))
|
32 |
|
33 |
-
#
|
34 |
-
#
|
35 |
-
#
|
|
|
|
|
|
|
36 |
|
37 |
-
|
38 |
-
|
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 |
-
#
|
51 |
-
|
52 |
-
|
53 |
-
|
|
|
54 |
|
55 |
-
#
|
|
|
|
|
56 |
if "messages" not in st.session_state:
|
57 |
st.session_state["messages"] = []
|
58 |
-
|
59 |
-
|
|
|
|
|
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 |
-
#
|
66 |
-
|
67 |
-
|
|
|
|
|
|
|
|
|
68 |
|
69 |
-
#
|
70 |
-
for
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
75 |
with st.chat_message("assistant"):
|
76 |
-
|
77 |
-
|
78 |
-
|
79 |
-
st.write(msg["content"])
|
80 |
|
81 |
-
#
|
82 |
-
|
83 |
|
84 |
-
#
|
85 |
-
|
86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
87 |
tool_outputs = []
|
88 |
-
data =
|
89 |
-
for
|
90 |
-
if
|
91 |
-
|
92 |
else:
|
93 |
-
|
94 |
-
|
|
|
95 |
case "hello_world":
|
96 |
-
|
97 |
-
|
98 |
-
|
|
|
|
|
99 |
case _:
|
100 |
-
|
101 |
-
|
102 |
-
|
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 |
-
#
|
110 |
-
|
111 |
-
|
112 |
-
|
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 |
-
|
|
|
|
|
|
|
123 |
"""
|
124 |
-
|
125 |
-
st.
|
126 |
-
|
127 |
-
|
128 |
-
|
129 |
-
|
130 |
-
|
131 |
-
|
132 |
-
|
133 |
-
|
134 |
-
|
135 |
-
|
136 |
-
|
137 |
-
|
138 |
-
|
139 |
-
|
140 |
-
|
141 |
-
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
|
149 |
-
|
150 |
-
|
151 |
-
|
152 |
-
|
153 |
-
|
154 |
-
|
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 |
-
#
|
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 |
-
#
|
178 |
-
|
179 |
-
|
180 |
-
|
181 |
-
|
182 |
-
|
183 |
-
|
184 |
-
|
185 |
-
#
|
186 |
-
|
187 |
-
|
188 |
-
role="user",
|
189 |
-
content=user_input
|
190 |
)
|
191 |
-
|
192 |
-
|
193 |
-
|
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()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|