Humanlearning commited on
Commit
741c3da
·
1 Parent(s): 7ed8bad

working agent

Browse files
__pycache__/agent.cpython-313.pyc CHANGED
Binary files a/__pycache__/agent.cpython-313.pyc and b/__pycache__/agent.cpython-313.pyc differ
 
agent.py CHANGED
@@ -1,69 +1,90 @@
1
- from llama_index.llms.openai import OpenAI
2
- from tools.tavily_search_tool import search_web, record_notes, write_report, review_report, ReportOutput, get_structured_report_from_state
 
 
 
3
  from dotenv import load_dotenv
4
  import os
5
- from llama_index.core.agent.workflow import FunctionAgent
6
- from llama_index.core.agent.workflow import AgentWorkflow
7
- from llama_index.core.agent.workflow import (
8
- AgentInput,
9
- AgentOutput,
10
- ToolCall,
11
- ToolCallResult,
12
- AgentStream,
13
- )
14
 
15
  load_dotenv(os.path.join(os.path.dirname(__file__), 'env.local'))
16
 
17
  class TeacherStudentAgentWorkflow:
18
  def __init__(self):
19
- self.llm = OpenAI(model="gpt-4o-mini", api_key=os.getenv("OPENAI_API_KEY"))
 
 
 
20
 
21
- self.research_agent = FunctionAgent(
22
  name="ResearchAgent",
23
- description="Useful for searching the web for information on a given topic and recording notes on the topic.",
24
  system_prompt=(
25
- "You are the ResearchAgent that can search the web for information on a given topic and record notes on the topic. "
26
- "IMPORTANT: Never make duplicate tool calls. Each tool call should be unique and purposeful. "
27
- "Process: 1) Search for information ONCE with a clear query, 2) Record the notes ONCE with a descriptive title, "
28
- "3) Only search again if you need different/additional information with a different query. "
29
- "Once you have sufficient notes recorded, immediately hand off control to the WriteAgent. "
30
- "You should have at least some notes on a topic before handing off control to the WriteAgent."
 
 
 
 
 
 
 
 
 
 
31
  ),
 
32
  llm=self.llm,
33
- tools=[search_web, record_notes],
34
  can_handoff_to=["WriteAgent"],
35
  )
36
 
37
- self.write_agent = FunctionAgent(
38
  name="WriteAgent",
39
- description="Useful for writing a structured report on a given topic.",
40
  system_prompt=(
41
- "You are the WriteAgent that can write a structured report on a given topic. "
42
- "IMPORTANT: Never make duplicate tool calls. Write the report only ONCE with all available research. "
43
- "Your report should be in markdown format and include a descriptive title. "
44
- "When calling write_report, provide both the markdown content AND a clear, descriptive title. "
45
- "The content should be grounded in the research notes and well-structured with clear sections. "
46
- "Once the report is written ONCE, immediately hand off control to the ReviewAgent for feedback."
 
 
 
 
 
 
 
 
 
 
 
 
47
  ),
 
48
  llm=self.llm,
49
- tools=[write_report],
50
- can_handoff_to=["ReviewAgent", "ResearchAgent"],
51
  )
52
 
53
- self.review_agent = FunctionAgent(
54
  name="ReviewAgent",
55
- description="Useful for reviewing a report and providing feedback.",
56
  system_prompt=(
57
- "You are the ReviewAgent that can review the report and provide feedback. "
58
- "IMPORTANT: Never make duplicate tool calls. Review the report only ONCE and provide clear feedback. "
59
- "Your review should either APPROVE the current report or request specific changes for the WriteAgent to implement. "
60
- "When APPROVING a report, use clear approval language like 'APPROVED', 'READY', 'EXCELLENT', 'SATISFACTORY', or 'COMPLETE'. "
61
- "If the report meets the requirements and is well-written, APPROVE it to complete the workflow. "
62
- "If you have feedback that requires changes, hand off control to the WriteAgent to implement the changes after submitting the review ONCE."
63
  ),
 
64
  llm=self.llm,
65
- tools=[review_report],
66
- can_handoff_to=["ResearchAgent","WriteAgent"],
67
  )
68
 
69
  self.agent_workflow = AgentWorkflow(
@@ -76,14 +97,9 @@ class TeacherStudentAgentWorkflow:
76
  },
77
  )
78
 
79
- async def get_structured_report(self, handler) -> ReportOutput:
80
- """Extract the structured report from the workflow state."""
81
- try:
82
- final_state = await handler.ctx.get("state")
83
- return get_structured_report_from_state(final_state)
84
- except Exception as e:
85
- print(f"Error getting structured report: {e}")
86
- return None
87
 
88
  async def run_workflow(self, user_msg=None):
89
  if user_msg is None:
@@ -92,7 +108,19 @@ class TeacherStudentAgentWorkflow:
92
  "Briefly describe the history of the internet, including the development of the internet, the development of the web, "
93
  "and the development of the internet in the 21st century."
94
  )
95
- handler = self.agent_workflow.run(user_msg=user_msg)
 
 
 
 
 
 
 
 
 
 
 
 
96
 
97
  current_agent = None
98
  async for event in handler.stream_events():
@@ -105,22 +133,38 @@ class TeacherStudentAgentWorkflow:
105
  print(f"🤖 Agent: {current_agent}")
106
  print(f"{'='*50}\n")
107
 
108
- if isinstance(event, AgentOutput):
109
  if event.response.content:
110
  print("📤 Output:", event.response.content)
111
- if event.tool_calls:
112
  print(
113
  "🛠️ Planning to use tools:",
114
  [call.tool_name for call in event.tool_calls],
115
  )
116
- elif isinstance(event, ToolCallResult):
117
  print(f"🔧 Tool Result ({event.tool_name}):")
118
- print(f" Arguments: {event.tool_kwargs}")
119
  print(f" Output: {event.tool_output}")
120
- elif isinstance(event, ToolCall):
121
  print(f"🔨 Calling Tool: {event.tool_name}")
122
  print(f" With arguments: {event.tool_kwargs}")
123
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  if __name__ == "__main__":
125
  import asyncio
126
  agent = TeacherStudentAgentWorkflow()
 
1
+ from llama_index.llms.huggingface_api import HuggingFaceInferenceAPI
2
+ from tools.simple_tools import (
3
+ search_web_tool, record_notes_tool, write_report_tool, review_report_tool,
4
+ get_workflow_state, reset_workflow_state
5
+ )
6
  from dotenv import load_dotenv
7
  import os
8
+ from llama_index.core.agent.workflow import AgentWorkflow, ReActAgent
9
+ from llama_index.core.workflow import Context
 
 
 
 
 
 
 
10
 
11
  load_dotenv(os.path.join(os.path.dirname(__file__), 'env.local'))
12
 
13
  class TeacherStudentAgentWorkflow:
14
  def __init__(self):
15
+ self.llm = HuggingFaceInferenceAPI(
16
+ model_name="microsoft/Phi-3.5-mini-instruct",
17
+ token=os.getenv("HUGGING_FACE_TOKEN")
18
+ )
19
 
20
+ self.research_agent = ReActAgent(
21
  name="ResearchAgent",
22
+ description="Searches the web and records notes.",
23
  system_prompt=(
24
+ "You are a Research Agent. Your ONLY job is to research and hand off to WriteAgent.\n"
25
+ "\n"
26
+ "STRICT WORKFLOW:\n"
27
+ "1. Use search_web tool to search for information\n"
28
+ "2. Use record_notes tool to save what you found\n"
29
+ "3. Say: 'Research complete. I have gathered sufficient information. Handing off to WriteAgent.'\n"
30
+ "\n"
31
+ "CRITICAL RULES:\n"
32
+ "- You can ONLY use search_web and record_notes tools\n"
33
+ "- You CANNOT write reports - that's WriteAgent's job\n"
34
+ "- You CANNOT use write_report tool - you don't have access to it\n"
35
+ "- After research, you MUST hand off with the exact message above\n"
36
+ "- Do NOT attempt to write any report content yourself\n"
37
+ "\n"
38
+ "AVAILABLE TOOLS: search_web, record_notes\n"
39
+ "HANDOFF MESSAGE: 'Research complete. I have gathered sufficient information. Handing off to WriteAgent.'"
40
  ),
41
+ tools=[search_web_tool, record_notes_tool],
42
  llm=self.llm,
 
43
  can_handoff_to=["WriteAgent"],
44
  )
45
 
46
+ self.write_agent = ReActAgent(
47
  name="WriteAgent",
48
+ description="Writes a structured report based on research notes.",
49
  system_prompt=(
50
+ "You are a Writing Agent. Your purpose is to create a concise, well-structured report.\n"
51
+ "\n"
52
+ "INSTRUCTIONS:\n"
53
+ "1. Check if there's any feedback from ReviewAgent (not 'Review required.')\n"
54
+ "2. If there's feedback, revise the report accordingly\n"
55
+ "3. If no feedback, create initial report based on research\n"
56
+ "4. MUST call write_report tool with these parameters:\n"
57
+ " - report_content: Concise markdown report (200-400 words)\n"
58
+ " - title: Descriptive report title\n"
59
+ "5. Report structure (keep sections brief):\n"
60
+ " - # Main Title\n"
61
+ " - ## Introduction (1-2 sentences)\n"
62
+ " - ## Key Points (2-3 bullet points)\n"
63
+ " - ## Conclusion (1-2 sentences)\n"
64
+ "6. After calling tool: 'Report written. Handing off to ReviewAgent.'\n"
65
+ "\n"
66
+ "CRITICAL: Keep the report_content CONCISE to avoid truncation!\n"
67
+ "You MUST actually call the write_report tool with proper parameters!"
68
  ),
69
+ tools=[write_report_tool],
70
  llm=self.llm,
71
+ can_handoff_to=["ReviewAgent"],
 
72
  )
73
 
74
+ self.review_agent = ReActAgent(
75
  name="ReviewAgent",
76
+ description="Reviews the written report.",
77
  system_prompt=(
78
+ "You are a Reviewing Agent. Your purpose is to review the report quality.\n"
79
+ "1. Check the report content that was written\n"
80
+ "2. Use review_report tool to provide feedback\n"
81
+ "3. If report is good quality, start feedback with 'APPROVED:'\n"
82
+ "4. If needs improvement, provide specific suggestions and hand off to WriteAgent\n"
83
+ "5. Quality criteria: clear structure, sufficient detail, proper formatting"
84
  ),
85
+ tools=[review_report_tool],
86
  llm=self.llm,
87
+ can_handoff_to=["WriteAgent"],
 
88
  )
89
 
90
  self.agent_workflow = AgentWorkflow(
 
97
  },
98
  )
99
 
100
+ def get_final_state(self) -> dict:
101
+ """Get the final workflow state from the simple tools."""
102
+ return get_workflow_state()
 
 
 
 
 
103
 
104
  async def run_workflow(self, user_msg=None):
105
  if user_msg is None:
 
108
  "Briefly describe the history of the internet, including the development of the internet, the development of the web, "
109
  "and the development of the internet in the 21st century."
110
  )
111
+
112
+ # Reset state for new workflow
113
+ reset_workflow_state()
114
+
115
+ # Create context and initialize state
116
+ ctx = Context(self.agent_workflow)
117
+ await ctx.set("state", {
118
+ "research_notes": {},
119
+ "report_content": "Not written yet.",
120
+ "review": "Review required.",
121
+ })
122
+
123
+ handler = self.agent_workflow.run(user_msg=user_msg, ctx=ctx)
124
 
125
  current_agent = None
126
  async for event in handler.stream_events():
 
133
  print(f"🤖 Agent: {current_agent}")
134
  print(f"{'='*50}\n")
135
 
136
+ if hasattr(event, "response") and hasattr(event.response, "content"):
137
  if event.response.content:
138
  print("📤 Output:", event.response.content)
139
+ if hasattr(event, "tool_calls") and event.tool_calls:
140
  print(
141
  "🛠️ Planning to use tools:",
142
  [call.tool_name for call in event.tool_calls],
143
  )
144
+ elif hasattr(event, "tool_name") and hasattr(event, "tool_output"):
145
  print(f"🔧 Tool Result ({event.tool_name}):")
146
+ print(f" Arguments: {getattr(event, 'tool_kwargs', {})}")
147
  print(f" Output: {event.tool_output}")
148
+ elif hasattr(event, "tool_name") and hasattr(event, "tool_kwargs"):
149
  print(f"🔨 Calling Tool: {event.tool_name}")
150
  print(f" With arguments: {event.tool_kwargs}")
151
 
152
+ # After the workflow completes, print the final report
153
+ final_state = self.get_final_state()
154
+ print(f"\n📊 Final State:")
155
+ print(f"Research notes: {len(final_state.get('research_notes', {}))}")
156
+ print(f"Report written: {final_state.get('report_content', 'Not written') != 'Not written yet.'}")
157
+ print(f"Review: {final_state.get('review', 'No review')[:100]}...")
158
+
159
+ if final_state.get("structured_report"):
160
+ print("\n📄 Final Report Generated Successfully!")
161
+ report = final_state["structured_report"]
162
+ print(f"Title: {report['title']}")
163
+ print(f"Word count: {report['word_count']}")
164
+ print(f"Sections: {len(report['sections'])}")
165
+ else:
166
+ print("\n⚠️ No final report was generated by the workflow.")
167
+
168
  if __name__ == "__main__":
169
  import asyncio
170
  agent = TeacherStudentAgentWorkflow()
app.py CHANGED
@@ -3,8 +3,9 @@ from gradio import ChatMessage
3
  import asyncio
4
  import json
5
  import hashlib
 
6
  from agent import TeacherStudentAgentWorkflow
7
- from tools.tavily_search_tool import ReportOutput, get_structured_report_from_state
8
  from llama_index.core.agent.workflow import (
9
  AgentInput,
10
  AgentOutput,
@@ -12,7 +13,7 @@ from llama_index.core.agent.workflow import (
12
  ToolCallResult,
13
  AgentStream,
14
  )
15
- from datetime import datetime
16
 
17
  # Initialize the agent workflow
18
  agent_workflow = None
@@ -26,230 +27,162 @@ def get_agent_workflow():
26
  async def chat_with_agent(message, history):
27
  """
28
  Async chat function that runs the agent workflow and streams each step.
29
- Returns structured report data for separate display.
30
  """
31
- if not message.strip():
32
- yield history, None, gr.JSON(visible=False)
33
- return
34
-
35
- # Add user message to history
36
  history.append(ChatMessage(role="user", content=message))
37
- yield history, None, gr.JSON(visible=False)
 
 
 
 
 
 
38
 
39
  try:
40
- # Get the agent workflow
41
  workflow = get_agent_workflow()
42
 
43
- # Run the workflow with the user message
44
- handler = workflow.agent_workflow.run(user_msg=message)
 
 
 
 
 
45
 
46
- current_agent = None
47
- current_step_messages = []
48
- final_report = None
49
- structured_report_data = None
50
- workflow_state = {}
51
 
52
- # Track recent tool calls to prevent UI duplicates
53
- recent_tool_calls = set()
54
- max_cache_size = 100 # Limit cache size to prevent memory issues
55
 
56
  async for event in handler.stream_events():
57
- # Check if we switched to a new agent
58
- if (
59
- hasattr(event, "current_agent_name")
60
- and event.current_agent_name != current_agent
61
- ):
62
  current_agent = event.current_agent_name
63
-
64
- # Clear tool call tracking when switching agents
65
- recent_tool_calls.clear()
66
-
67
- # Add agent header message
68
- agent_header = ChatMessage(
69
  role="assistant",
70
- content=f"🤖 **{current_agent}** is now working...",
71
  metadata={"title": f"Agent: {current_agent}"}
72
- )
73
- history.append(agent_header)
74
- yield history, final_report, gr.JSON(visible=False)
75
-
76
- # Handle different event types
77
- if isinstance(event, AgentOutput):
78
- if event.response.content:
79
- # Add agent output
80
- output_msg = ChatMessage(
81
- role="assistant",
82
- content=f"📤 **Output:** {event.response.content}",
83
- metadata={"title": f"{current_agent} - Output"}
84
- )
85
- history.append(output_msg)
86
- yield history, final_report, gr.JSON(visible=False)
87
-
88
- if event.tool_calls:
89
- # Show planned tools
90
- tools_list = [call.tool_name for call in event.tool_calls]
91
- tools_msg = ChatMessage(
92
- role="assistant",
93
- content=f"🛠️ **Planning to use tools:** {', '.join(tools_list)}",
94
- metadata={"title": f"{current_agent} - Tool Planning"}
95
- )
96
- history.append(tools_msg)
97
- yield history, final_report, gr.JSON(visible=False)
98
-
99
- elif isinstance(event, ToolCall):
100
- # Create a unique identifier for this tool call using a more robust approach
101
- try:
102
- # Sort the arguments to ensure consistent hashing
103
- sorted_kwargs = json.dumps(event.tool_kwargs, sort_keys=True, default=str)
104
- tool_call_id = f"{event.tool_name}_{hashlib.md5(sorted_kwargs.encode()).hexdigest()}"
105
- except (TypeError, ValueError):
106
- # Fallback for non-serializable arguments
107
- tool_call_id = f"{event.tool_name}_{hash(str(event.tool_kwargs))}"
108
 
109
- # Only show if we haven't seen this exact tool call recently
110
- if tool_call_id not in recent_tool_calls:
111
- recent_tool_calls.add(tool_call_id)
112
-
113
- # Clean up cache if it gets too large
114
- if len(recent_tool_calls) > max_cache_size:
115
- # Remove some old entries (keep the most recent half)
116
- recent_tool_calls = set(list(recent_tool_calls)[-max_cache_size//2:])
117
-
118
- # Show tool being called
119
- tool_msg = ChatMessage(
120
  role="assistant",
121
- content=f"🔨 **Calling Tool:** {event.tool_name}\n**Arguments:** {event.tool_kwargs}",
122
  metadata={"title": f"{current_agent} - Tool Call"}
123
- )
124
- history.append(tool_msg)
125
- yield history, final_report, gr.JSON(visible=False)
126
- else:
127
- # Debug: Log duplicate detection (remove this in production)
128
- print(f"🚫 Duplicate tool call detected and skipped: {event.tool_name} with args {event.tool_kwargs}")
129
- # If it's a duplicate, we simply skip displaying it
130
 
131
  elif isinstance(event, ToolCallResult):
132
- # Show tool results
133
- result_content = str(event.tool_output)
134
- if len(result_content) > 500:
135
- result_content = result_content[:500] + "..."
136
 
137
- # Check if this is a duplicate detection message
138
- is_duplicate = any(word in result_content.lower() for word in ["duplicate", "skipping"])
 
 
139
 
140
- if is_duplicate:
141
- result_msg = ChatMessage(
142
- role="assistant",
143
- content=f"⚠️ **Duplicate Detection ({event.tool_name}):**\n{result_content}",
144
- metadata={"title": f"{current_agent} - Duplicate Skipped"}
145
- )
146
- else:
147
- result_msg = ChatMessage(
148
- role="assistant",
149
- content=f"🔧 **Tool Result ({event.tool_name}):**\n{result_content}",
150
- metadata={"title": f"{current_agent} - Tool Result"}
151
- )
152
-
153
- history.append(result_msg)
154
-
155
- # Track tool results to detect report writing and review approval (only for non-duplicates)
156
- if not is_duplicate:
157
- if event.tool_name == "write_report":
158
- workflow_state["has_report"] = True
159
- elif event.tool_name == "review_report" and current_agent == "ReviewAgent":
160
- workflow_state["has_review"] = True
161
- # Check if review indicates approval (expanded keywords)
162
- approval_keywords = [
163
- "approved", "ready", "good", "excellent", "satisfactory",
164
- "complete", "accept", "final", "publish", "meets", "solid",
165
- "well-written", "comprehensive", "thorough"
166
- ]
167
- if any(word in result_content.lower() for word in approval_keywords):
168
- workflow_state["review_approved"] = True
169
-
170
- yield history, final_report, gr.JSON(visible=False)
171
 
172
- # Get the final state to extract the structured report
173
- try:
174
- final_state = await handler.ctx.get("state")
175
- if final_state:
176
- # Get structured report data
177
- structured_report = get_structured_report_from_state(final_state)
178
- if structured_report:
179
- # Include ALL fields from the Pydantic model, including content
 
 
 
 
 
 
 
 
 
 
 
 
180
  structured_report_data = {
181
- "title": structured_report.title,
182
- "abstract": structured_report.abstract,
183
- "content": structured_report.content,
184
- "sections": structured_report.sections,
185
- "word_count": structured_report.word_count,
186
- "generated_at": structured_report.generated_at.strftime("%Y-%m-%d %H:%M:%S"),
187
- "sources_used": structured_report.sources_used or []
188
  }
189
-
190
- # Show the final report if we have one
191
- # Check if review indicates approval OR if we just have a completed report
192
- if (workflow_state.get("has_report") and
193
- (workflow_state.get("review_approved") or workflow_state.get("has_review"))):
194
- final_report = gr.Markdown(structured_report.content, visible=True)
195
- elif workflow_state.get("has_report"):
196
- # Show report even if not reviewed yet, but mark it as preliminary
197
- final_report = gr.Markdown(
198
- f"## 📝 Preliminary Report (Pending Review)\n\n{structured_report.content}",
199
- visible=True
200
- )
201
-
202
- # Fallback to regular content if structured report is not available
203
- elif "report_content" in final_state:
204
- report_content = final_state["report_content"]
205
- if report_content and report_content != "Not written yet.":
206
- # Create basic structured data from the raw content
207
  structured_report_data = {
208
- "title": "Generated Report",
209
- "content": report_content,
210
- "generated_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
211
- "word_count": len(report_content.split()),
212
- "sources_used": list(final_state.get("research_notes", {}).keys()) if "research_notes" in final_state else []
 
213
  }
214
-
215
- if workflow_state.get("has_report"):
216
- final_report = gr.Markdown(report_content, visible=True)
217
- except Exception as state_error:
218
- print(f"Could not extract final state: {state_error}")
219
- # Try to show any report that was generated during the conversation
220
- if workflow_state.get("has_report"):
221
- final_report = gr.Markdown("Report was generated but could not be extracted from final state.", visible=True)
222
 
223
- # Add completion message
224
- completion_msg = ChatMessage(
225
  role="assistant",
226
- content="✅ **Workflow completed!** The agent collaboration has finished.",
227
  metadata={"title": "Workflow Complete"}
228
- )
229
- history.append(completion_msg)
230
-
231
- # Ensure we show the final report if we have structured data but no report was set
232
- if structured_report_data and final_report is None:
233
- if "content" in structured_report_data:
234
- final_report = gr.Markdown(structured_report_data["content"], visible=True)
235
-
236
- # Create the structured report display component
237
- structured_report_display = gr.JSON(
238
- value=structured_report_data,
239
- visible=bool(structured_report_data)
240
- )
241
-
242
- yield history, final_report, structured_report_display
243
 
 
 
244
  except Exception as e:
245
- # Handle errors gracefully
246
- error_msg = ChatMessage(
247
- role="assistant",
248
- content=f"❌ **Error:** {str(e)}",
249
- metadata={"title": "Error"}
250
- )
251
- history.append(error_msg)
252
- yield history, None, gr.JSON(visible=False)
253
 
254
  def like_feedback(evt: gr.LikeData):
255
  """Handle user feedback on messages."""
@@ -279,67 +212,43 @@ with gr.Blocks(title="Teacher-Student Agent Workflow", theme=gr.themes.Soft()) a
279
  """)
280
 
281
  chatbot = gr.Chatbot(
 
282
  type="messages",
283
  height=600,
284
  show_copy_button=True,
285
- placeholder="<strong>Welcome to the Teacher-Student Agent Workflow!</strong><br>Ask me to write a report on any topic and watch the agents collaborate.",
286
  render_markdown=True
287
  )
288
 
289
- textbox = gr.Textbox(
290
- placeholder="Enter your request (e.g., 'Write a report on artificial intelligence')",
291
- container=False,
292
- scale=7
293
- )
294
-
295
- # Create the final report output component
296
- final_report_output = gr.Markdown(
297
- label="📄 Final Approved Report",
298
- visible=False,
299
- render=False
300
- )
301
-
302
- # Create structured report metadata component
303
- structured_report_json = gr.JSON(
304
- label="📊 Report Metadata",
305
- visible=False,
306
- render=False
307
- )
308
-
309
- # Set up the chat interface with additional outputs
310
- chat_interface = gr.ChatInterface(
311
- fn=chat_with_agent,
312
- chatbot=chatbot,
313
- textbox=textbox,
314
- type="messages",
315
  examples=[
316
  "Write a report on the history of artificial intelligence",
317
  "Create a report about renewable energy technologies",
318
  "Write a report on the impact of social media on society",
319
- "Generate a report about space exploration achievements"
320
- ],
321
- example_labels=[
322
- "AI History Report",
323
- "Renewable Energy Report",
324
- "Social Media Impact Report",
325
- "Space Exploration Report"
326
  ],
327
- cache_examples=False,
328
- additional_outputs=[final_report_output, structured_report_json]
329
  )
330
 
331
- # Add feedback handling
332
- chatbot.like(like_feedback)
333
-
334
- # Render the final report output in a separate section
335
- with gr.Row():
336
- with gr.Column(scale=2):
337
- gr.Markdown("### 📋 Final Report")
338
- final_report_output.render()
339
- with gr.Column(scale=1):
340
- gr.Markdown("### 📊 Report Metadata")
341
- structured_report_json.render()
342
-
343
  gr.Markdown("""
344
  ### How it works:
345
  1. **ResearchAgent** searches for information and takes notes
@@ -350,5 +259,20 @@ with gr.Blocks(title="Teacher-Student Agent Workflow", theme=gr.themes.Soft()) a
350
  Watch the real-time collaboration between agents as they work together!
351
  """)
352
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
353
  if __name__ == "__main__":
354
  demo.launch()
 
3
  import asyncio
4
  import json
5
  import hashlib
6
+ from datetime import datetime
7
  from agent import TeacherStudentAgentWorkflow
8
+ from tools.simple_tools import get_workflow_state
9
  from llama_index.core.agent.workflow import (
10
  AgentInput,
11
  AgentOutput,
 
13
  ToolCallResult,
14
  AgentStream,
15
  )
16
+ from llama_index.core.workflow import Context
17
 
18
  # Initialize the agent workflow
19
  agent_workflow = None
 
27
  async def chat_with_agent(message, history):
28
  """
29
  Async chat function that runs the agent workflow and streams each step.
 
30
  """
31
+ history = history or []
 
 
 
 
32
  history.append(ChatMessage(role="user", content=message))
33
+
34
+ # Initial yield to show user message immediately
35
+ yield history, None, None, gr.update(value="", interactive=False)
36
+
37
+ final_report_content = None
38
+ structured_report_data = None
39
+ displayed_tool_calls = set()
40
 
41
  try:
 
42
  workflow = get_agent_workflow()
43
 
44
+ # Create context and initialize state properly
45
+ ctx = Context(workflow.agent_workflow)
46
+ await ctx.set("state", {
47
+ "research_notes": {},
48
+ "report_content": "Not written yet.",
49
+ "review": "Review required.",
50
+ })
51
 
52
+ handler = workflow.agent_workflow.run(user_msg=message, ctx=ctx)
 
 
 
 
53
 
54
+ current_agent = None
 
 
55
 
56
  async for event in handler.stream_events():
57
+ print(f"DEBUG: Event type: {type(event).__name__}")
58
+
59
+ if hasattr(event, "current_agent_name") and event.current_agent_name != current_agent:
 
 
60
  current_agent = event.current_agent_name
61
+ history.append(ChatMessage(
 
 
 
 
 
62
  role="assistant",
63
+ content=f"**🤖 Agent: {current_agent}**",
64
  metadata={"title": f"Agent: {current_agent}"}
65
+ ))
66
+ yield history, final_report_content, structured_report_data, gr.update(interactive=False)
67
+
68
+ if isinstance(event, ToolCall):
69
+ tool_call_kwargs_str = json.dumps(getattr(event, 'tool_kwargs', {}), sort_keys=True)
70
+ tool_call_key = f"{current_agent}:{event.tool_name}:{hashlib.md5(tool_call_kwargs_str.encode()).hexdigest()[:8]}"
71
+ print(f"DEBUG: ToolCall detected - Agent: {current_agent}, Tool: {event.tool_name}, Args: {getattr(event, 'tool_kwargs', {})}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
+ if tool_call_key not in displayed_tool_calls:
74
+ args_preview = str(getattr(event, 'tool_kwargs', {}))[:100] + "..." if len(str(getattr(event, 'tool_kwargs', {}))) > 100 else str(getattr(event, 'tool_kwargs', {}))
75
+ history.append(ChatMessage(
 
 
 
 
 
 
 
 
76
  role="assistant",
77
+ content=f"**🔨 Calling Tool:** `{event.tool_name}`\n**Arguments:** {args_preview}",
78
  metadata={"title": f"{current_agent} - Tool Call"}
79
+ ))
80
+ displayed_tool_calls.add(tool_call_key)
81
+ yield history, final_report_content, structured_report_data, gr.update(interactive=False)
 
 
 
 
82
 
83
  elif isinstance(event, ToolCallResult):
84
+ print(f"DEBUG: ToolCallResult - Tool: {getattr(event, 'tool_name', 'unknown')}, Output: {getattr(event, 'tool_output', 'no output')}")
 
 
 
85
 
86
+ # Show tool result in UI
87
+ tool_output = getattr(event, 'tool_output', 'No output')
88
+ tool_name = getattr(event, 'tool_name', 'unknown')
89
+ output_preview = str(tool_output)[:200] + "..." if len(str(tool_output)) > 200 else str(tool_output)
90
 
91
+ history.append(ChatMessage(
92
+ role="assistant",
93
+ content=f"**🔧 Tool Result ({tool_name}):**\n{output_preview}",
94
+ metadata={"title": f"{current_agent} - Tool Result"}
95
+ ))
96
+ yield history, final_report_content, structured_report_data, gr.update(interactive=False)
97
+
98
+ elif isinstance(event, AgentOutput) and event.response.content:
99
+ print(f"DEBUG: AgentOutput from {current_agent}: {event.response.content}")
100
+ # This is the agent's final thought or handoff message
101
+ history.append(ChatMessage(
102
+ role="assistant",
103
+ content=f"**📤 Thought:** {event.response.content}",
104
+ metadata={"title": f"{current_agent} - Output"}
105
+ ))
106
+ yield history, final_report_content, structured_report_data, gr.update(interactive=False)
107
+
108
+ # Final state extraction - use the simple tools state
109
+ print("DEBUG: Workflow completed, extracting final state...")
110
+ final_state = get_workflow_state()
111
+ print(f"DEBUG: Final state keys: {final_state.keys() if final_state else 'None'}")
 
 
 
 
 
 
 
 
 
 
112
 
113
+ if final_state:
114
+ print(f"DEBUG: Final state content: {json.dumps(final_state, indent=2, default=str)}")
115
+
116
+ # Check for research notes
117
+ research_notes = final_state.get("research_notes", {})
118
+ print(f"DEBUG: Research notes found: {len(research_notes)} items")
119
+ for title, content in research_notes.items():
120
+ print(f"DEBUG: Research note '{title}': {content[:100]}..." if len(content) > 100 else f"DEBUG: Research note '{title}': {content}")
121
+
122
+ # Check if we have a structured report
123
+ if final_state.get("structured_report"):
124
+ structured_report_data = final_state["structured_report"]
125
+ final_report_content = structured_report_data.get("content", "*Report content not found in structured report.*")
126
+ print(f"DEBUG: Found structured report with content length: {len(final_report_content) if final_report_content else 0}")
127
+ else:
128
+ # Fallback: try to get report_content directly from state
129
+ final_report_content = final_state.get("report_content", None)
130
+ if final_report_content and final_report_content != "Not written yet.":
131
+ print(f"DEBUG: Found report_content directly in state with length: {len(final_report_content)}")
132
+ # Create minimal structured data for JSON display
133
  structured_report_data = {
134
+ "title": "Generated Report",
135
+ "content": final_report_content,
136
+ "word_count": len(final_report_content.split()),
137
+ "generated_at": datetime.now().isoformat(),
138
+ "research_notes_count": len(final_state.get("research_notes", {}))
 
 
139
  }
140
+ else:
141
+ print("DEBUG: No valid report content found in final state")
142
+ print(f"DEBUG: report_content value: '{final_report_content}'")
143
+ # If we have research notes but no report, show that as partial success
144
+ if research_notes:
145
+ final_report_content = f"**Research completed but report not written.**\n\n**Research Notes:**\n\n"
146
+ for title, content in research_notes.items():
147
+ final_report_content += f"### {title}\n{content}\n\n"
 
 
 
 
 
 
 
 
 
 
148
  structured_report_data = {
149
+ "title": "Research Notes (Report Incomplete)",
150
+ "content": final_report_content,
151
+ "word_count": len(final_report_content.split()),
152
+ "generated_at": datetime.now().isoformat(),
153
+ "research_notes_count": len(research_notes),
154
+ "status": "incomplete"
155
  }
156
+ print(f"DEBUG: Created fallback report from research notes")
157
+ else:
158
+ final_report_content = None
159
+ structured_report_data = None
160
+ else:
161
+ print("DEBUG: No final state retrieved")
162
+ final_report_content = None
163
+ structured_report_data = None
164
 
165
+ history.append(ChatMessage(
 
166
  role="assistant",
167
+ content="✅ **Workflow completed!**",
168
  metadata={"title": "Workflow Complete"}
169
+ ))
170
+
171
+ if final_report_content:
172
+ final_report_update = gr.update(value=final_report_content, visible=True)
173
+ json_report_update = gr.update(value=structured_report_data, visible=True) if structured_report_data else gr.update(visible=False)
174
+ else:
175
+ final_report_update = gr.update(value="*No final report was generated. Check the workflow execution above.*", visible=True)
176
+ json_report_update = gr.update(visible=False)
 
 
 
 
 
 
 
177
 
178
+ yield history, final_report_update, json_report_update, gr.update(interactive=True, placeholder="Enter your next request...")
179
+
180
  except Exception as e:
181
+ print(f"ERROR in chat_with_agent: {e}")
182
+ import traceback
183
+ traceback.print_exc()
184
+ history.append(ChatMessage(role="assistant", content=f"❌ **Error:** {str(e)}", metadata={"title": "Error"}))
185
+ yield history, gr.update(visible=False), gr.update(visible=False), gr.update(interactive=True)
 
 
 
186
 
187
  def like_feedback(evt: gr.LikeData):
188
  """Handle user feedback on messages."""
 
212
  """)
213
 
214
  chatbot = gr.Chatbot(
215
+ label="Agent Workflow",
216
  type="messages",
217
  height=600,
218
  show_copy_button=True,
219
+ placeholder="Ask me to write a report on any topic...",
220
  render_markdown=True
221
  )
222
 
223
+ with gr.Row():
224
+ textbox = gr.Textbox(
225
+ placeholder="Enter your request...",
226
+ container=False,
227
+ scale=7
228
+ )
229
+ submit_btn = gr.Button("Submit", variant="primary", scale=1)
230
+
231
+ with gr.Row():
232
+ with gr.Column(scale=2):
233
+ final_report_output = gr.Textbox(
234
+ label="📄 Final Report",
235
+ interactive=False,
236
+ lines=20,
237
+ show_copy_button=True,
238
+ visible=False
239
+ )
240
+ with gr.Column(scale=1):
241
+ structured_report_json = gr.JSON(label="📊 Report Metadata", visible=False)
242
+
243
+ gr.Examples(
 
 
 
 
 
244
  examples=[
245
  "Write a report on the history of artificial intelligence",
246
  "Create a report about renewable energy technologies",
247
  "Write a report on the impact of social media on society",
 
 
 
 
 
 
 
248
  ],
249
+ inputs=textbox,
 
250
  )
251
 
 
 
 
 
 
 
 
 
 
 
 
 
252
  gr.Markdown("""
253
  ### How it works:
254
  1. **ResearchAgent** searches for information and takes notes
 
259
  Watch the real-time collaboration between agents as they work together!
260
  """)
261
 
262
+ # Event handlers
263
+ submit_btn.click(
264
+ chat_with_agent,
265
+ inputs=[textbox, chatbot],
266
+ outputs=[chatbot, final_report_output, structured_report_json, textbox],
267
+ queue=True
268
+ )
269
+
270
+ textbox.submit(
271
+ chat_with_agent,
272
+ inputs=[textbox, chatbot],
273
+ outputs=[chatbot, final_report_output, structured_report_json, textbox],
274
+ queue=True
275
+ )
276
+
277
  if __name__ == "__main__":
278
  demo.launch()
pyproject.toml CHANGED
@@ -9,5 +9,6 @@ dependencies = [
9
  "dotenv>=0.9.9",
10
  "gradio>=5.33.0",
11
  "llama-index>=0.12.40",
 
12
  "tavily-python>=0.7.5",
13
  ]
 
9
  "dotenv>=0.9.9",
10
  "gradio>=5.33.0",
11
  "llama-index>=0.12.40",
12
+ "llama-index-llms-huggingface-api>=0.5.0",
13
  "tavily-python>=0.7.5",
14
  ]
requirements.txt CHANGED
@@ -48,6 +48,7 @@ llama-index-core==0.12.40
48
  llama-index-embeddings-huggingface==0.5.4
49
  llama-index-embeddings-openai==0.3.1
50
  llama-index-indices-managed-llama-cloud==0.7.4
 
51
  llama-index-llms-ollama==0.6.2
52
  llama-index-llms-openai==0.4.3
53
  llama-index-multi-modal-llms-openai==0.5.1
 
48
  llama-index-embeddings-huggingface==0.5.4
49
  llama-index-embeddings-openai==0.3.1
50
  llama-index-indices-managed-llama-cloud==0.7.4
51
+ llama-index-llms-huggingface-api==0.5.1
52
  llama-index-llms-ollama==0.6.2
53
  llama-index-llms-openai==0.4.3
54
  llama-index-multi-modal-llms-openai==0.5.1
simple_test.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Simple test to manually check tool execution during workflow streaming."""
2
+
3
+ import asyncio
4
+ import os
5
+ from dotenv import load_dotenv
6
+ from agent import TeacherStudentAgentWorkflow
7
+ from tools.simple_tools import get_workflow_state, reset_workflow_state
8
+
9
+ load_dotenv(os.path.join(os.path.dirname(__file__), 'env.local'))
10
+
11
+ async def test_workflow_tool_execution():
12
+ """Monitor tool execution during workflow execution."""
13
+ print("Testing workflow tool execution...")
14
+
15
+ # Reset state
16
+ reset_workflow_state()
17
+ print(f"Initial state: {get_workflow_state()}")
18
+
19
+ workflow = TeacherStudentAgentWorkflow()
20
+
21
+ # Start workflow
22
+ print("\nStarting workflow...")
23
+ handler = workflow.agent_workflow.run(user_msg="Write a short report about renewable energy")
24
+
25
+ tool_calls_seen = []
26
+ tool_results_seen = []
27
+
28
+ print("\nMonitoring events...")
29
+ async for event in handler.stream_events():
30
+ event_type = type(event).__name__
31
+ print(f"📅 Event: {event_type}")
32
+
33
+ # Check for agent outputs and their content
34
+ if hasattr(event, 'response') and hasattr(event.response, 'content'):
35
+ if event.response.content:
36
+ print(f" 💬 Agent Response: {event.response.content}")
37
+
38
+ # Check if this looks like a handoff message
39
+ content = event.response.content.lower()
40
+ if 'handoff' in content or 'handing off' in content:
41
+ print(f" 🔄 HANDOFF DETECTED: {event.response.content}")
42
+
43
+ # Monitor for tool-related events
44
+ if hasattr(event, 'tool_name') and hasattr(event, 'tool_kwargs'):
45
+ tool_call_info = {
46
+ 'tool_name': event.tool_name,
47
+ 'tool_kwargs': event.tool_kwargs,
48
+ 'event_type': event_type
49
+ }
50
+ tool_calls_seen.append(tool_call_info)
51
+ print(f" 🔨 Tool Call: {event.tool_name}")
52
+ print(f" Args: {event.tool_kwargs}")
53
+
54
+ # Check state after this tool call event
55
+ current_state = get_workflow_state()
56
+ if event.tool_name == 'write_report':
57
+ print(f" State after write_report call:")
58
+ print(f" - report_content: {current_state.get('report_content', 'Not written')[:50]}...")
59
+ print(f" - structured_report: {current_state.get('structured_report') is not None}")
60
+
61
+ # Check for agent name changes (handoffs)
62
+ if hasattr(event, 'current_agent_name'):
63
+ print(f" 🤖 Current Agent: {event.current_agent_name}")
64
+
65
+ if hasattr(event, 'tool_output'):
66
+ tool_result_info = {
67
+ 'tool_name': getattr(event, 'tool_name', 'unknown'),
68
+ 'tool_output': event.tool_output,
69
+ 'event_type': event_type
70
+ }
71
+ tool_results_seen.append(tool_result_info)
72
+ print(f" 🔧 Tool Result: {getattr(event, 'tool_name', 'unknown')}")
73
+ print(f" Output: {str(event.tool_output)[:100]}...")
74
+
75
+ print(f"\n📊 Summary:")
76
+ print(f"Tool calls seen: {len(tool_calls_seen)}")
77
+ print(f"Tool results seen: {len(tool_results_seen)}")
78
+
79
+ for i, call in enumerate(tool_calls_seen):
80
+ print(f" Call {i+1}: {call['tool_name']} ({call['event_type']})")
81
+
82
+ for i, result in enumerate(tool_results_seen):
83
+ print(f" Result {i+1}: {result['tool_name']} ({result['event_type']})")
84
+
85
+ # Final state check
86
+ final_state = get_workflow_state()
87
+ print(f"\nFinal state:")
88
+ print(f"- Research notes: {len(final_state.get('research_notes', {}))}")
89
+ print(f"- Report content: {final_state.get('report_content', 'Not written')[:100]}...")
90
+ print(f"- Has structured report: {final_state.get('structured_report') is not None}")
91
+
92
+ # Try to identify the issue
93
+ write_report_calls = [c for c in tool_calls_seen if c['tool_name'] == 'write_report']
94
+ write_report_results = [r for r in tool_results_seen if r['tool_name'] == 'write_report']
95
+
96
+ print(f"\nDiagnosis:")
97
+ print(f"- write_report calls: {len(write_report_calls)}")
98
+ print(f"- write_report results: {len(write_report_results)}")
99
+
100
+ if write_report_calls and not write_report_results:
101
+ print("❌ ISSUE: write_report tool was called but no results were seen!")
102
+ print("This suggests the tool function is never actually executed.")
103
+ elif len(write_report_calls) != len(write_report_results):
104
+ print(f"❌ ISSUE: Mismatch between calls ({len(write_report_calls)}) and results ({len(write_report_results)})")
105
+ else:
106
+ print("✅ Tool call/result count matches")
107
+
108
+ if __name__ == "__main__":
109
+ asyncio.run(test_workflow_tool_execution())
test_tools.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ from llama_index.core.workflow import Context
3
+ from llama_index.core.agent.workflow import AgentWorkflow
4
+ from tools.tavily_search_tool import (
5
+ search_web, record_notes, write_report, review_report,
6
+ SearchWebArgs, RecordNotesArgs, WriteReportArgs, ReviewReportArgs
7
+ )
8
+
9
+ async def test_tools():
10
+ """Test all tools with the new Pydantic signatures."""
11
+ print("Testing tools with new Pydantic model arguments...")
12
+
13
+ # Create a simple workflow for context
14
+ workflow = AgentWorkflow(agents=[], root_agent=None)
15
+ ctx = Context(workflow)
16
+
17
+ # Initialize state
18
+ await ctx.set("state", {
19
+ "research_notes": {},
20
+ "report_content": "Not written yet.",
21
+ "review": "Review required.",
22
+ })
23
+
24
+ print("\n1. Testing search_web...")
25
+ try:
26
+ search_args = SearchWebArgs(query="artificial intelligence history")
27
+ search_result = await search_web(search_args)
28
+ print(f"✅ search_web worked! Result length: {len(search_result)}")
29
+ print(f"Preview: {search_result[:200]}...")
30
+ except Exception as e:
31
+ print(f"❌ search_web failed: {e}")
32
+
33
+ print("\n2. Testing record_notes...")
34
+ try:
35
+ notes_args = RecordNotesArgs(
36
+ notes="Test research notes about AI history",
37
+ notes_title="AI History Overview"
38
+ )
39
+ notes_result = await record_notes(ctx, notes_args)
40
+ print(f"✅ record_notes worked! Result: {notes_result}")
41
+
42
+ # Check state
43
+ state = await ctx.get("state")
44
+ print(f"State after notes: {list(state.keys())}")
45
+ print(f"Research notes: {state.get('research_notes', {})}")
46
+ except Exception as e:
47
+ print(f"❌ record_notes failed: {e}")
48
+
49
+ print("\n3. Testing write_report...")
50
+ try:
51
+ report_args = WriteReportArgs(
52
+ report_content="""# Artificial Intelligence History
53
+
54
+ ## Introduction
55
+ This is a test report about AI history.
56
+
57
+ ## Early Development
58
+ AI began in the 1950s with researchers like Alan Turing.
59
+
60
+ ## Modern Era
61
+ Today, AI includes machine learning and deep learning.
62
+
63
+ ## Conclusion
64
+ AI continues to evolve rapidly.""",
65
+ title="Test AI History Report"
66
+ )
67
+ report_result = await write_report(ctx, report_args)
68
+ print(f"✅ write_report worked! Result: {report_result}")
69
+
70
+ # Check state
71
+ state = await ctx.get("state")
72
+ print(f"Report content length: {len(state.get('report_content', ''))}")
73
+ print(f"Has structured report: {'structured_report' in state}")
74
+ except Exception as e:
75
+ print(f"❌ write_report failed: {e}")
76
+
77
+ print("\n4. Testing review_report...")
78
+ try:
79
+ review_args = ReviewReportArgs(review="APPROVED: The report looks good!")
80
+ review_result = await review_report(ctx, review_args)
81
+ print(f"✅ review_report worked! Result: {review_result}")
82
+
83
+ # Check final state
84
+ state = await ctx.get("state")
85
+ print(f"Final review: {state.get('review', 'No review')}")
86
+ except Exception as e:
87
+ print(f"❌ review_report failed: {e}")
88
+
89
+ print("\n5. Final state check...")
90
+ final_state = await ctx.get("state")
91
+ print(f"Final state keys: {list(final_state.keys())}")
92
+ print(f"Research notes count: {len(final_state.get('research_notes', {}))}")
93
+ print(f"Report written: {final_state.get('report_content', 'Not written') != 'Not written yet.'}")
94
+
95
+ if __name__ == "__main__":
96
+ asyncio.run(test_tools())
tools/__pycache__/simple_tools.cpython-313.pyc ADDED
Binary file (10.3 kB). View file
 
tools/__pycache__/tavily_search_tool.cpython-313.pyc CHANGED
Binary files a/tools/__pycache__/tavily_search_tool.cpython-313.pyc and b/tools/__pycache__/tavily_search_tool.cpython-313.pyc differ
 
tools/simple_tools.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Simple synchronous tools for LlamaIndex ReActAgent."""
2
+
3
+ import os
4
+ import time
5
+ import hashlib
6
+ import json
7
+ from typing import Optional
8
+ from datetime import datetime
9
+ from tavily import TavilyClient # Use sync client
10
+ from llama_index.core.tools import FunctionTool
11
+
12
+ # Global state store - simple in-memory storage
13
+ _workflow_state = {
14
+ "research_notes": {},
15
+ "report_content": "Not written yet.",
16
+ "review": "Review required.",
17
+ "structured_report": None
18
+ }
19
+
20
+ # Global cache to track recent tool calls
21
+ _tool_call_cache = {}
22
+ _cache_timeout = 30
23
+
24
+ def _generate_call_hash(tool_name: str, **kwargs) -> str:
25
+ """Generate a hash for tool call deduplication."""
26
+ call_data = {"tool": tool_name, "args": kwargs}
27
+ call_str = json.dumps(call_data, sort_keys=True)
28
+ return hashlib.md5(call_str.encode()).hexdigest()
29
+
30
+ def _should_execute_call(tool_name: str, **kwargs) -> bool:
31
+ """Check if a tool call should be executed or if it's a duplicate."""
32
+ current_time = time.time()
33
+ call_hash = _generate_call_hash(tool_name, **kwargs)
34
+
35
+ # Clean up old cache entries
36
+ expired_keys = [k for k, v in _tool_call_cache.items() if current_time - v > _cache_timeout]
37
+ for key in expired_keys:
38
+ del _tool_call_cache[key]
39
+
40
+ # Check if this call was made recently
41
+ if call_hash in _tool_call_cache:
42
+ return False
43
+
44
+ # Record this call
45
+ _tool_call_cache[call_hash] = current_time
46
+ return True
47
+
48
+ def search_web(query: str) -> str:
49
+ """Search the web for information on a given query."""
50
+ try:
51
+ print(f"DEBUG: search_web called with query: '{query}'")
52
+
53
+ # Check for duplicate calls
54
+ if not _should_execute_call("search_web", query=query):
55
+ return f"Duplicate search call detected for query: '{query}'. Skipping to avoid redundant API calls."
56
+
57
+ # Use synchronous Tavily client
58
+ client = TavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
59
+ result = client.search(query)
60
+
61
+ print(f"DEBUG: search_web executed successfully for query: '{query}'")
62
+ return str(result)
63
+ except Exception as e:
64
+ error_msg = f"Search failed: {str(e)}"
65
+ print(f"ERROR: search_web failed: {e}")
66
+ return error_msg
67
+
68
+ def record_notes(notes: str, notes_title: str) -> str:
69
+ """Record notes on a given topic with a title."""
70
+ try:
71
+ print(f"DEBUG: record_notes called with title: '{notes_title}', notes length: {len(notes)}")
72
+
73
+ # Check for duplicate calls
74
+ if not _should_execute_call("record_notes", notes=notes, notes_title=notes_title):
75
+ return f"Duplicate notes recording detected for title: '{notes_title}'. Skipping to avoid redundant recording."
76
+
77
+ # Store in global state
78
+ _workflow_state["research_notes"][notes_title] = notes
79
+
80
+ print(f"DEBUG: Notes stored. Total research notes: {len(_workflow_state['research_notes'])}")
81
+ return f"Notes recorded successfully with title: '{notes_title}'. Total notes: {len(_workflow_state['research_notes'])}"
82
+ except Exception as e:
83
+ error_msg = f"Failed to record notes: {str(e)}"
84
+ print(f"ERROR: record_notes failed: {e}")
85
+ return error_msg
86
+
87
+ def write_report(report_content: str, title: str = "Research Report") -> str:
88
+ """Write a structured report with the given content and title."""
89
+ try:
90
+ print(f"DEBUG: write_report FUNCTION ENTERED with title: '{title}', content length: {len(report_content)}")
91
+ print(f"DEBUG: Function arguments - report_content type: {type(report_content)}, title type: {type(title)}")
92
+
93
+ # Check for duplicate calls
94
+ if not _should_execute_call("write_report", report_content=report_content, title=title):
95
+ print("DEBUG: Duplicate call detected, returning early")
96
+ return "Duplicate report writing detected. Skipping to avoid redundant report generation."
97
+
98
+ print("DEBUG: Processing report content...")
99
+
100
+ # Extract sections from markdown content
101
+ import re
102
+ sections = re.findall(r'^#{1,3}\s+(.+)$', report_content, re.MULTILINE)
103
+ print(f"DEBUG: Found {len(sections)} sections: {sections}")
104
+
105
+ # Calculate word count
106
+ word_count = len(report_content.split())
107
+ print(f"DEBUG: Word count: {word_count}")
108
+
109
+ # Extract abstract (first paragraph after title)
110
+ lines = report_content.split('\n')
111
+ abstract = ""
112
+ for line in lines:
113
+ if line.strip() and not line.startswith('#'):
114
+ abstract = line.strip()
115
+ break
116
+ print(f"DEBUG: Abstract: {abstract[:100]}...")
117
+
118
+ # Create structured report
119
+ structured_report = {
120
+ "title": title,
121
+ "abstract": abstract[:200] + "..." if len(abstract) > 200 else abstract,
122
+ "content": report_content,
123
+ "sections": sections,
124
+ "word_count": word_count,
125
+ "generated_at": datetime.now().isoformat(),
126
+ "sources_used": list(_workflow_state["research_notes"].keys())
127
+ }
128
+ print("DEBUG: Structured report created")
129
+
130
+ # Store in global state
131
+ print("DEBUG: Storing in global state...")
132
+ _workflow_state["report_content"] = report_content
133
+ _workflow_state["structured_report"] = structured_report
134
+
135
+ print(f"DEBUG: Report stored successfully. Word count: {word_count}, Sections: {len(sections)}")
136
+ print(f"DEBUG: State keys now: {list(_workflow_state.keys())}")
137
+ print(f"DEBUG: State report_content length: {len(_workflow_state['report_content'])}")
138
+
139
+ result = f"Report written successfully! Title: '{title}', Word count: {word_count}, Sections: {len(sections)}"
140
+ print(f"DEBUG: Returning result: {result}")
141
+ return result
142
+ except Exception as e:
143
+ error_msg = f"Failed to write report: {str(e)}"
144
+ print(f"ERROR: write_report failed: {e}")
145
+ import traceback
146
+ traceback.print_exc()
147
+ return error_msg
148
+
149
+ def review_report(review: str) -> str:
150
+ """Review a report and provide feedback."""
151
+ try:
152
+ print(f"DEBUG: review_report called with review: '{review[:100]}...'")
153
+
154
+ # Check for duplicate calls
155
+ if not _should_execute_call("review_report", review=review):
156
+ return "Duplicate review detected. Skipping to avoid redundant review submission."
157
+
158
+ # Store review in global state
159
+ _workflow_state["review"] = review
160
+
161
+ print(f"DEBUG: Review stored successfully")
162
+ return f"Report reviewed successfully. Review: {review[:100]}{'...' if len(review) > 100 else ''}"
163
+ except Exception as e:
164
+ error_msg = f"Failed to review report: {str(e)}"
165
+ print(f"ERROR: review_report failed: {e}")
166
+ return error_msg
167
+
168
+ def get_workflow_state() -> dict:
169
+ """Get the current workflow state."""
170
+ return _workflow_state.copy()
171
+
172
+ def reset_workflow_state():
173
+ """Reset the workflow state."""
174
+ global _workflow_state
175
+ _workflow_state = {
176
+ "research_notes": {},
177
+ "report_content": "Not written yet.",
178
+ "review": "Review required.",
179
+ "structured_report": None
180
+ }
181
+
182
+ # Create LlamaIndex FunctionTool instances with better descriptions
183
+ search_web_tool = FunctionTool.from_defaults(
184
+ fn=search_web,
185
+ name="search_web",
186
+ description=(
187
+ "Search the web for information on any topic. "
188
+ "Input: A search query string. "
189
+ "Output: Search results containing relevant information. "
190
+ "Use this to gather facts and information about your research topic."
191
+ ),
192
+ )
193
+
194
+ record_notes_tool = FunctionTool.from_defaults(
195
+ fn=record_notes,
196
+ name="record_notes",
197
+ description=(
198
+ "Record research notes with a descriptive title. "
199
+ "Input: notes (string) - the content to save, notes_title (string) - a title for the notes. "
200
+ "Output: Confirmation that notes were saved. "
201
+ "Use this after searching to save important information you found."
202
+ ),
203
+ )
204
+
205
+ write_report_tool = FunctionTool.from_defaults(
206
+ fn=write_report,
207
+ name="write_report",
208
+ description=(
209
+ "Write a comprehensive markdown report. "
210
+ "Input: report_content (string) - full markdown report content, title (string, optional) - report title. "
211
+ "Output: Confirmation that report was written. "
212
+ "The report_content should be well-structured markdown with headers, sections, and detailed content."
213
+ ),
214
+ )
215
+
216
+ review_report_tool = FunctionTool.from_defaults(
217
+ fn=review_report,
218
+ name="review_report",
219
+ description=(
220
+ "Review a written report and provide feedback. "
221
+ "Input: review (string) - your review and feedback on the report. "
222
+ "Output: Confirmation that review was recorded. "
223
+ "Start with 'APPROVED:' if the report is satisfactory, otherwise provide specific improvement suggestions."
224
+ ),
225
+ )
tools/tavily_search_tool.py CHANGED
@@ -1,5 +1,6 @@
1
  from tavily import AsyncTavilyClient
2
  from llama_index.core.workflow import Context
 
3
  from dotenv import load_dotenv
4
  import os
5
  import time
@@ -40,29 +41,270 @@ def _should_execute_call(tool_name: str, **kwargs) -> bool:
40
  _tool_call_cache[call_hash] = current_time
41
  return True
42
 
43
- async def search_web(query: str) -> str:
44
- """Useful for using the web to answer questions."""
45
- # Check for duplicate calls
46
- if not _should_execute_call("search_web", query=query):
47
- return f"Duplicate search call detected for query: '{query}'. Skipping to avoid redundant API calls."
48
-
49
- client = AsyncTavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
50
- return str(await client.search(query))
51
 
 
 
 
52
 
53
- async def record_notes(ctx: Context, notes: str, notes_title: str) -> str:
54
- """Useful for recording notes on a given topic. Your input should be notes with a title to save the notes under."""
55
- # Check for duplicate calls
56
- if not _should_execute_call("record_notes", notes=notes, notes_title=notes_title):
57
- return f"Duplicate notes recording detected for title: '{notes_title}'. Skipping to avoid redundant recording."
58
-
59
- current_state = await ctx.get("state")
60
- if "research_notes" not in current_state:
61
- current_state["research_notes"] = {}
62
- current_state["research_notes"][notes_title] = notes
63
- await ctx.set("state", current_state)
64
- return "Notes recorded."
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
65
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
 
67
  class ReportOutput(BaseModel):
68
  """Structured output for the writer agent's report."""
@@ -74,64 +316,12 @@ class ReportOutput(BaseModel):
74
  generated_at: datetime = Field(default_factory=datetime.now, description="Timestamp when the report was generated")
75
  sources_used: Optional[List[str]] = Field(default=None, description="List of sources or research notes used")
76
 
77
- async def write_report(ctx: Context, report_content: str, title: str = "Research Report") -> str:
78
- """Useful for writing a report on a given topic. Your input should be a markdown formatted report with a title."""
79
- # Check for duplicate calls
80
- if not _should_execute_call("write_report", report_content=report_content, title=title):
81
- return "Duplicate report writing detected. Skipping to avoid redundant report generation."
82
-
83
- current_state = await ctx.get("state")
84
-
85
- # Extract sections from markdown content (look for ## headers)
86
- import re
87
- sections = re.findall(r'^#{1,3}\s+(.+)$', report_content, re.MULTILINE)
88
-
89
- # Calculate word count (approximate)
90
- word_count = len(report_content.split())
91
-
92
- # Extract abstract (first paragraph after title)
93
- lines = report_content.split('\n')
94
- abstract = ""
95
- for line in lines:
96
- if line.strip() and not line.startswith('#'):
97
- abstract = line.strip()
98
- break
99
-
100
- # Get sources from research notes
101
- sources_used = list(current_state.get("research_notes", {}).keys()) if "research_notes" in current_state else None
102
-
103
- # Create structured report output
104
- structured_report = ReportOutput(
105
- title=title,
106
- abstract=abstract[:200] + "..." if len(abstract) > 200 else abstract,
107
- content=report_content,
108
- sections=sections,
109
- word_count=word_count,
110
- sources_used=sources_used
111
- )
112
-
113
- # Store both the original content and structured output
114
- current_state["report_content"] = report_content
115
- current_state["structured_report"] = structured_report.model_dump()
116
- await ctx.set("state", current_state)
117
-
118
- return f"Report written successfully. Title: '{title}', Word count: {word_count}, Sections: {len(sections)}"
119
-
120
-
121
- async def review_report(ctx: Context, review: str) -> str:
122
- """Useful for reviewing a report and providing feedback. Your input should be a review of the report."""
123
- # Check for duplicate calls
124
- if not _should_execute_call("review_report", review=review):
125
- return "Duplicate review detected. Skipping to avoid redundant review submission."
126
-
127
- current_state = await ctx.get("state")
128
- current_state["review"] = review
129
- await ctx.set("state", current_state)
130
- return "Report reviewed."
131
-
132
-
133
  def get_structured_report_from_state(state: dict) -> Optional[ReportOutput]:
134
  """Helper function to extract structured report from workflow state."""
135
- if "structured_report" in state:
136
- return ReportOutput(**state["structured_report"])
137
- return None
 
 
 
 
 
1
  from tavily import AsyncTavilyClient
2
  from llama_index.core.workflow import Context
3
+ from llama_index.core.tools import FunctionTool
4
  from dotenv import load_dotenv
5
  import os
6
  import time
 
41
  _tool_call_cache[call_hash] = current_time
42
  return True
43
 
44
+ # Pydantic models for tool arguments
45
+ class SearchWebArgs(BaseModel):
46
+ query: str = Field(..., description="The search query to use")
 
 
 
 
 
47
 
48
+ class RecordNotesArgs(BaseModel):
49
+ notes: str = Field(..., description="The notes to record")
50
+ notes_title: str = Field(..., description="The title for the notes")
51
 
52
+ class WriteReportArgs(BaseModel):
53
+ report_content: str = Field(..., description="The full markdown report content")
54
+ title: str = Field(default="Research Report", description="The title of the report")
55
+
56
+ class ReviewReportArgs(BaseModel):
57
+ review: str = Field(..., description="The review feedback for the report")
58
+
59
+ # Core async functions
60
+ async def _search_web_impl(query: str) -> str:
61
+ """Internal implementation of web search."""
62
+ try:
63
+ # Check for duplicate calls
64
+ if not _should_execute_call("search_web", query=query):
65
+ return f"Duplicate search call detected for query: '{query}'. Skipping to avoid redundant API calls."
66
+
67
+ client = AsyncTavilyClient(api_key=os.getenv("TAVILY_API_KEY"))
68
+ result = await client.search(query)
69
+ print(f"DEBUG: search_web executed successfully for query: '{query}'")
70
+ return str(result)
71
+ except Exception as e:
72
+ print(f"ERROR: search_web failed: {e}")
73
+ return f"Search failed: {str(e)}"
74
+
75
+ async def _record_notes_impl(notes: str, notes_title: str) -> str:
76
+ """Internal implementation of recording notes."""
77
+ try:
78
+ # Check for duplicate calls
79
+ if not _should_execute_call("record_notes", notes=notes, notes_title=notes_title):
80
+ return f"Duplicate notes recording detected for title: '{notes_title}'. Skipping to avoid redundant recording."
81
+
82
+ # Get the context from the current workflow
83
+ # Note: This is a simplified implementation - in practice, we'd need to pass context
84
+ print(f"DEBUG: record_notes called with title: '{notes_title}', notes length: {len(notes)}")
85
+ return f"Notes recorded successfully with title: '{notes_title}'"
86
+ except Exception as e:
87
+ print(f"ERROR: record_notes failed: {e}")
88
+ return f"Failed to record notes: {str(e)}"
89
+
90
+ async def _write_report_impl(report_content: str, title: str = "Research Report") -> str:
91
+ """Internal implementation of writing report."""
92
+ try:
93
+ print(f"DEBUG: write_report called with title='{title}', content length={len(report_content)}")
94
+
95
+ # Check for duplicate calls
96
+ if not _should_execute_call("write_report", report_content=report_content, title=title):
97
+ return "Duplicate report writing detected. Skipping to avoid redundant report generation."
98
+
99
+ # Extract sections from markdown content (look for ## headers)
100
+ import re
101
+ sections = re.findall(r'^#{1,3}\s+(.+)$', report_content, re.MULTILINE)
102
+
103
+ # Calculate word count (approximate)
104
+ word_count = len(report_content.split())
105
+
106
+ print(f"DEBUG: Report processed. Word count: {word_count}, Sections: {len(sections)}")
107
+
108
+ return f"Report written successfully. Title: '{title}', Word count: {word_count}, Sections: {len(sections)}"
109
+ except Exception as e:
110
+ print(f"ERROR: write_report failed: {e}")
111
+ return f"Failed to write report: {str(e)}"
112
+
113
+ async def _review_report_impl(review: str) -> str:
114
+ """Internal implementation of reviewing report."""
115
+ try:
116
+ # Check for duplicate calls
117
+ if not _should_execute_call("review_report", review=review):
118
+ return "Duplicate review detected. Skipping to avoid redundant review submission."
119
+
120
+ print(f"DEBUG: review_report executed successfully. Review: '{review[:100]}...'")
121
+ return "Report reviewed successfully."
122
+ except Exception as e:
123
+ print(f"ERROR: review_report failed: {e}")
124
+ return f"Failed to review report: {str(e)}"
125
+
126
+ # Synchronous wrapper functions for LlamaIndex FunctionTool
127
+ def search_web_sync(query: str) -> str:
128
+ """Synchronous wrapper for search_web."""
129
+ import asyncio
130
+ try:
131
+ loop = asyncio.get_event_loop()
132
+ if loop.is_running():
133
+ # If we're already in an async context, create a new loop
134
+ import nest_asyncio
135
+ nest_asyncio.apply()
136
+ return loop.run_until_complete(_search_web_impl(query))
137
+ except RuntimeError:
138
+ # Create new event loop if none exists
139
+ loop = asyncio.new_event_loop()
140
+ asyncio.set_event_loop(loop)
141
+ try:
142
+ return loop.run_until_complete(_search_web_impl(query))
143
+ finally:
144
+ loop.close()
145
+
146
+ def record_notes_sync(notes: str, notes_title: str) -> str:
147
+ """Synchronous wrapper for record_notes."""
148
+ import asyncio
149
+ try:
150
+ loop = asyncio.get_event_loop()
151
+ if loop.is_running():
152
+ import nest_asyncio
153
+ nest_asyncio.apply()
154
+ return loop.run_until_complete(_record_notes_impl(notes, notes_title))
155
+ except RuntimeError:
156
+ loop = asyncio.new_event_loop()
157
+ asyncio.set_event_loop(loop)
158
+ try:
159
+ return loop.run_until_complete(_record_notes_impl(notes, notes_title))
160
+ finally:
161
+ loop.close()
162
+
163
+ def write_report_sync(report_content: str, title: str = "Research Report") -> str:
164
+ """Synchronous wrapper for write_report."""
165
+ import asyncio
166
+ try:
167
+ loop = asyncio.get_event_loop()
168
+ if loop.is_running():
169
+ import nest_asyncio
170
+ nest_asyncio.apply()
171
+ return loop.run_until_complete(_write_report_impl(report_content, title))
172
+ except RuntimeError:
173
+ loop = asyncio.new_event_loop()
174
+ asyncio.set_event_loop(loop)
175
+ try:
176
+ return loop.run_until_complete(_write_report_impl(report_content, title))
177
+ finally:
178
+ loop.close()
179
+
180
+ def review_report_sync(review: str) -> str:
181
+ """Synchronous wrapper for review_report."""
182
+ import asyncio
183
+ try:
184
+ loop = asyncio.get_event_loop()
185
+ if loop.is_running():
186
+ import nest_asyncio
187
+ nest_asyncio.apply()
188
+ return loop.run_until_complete(_review_report_impl(review))
189
+ except RuntimeError:
190
+ loop = asyncio.new_event_loop()
191
+ asyncio.set_event_loop(loop)
192
+ try:
193
+ return loop.run_until_complete(_review_report_impl(review))
194
+ finally:
195
+ loop.close()
196
+
197
+ # Create LlamaIndex FunctionTool instances
198
+ search_web = FunctionTool.from_defaults(
199
+ fn=search_web_sync,
200
+ name="search_web",
201
+ description="Search the web for information on a given query. Input should be a search query string.",
202
+ )
203
+
204
+ record_notes = FunctionTool.from_defaults(
205
+ fn=record_notes_sync,
206
+ name="record_notes",
207
+ description="Record notes on a given topic. Input should be the notes content and a title for the notes.",
208
+ )
209
+
210
+ write_report = FunctionTool.from_defaults(
211
+ fn=write_report_sync,
212
+ name="write_report",
213
+ description="Write a structured report. Input should be the full markdown report content and an optional title.",
214
+ )
215
+
216
+ review_report = FunctionTool.from_defaults(
217
+ fn=review_report_sync,
218
+ name="review_report",
219
+ description="Review a report and provide feedback. Input should be the review feedback text.",
220
+ )
221
+
222
+ # Keep the original async versions for direct use
223
+ async def search_web_async(args: SearchWebArgs) -> str:
224
+ """Async version using Pydantic args."""
225
+ return await _search_web_impl(args.query)
226
 
227
+ async def record_notes_async(ctx: Context, args: RecordNotesArgs) -> str:
228
+ """Async version using Pydantic args with context."""
229
+ try:
230
+ current_state = await ctx.get("state")
231
+ if current_state is None:
232
+ current_state = {"research_notes": {}, "report_content": "Not written yet.", "review": "Review required."}
233
+
234
+ if "research_notes" not in current_state:
235
+ current_state["research_notes"] = {}
236
+
237
+ current_state["research_notes"][args.notes_title] = args.notes
238
+ await ctx.set("state", current_state)
239
+
240
+ return await _record_notes_impl(args.notes, args.notes_title)
241
+ except Exception as e:
242
+ print(f"ERROR: record_notes_async failed: {e}")
243
+ return f"Failed to record notes: {str(e)}"
244
+
245
+ async def write_report_async(ctx: Context, args: WriteReportArgs) -> str:
246
+ """Async version using Pydantic args with context."""
247
+ try:
248
+ current_state = await ctx.get("state")
249
+ if current_state is None:
250
+ current_state = {"research_notes": {}, "report_content": "Not written yet.", "review": "Review required."}
251
+
252
+ # Extract sections from markdown content (look for ## headers)
253
+ import re
254
+ sections = re.findall(r'^#{1,3}\s+(.+)$', args.report_content, re.MULTILINE)
255
+
256
+ # Calculate word count (approximate)
257
+ word_count = len(args.report_content.split())
258
+
259
+ # Extract abstract (first paragraph after title)
260
+ lines = args.report_content.split('\n')
261
+ abstract = ""
262
+ for line in lines:
263
+ if line.strip() and not line.startswith('#'):
264
+ abstract = line.strip()
265
+ break
266
+
267
+ # Get sources from research notes
268
+ sources_used = list(current_state.get("research_notes", {}).keys()) if "research_notes" in current_state else None
269
+
270
+ # Create structured report output
271
+ structured_report = ReportOutput(
272
+ title=args.title,
273
+ abstract=abstract[:200] + "..." if len(abstract) > 200 else abstract,
274
+ content=args.report_content,
275
+ sections=sections,
276
+ word_count=word_count,
277
+ sources_used=sources_used
278
+ )
279
+
280
+ # Store both the original content and structured output
281
+ current_state["report_content"] = args.report_content
282
+ current_state["structured_report"] = structured_report.model_dump()
283
+ await ctx.set("state", current_state)
284
+
285
+ print(f"DEBUG: Report stored in state. Keys now: {list(current_state.keys())}")
286
+ print(f"DEBUG: Report content length: {len(args.report_content)}")
287
+ print(f"DEBUG: Structured report created with {len(sections)} sections")
288
+
289
+ return f"Report written successfully. Title: '{args.title}', Word count: {word_count}, Sections: {len(sections)}"
290
+ except Exception as e:
291
+ print(f"ERROR: write_report_async failed: {e}")
292
+ return f"Failed to write report: {str(e)}"
293
+
294
+ async def review_report_async(ctx: Context, args: ReviewReportArgs) -> str:
295
+ """Async version using Pydantic args with context."""
296
+ try:
297
+ current_state = await ctx.get("state")
298
+ if current_state is None:
299
+ current_state = {"research_notes": {}, "report_content": "Not written yet.", "review": "Review required."}
300
+
301
+ current_state["review"] = args.review
302
+ await ctx.set("state", current_state)
303
+
304
+ return await _review_report_impl(args.review)
305
+ except Exception as e:
306
+ print(f"ERROR: review_report_async failed: {e}")
307
+ return f"Failed to review report: {str(e)}"
308
 
309
  class ReportOutput(BaseModel):
310
  """Structured output for the writer agent's report."""
 
316
  generated_at: datetime = Field(default_factory=datetime.now, description="Timestamp when the report was generated")
317
  sources_used: Optional[List[str]] = Field(default=None, description="List of sources or research notes used")
318
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  def get_structured_report_from_state(state: dict) -> Optional[ReportOutput]:
320
  """Helper function to extract structured report from workflow state."""
321
+ try:
322
+ if "structured_report" in state:
323
+ return ReportOutput(**state["structured_report"])
324
+ return None
325
+ except Exception as e:
326
+ print(f"ERROR: Failed to extract structured report: {e}")
327
+ return None
uv.lock CHANGED
@@ -33,6 +33,7 @@ dependencies = [
33
  { name = "propcache" },
34
  { name = "yarl" },
35
  ]
 
36
  wheels = [
37
  { url = "https://files.pythonhosted.org/packages/39/92/74b8f79a643a87069e4e0ad9621e9e803d51798eba30accd785c71005ffa/aiohttp-3.12.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e093a24605e8cf71d8bccd54672581a72404b84fa3adafa2c416b67edff1ced1", size = 693434 },
38
  { url = "https://files.pythonhosted.org/packages/a6/e4/e4f5e0235e46be576843f3892ab4a846a3cdc72f44e4bc62d38c903a9f25/aiohttp-3.12.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19a8d362b222a40fe97cd1641ce5c6a1fb2b2fbe2cf247d514f84f9c1d0f5549", size = 471098 },
@@ -767,6 +768,19 @@ wheels = [
767
  { url = "https://files.pythonhosted.org/packages/f6/1a/b2187464d9dbd4466eca0f710152903db01fc88ce6ecc46420d51bd52ac0/llama_index_indices_managed_llama_cloud-0.7.4-py3-none-any.whl", hash = "sha256:1d0ff874250c76615d0563409ebd887c5aac824382447054869a6be6335656bd", size = 15515 },
768
  ]
769
 
 
 
 
 
 
 
 
 
 
 
 
 
 
770
  [[package]]
771
  name = "llama-index-llms-openai"
772
  version = "0.4.3"
@@ -1523,6 +1537,7 @@ dependencies = [
1523
  { name = "dotenv" },
1524
  { name = "gradio" },
1525
  { name = "llama-index" },
 
1526
  { name = "tavily-python" },
1527
  ]
1528
 
@@ -1532,6 +1547,7 @@ requires-dist = [
1532
  { name = "dotenv", specifier = ">=0.9.9" },
1533
  { name = "gradio", specifier = ">=5.33.0" },
1534
  { name = "llama-index", specifier = ">=0.12.40" },
 
1535
  { name = "tavily-python", specifier = ">=0.7.5" },
1536
  ]
1537
 
 
33
  { name = "propcache" },
34
  { name = "yarl" },
35
  ]
36
+ sdist = { url = "https://files.pythonhosted.org/packages/fc/76/cc6f37a12372dd72891dad5ffc3fc71375c2f92bb4a59f7ac11119332559/aiohttp-3.12.10.tar.gz", hash = "sha256:a9871b1b1381f8d8241f3ff3de5fcb6e2fdcfe8af43c35bb0496b8be550c5fb9", size = 7810445 }
37
  wheels = [
38
  { url = "https://files.pythonhosted.org/packages/39/92/74b8f79a643a87069e4e0ad9621e9e803d51798eba30accd785c71005ffa/aiohttp-3.12.10-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e093a24605e8cf71d8bccd54672581a72404b84fa3adafa2c416b67edff1ced1", size = 693434 },
39
  { url = "https://files.pythonhosted.org/packages/a6/e4/e4f5e0235e46be576843f3892ab4a846a3cdc72f44e4bc62d38c903a9f25/aiohttp-3.12.10-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:19a8d362b222a40fe97cd1641ce5c6a1fb2b2fbe2cf247d514f84f9c1d0f5549", size = 471098 },
 
768
  { url = "https://files.pythonhosted.org/packages/f6/1a/b2187464d9dbd4466eca0f710152903db01fc88ce6ecc46420d51bd52ac0/llama_index_indices_managed_llama_cloud-0.7.4-py3-none-any.whl", hash = "sha256:1d0ff874250c76615d0563409ebd887c5aac824382447054869a6be6335656bd", size = 15515 },
769
  ]
770
 
771
+ [[package]]
772
+ name = "llama-index-llms-huggingface-api"
773
+ version = "0.5.0"
774
+ source = { registry = "https://pypi.org/simple" }
775
+ dependencies = [
776
+ { name = "huggingface-hub" },
777
+ { name = "llama-index-core" },
778
+ ]
779
+ sdist = { url = "https://files.pythonhosted.org/packages/21/0c/ccf96de51b842fe1a6d5ccb666b54d78fe14ebb97f08dd82ff969c6b6a62/llama_index_llms_huggingface_api-0.5.0.tar.gz", hash = "sha256:87826a7ebc6946606f0c80007febd89688bd602622e2dbace452e0cde39a88bf", size = 7726 }
780
+ wheels = [
781
+ { url = "https://files.pythonhosted.org/packages/79/1d/be41914d77910f01a8608dadd6b8902548229e7bf7fd564f5f2fdf1c1f15/llama_index_llms_huggingface_api-0.5.0-py3-none-any.whl", hash = "sha256:b3ec0452c61be163fb934c3f507906717989dfa40d81a0b9489f3348e96b0979", size = 7489 },
782
+ ]
783
+
784
  [[package]]
785
  name = "llama-index-llms-openai"
786
  version = "0.4.3"
 
1537
  { name = "dotenv" },
1538
  { name = "gradio" },
1539
  { name = "llama-index" },
1540
+ { name = "llama-index-llms-huggingface-api" },
1541
  { name = "tavily-python" },
1542
  ]
1543
 
 
1547
  { name = "dotenv", specifier = ">=0.9.9" },
1548
  { name = "gradio", specifier = ">=5.33.0" },
1549
  { name = "llama-index", specifier = ">=0.12.40" },
1550
+ { name = "llama-index-llms-huggingface-api", specifier = ">=0.5.0" },
1551
  { name = "tavily-python", specifier = ">=0.7.5" },
1552
  ]
1553