Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -55,94 +55,91 @@ graph.add_edge("agent", END)
|
|
55 |
compiled_graph = graph.compile()
|
56 |
|
57 |
def respond_to_input(user_input: str) -> str:
|
58 |
-
|
59 |
-
Wrap compiled_graph.invoke(…) (and the “second pass”) in a try/except
|
60 |
-
so that if the agent ever returns something unparsable (a plain str),
|
61 |
-
we catch it and avoid the `'str' object has no attribute 'items'` error.
|
62 |
-
"""
|
63 |
-
|
64 |
-
# A) Build a SystemMessage that forces the model to emit bare JSON if it calls a tool:
|
65 |
system_msg = SystemMessage(
|
66 |
content=(
|
67 |
"You are an assistant with access to exactly these tools:\n"
|
68 |
" 1) web_search(query:str)\n"
|
69 |
" 2) parse_excel(path:str,sheet_name:str)\n"
|
70 |
" 3) ocr_image(path:str)\n\n"
|
71 |
-
"⚠️ **IMPORTANT** ⚠️: If (and only if) you need to call a tool, "
|
72 |
-
"
|
73 |
"```json\n"
|
74 |
'{"tool":"web_search","query":"Mercedes Sosa albums 2000-2009"}\n'
|
75 |
"```\n"
|
76 |
-
"That JSON must start at the very first character of your response and end at the
|
77 |
-
"no quotes, no code fences, and no
|
78 |
"If you do NOT need any tool, simply reply with your final answer as plain text."
|
79 |
)
|
80 |
)
|
81 |
|
82 |
-
#
|
83 |
-
initial_state = {
|
84 |
|
|
|
85 |
try:
|
86 |
-
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
92 |
if isinstance(msg, AIMessage):
|
93 |
-
print(f"[
|
94 |
print("=========================================")
|
95 |
|
96 |
-
#
|
97 |
-
|
98 |
-
for msg in reversed(final_state["messages"]):
|
99 |
if isinstance(msg, AIMessage):
|
100 |
-
|
101 |
-
|
102 |
-
|
103 |
-
#
|
104 |
-
|
105 |
-
if tool_dict:
|
106 |
-
# G) If valid JSON, run the tool:
|
107 |
-
print(">> Parsed tool call:", tool_dict)
|
108 |
-
tool_result = tool_node.run(tool_dict)
|
109 |
-
|
110 |
-
# H) Log output of the tool (optional):
|
111 |
-
print(f">> Tool '{tool_dict['tool']}' returned: {repr(tool_result)}")
|
112 |
-
|
113 |
-
# I) Second pass: feed the tool’s result back into the graph,
|
114 |
-
# with an empty user_input because we’re continuing the same turn.
|
115 |
-
continuation_state = {
|
116 |
-
"messages": [
|
117 |
-
*final_state["messages"],
|
118 |
-
AIMessage(content=tool_result)
|
119 |
-
]
|
120 |
-
}
|
121 |
-
second_pass = compiled_graph.invoke(continuation_state, "")
|
122 |
-
|
123 |
-
# J) Log every AIMessage from the second pass (optional):
|
124 |
-
print("===== AGENT MESSAGES (Second Pass) =====")
|
125 |
-
for i, msg in enumerate(second_pass["messages"]):
|
126 |
-
if isinstance(msg, AIMessage):
|
127 |
-
print(f"[AIMessage2 #{i}]: {repr(msg.content)}")
|
128 |
-
print("=========================================")
|
129 |
-
|
130 |
-
# K) Return the last AIMessage from the second pass as the final answer:
|
131 |
-
for msg in reversed(second_pass["messages"]):
|
132 |
-
if isinstance(msg, AIMessage):
|
133 |
-
return msg.content or ""
|
134 |
-
return ""
|
135 |
-
|
136 |
-
else:
|
137 |
-
# L) If it wasn’t valid JSON, just treat last_msg as plain‐text final answer:
|
138 |
-
return last_msg or ""
|
139 |
-
|
140 |
-
except Exception as e:
|
141 |
-
# M) If *any* part of the above raises (e.g. because the LLM returned a string
|
142 |
-
# that the routing layer tried to treat as a dict), we catch it here:
|
143 |
-
print(f"‼️ respond_to_input error: {repr(e)}")
|
144 |
-
return "" # or return a default fallback string if you prefer
|
145 |
-
|
146 |
|
147 |
class BasicAgent:
|
148 |
def __init__(self):
|
|
|
55 |
compiled_graph = graph.compile()
|
56 |
|
57 |
def respond_to_input(user_input: str) -> str:
|
58 |
+
# 1) Build a SystemMessage that insists on bare JSON if calling a tool
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
system_msg = SystemMessage(
|
60 |
content=(
|
61 |
"You are an assistant with access to exactly these tools:\n"
|
62 |
" 1) web_search(query:str)\n"
|
63 |
" 2) parse_excel(path:str,sheet_name:str)\n"
|
64 |
" 3) ocr_image(path:str)\n\n"
|
65 |
+
"⚠️ **IMPORTANT** ⚠️: If (and only if) you need to call a tool, output exactly one\n"
|
66 |
+
"JSON object (no extra text). For example:\n"
|
67 |
"```json\n"
|
68 |
'{"tool":"web_search","query":"Mercedes Sosa albums 2000-2009"}\n'
|
69 |
"```\n"
|
70 |
+
"That JSON must start at the very first character of your response and end at the last\n"
|
71 |
+
"character—no quotes, no code fences, and no explanation.\n\n"
|
72 |
"If you do NOT need any tool, simply reply with your final answer as plain text."
|
73 |
)
|
74 |
)
|
75 |
|
76 |
+
# 2) Initialize state with just that SystemMessage
|
77 |
+
initial_state = {"messages": [system_msg]}
|
78 |
|
79 |
+
# 3) --- FIRST PASS: just invoke the graph and see what it returns ---
|
80 |
try:
|
81 |
+
first_pass = compiled_graph.invoke(initial_state, user_input)
|
82 |
+
except Exception as e:
|
83 |
+
# If the error happens here, we log it plus any partial messages
|
84 |
+
print("‼️ ERROR during first invoke:", repr(e))
|
85 |
+
# If there is some state, print it for debugging:
|
86 |
+
try:
|
87 |
+
partial = first_pass["messages"]
|
88 |
+
except Exception:
|
89 |
+
partial = None
|
90 |
+
print("Partial state messages (if any):", partial)
|
91 |
+
# Return empty string so the loop can continue
|
92 |
+
return ""
|
93 |
+
|
94 |
+
# 4) If we reach here, first_pass succeeded. Log every AIMessage so we can see exactly
|
95 |
+
print("===== AGENT MESSAGES (First Pass) =====")
|
96 |
+
for idx, msg in enumerate(first_pass["messages"]):
|
97 |
+
if isinstance(msg, AIMessage):
|
98 |
+
# Print out raw content to see if it’s JSON or plain text
|
99 |
+
print(f"[AIMessage #{idx}]: {repr(msg.content)}")
|
100 |
+
print("=========================================")
|
101 |
+
|
102 |
+
# 5) Find the last AIMessage content (might be JSON or might be plain text)
|
103 |
+
last_msg = None
|
104 |
+
for msg in reversed(first_pass["messages"]):
|
105 |
+
if isinstance(msg, AIMessage):
|
106 |
+
last_msg = msg.content
|
107 |
+
break
|
108 |
+
|
109 |
+
# 6) Attempt to parse last_msg as a JSON dict for a tool call
|
110 |
+
tool_dict = parse_tool_json(last_msg or "")
|
111 |
+
if tool_dict:
|
112 |
+
# 7) If valid JSON, run the tool and do a second pass
|
113 |
+
print(">> Parsed tool call:", tool_dict)
|
114 |
+
tool_result = tool_node.run(tool_dict)
|
115 |
+
print(f">> Tool '{tool_dict['tool']}' returned: {repr(tool_result)}")
|
116 |
+
|
117 |
+
continuation_state = {
|
118 |
+
"messages": [
|
119 |
+
*first_pass["messages"],
|
120 |
+
AIMessage(content=tool_result)
|
121 |
+
]
|
122 |
+
}
|
123 |
+
try:
|
124 |
+
second_pass = compiled_graph.invoke(continuation_state, "")
|
125 |
+
except Exception as e2:
|
126 |
+
print("‼️ ERROR during second invoke:", repr(e2))
|
127 |
+
return ""
|
128 |
+
# 8) Log second‐pass AIMessages
|
129 |
+
print("===== AGENT MESSAGES (Second Pass) =====")
|
130 |
+
for idx, msg in enumerate(second_pass["messages"]):
|
131 |
if isinstance(msg, AIMessage):
|
132 |
+
print(f"[AIMessage2 #{idx}]: {repr(msg.content)}")
|
133 |
print("=========================================")
|
134 |
|
135 |
+
# 9) Return the final AIMessage from second_pass
|
136 |
+
for msg in reversed(second_pass["messages"]):
|
|
|
137 |
if isinstance(msg, AIMessage):
|
138 |
+
return msg.content or ""
|
139 |
+
return ""
|
140 |
+
else:
|
141 |
+
# 10) No valid JSON, return last_msg as plain text
|
142 |
+
return last_msg or ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
143 |
|
144 |
class BasicAgent:
|
145 |
def __init__(self):
|