Commit
·
741c3da
1
Parent(s):
7ed8bad
working agent
Browse files- __pycache__/agent.cpython-313.pyc +0 -0
- agent.py +99 -55
- app.py +168 -244
- pyproject.toml +1 -0
- requirements.txt +1 -0
- simple_test.py +109 -0
- test_tools.py +96 -0
- tools/__pycache__/simple_tools.cpython-313.pyc +0 -0
- tools/__pycache__/tavily_search_tool.cpython-313.pyc +0 -0
- tools/simple_tools.py +225 -0
- tools/tavily_search_tool.py +269 -79
- uv.lock +16 -0
__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.
|
| 2 |
-
from tools.
|
|
|
|
|
|
|
|
|
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
import os
|
| 5 |
-
from llama_index.core.agent.workflow import
|
| 6 |
-
from llama_index.core.
|
| 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 =
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
self.research_agent =
|
| 22 |
name="ResearchAgent",
|
| 23 |
-
description="
|
| 24 |
system_prompt=(
|
| 25 |
-
"You are
|
| 26 |
-
"
|
| 27 |
-
"
|
| 28 |
-
"
|
| 29 |
-
"
|
| 30 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
),
|
|
|
|
| 32 |
llm=self.llm,
|
| 33 |
-
tools=[search_web, record_notes],
|
| 34 |
can_handoff_to=["WriteAgent"],
|
| 35 |
)
|
| 36 |
|
| 37 |
-
self.write_agent =
|
| 38 |
name="WriteAgent",
|
| 39 |
-
description="
|
| 40 |
system_prompt=(
|
| 41 |
-
"You are
|
| 42 |
-
"
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
),
|
|
|
|
| 48 |
llm=self.llm,
|
| 49 |
-
|
| 50 |
-
can_handoff_to=["ReviewAgent", "ResearchAgent"],
|
| 51 |
)
|
| 52 |
|
| 53 |
-
self.review_agent =
|
| 54 |
name="ReviewAgent",
|
| 55 |
-
description="
|
| 56 |
system_prompt=(
|
| 57 |
-
"You are
|
| 58 |
-
"
|
| 59 |
-
"
|
| 60 |
-
"
|
| 61 |
-
"If
|
| 62 |
-
"
|
| 63 |
),
|
|
|
|
| 64 |
llm=self.llm,
|
| 65 |
-
|
| 66 |
-
can_handoff_to=["ResearchAgent","WriteAgent"],
|
| 67 |
)
|
| 68 |
|
| 69 |
self.agent_workflow = AgentWorkflow(
|
|
@@ -76,14 +97,9 @@ class TeacherStudentAgentWorkflow:
|
|
| 76 |
},
|
| 77 |
)
|
| 78 |
|
| 79 |
-
|
| 80 |
-
"""
|
| 81 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 117 |
print(f"🔧 Tool Result ({event.tool_name}):")
|
| 118 |
-
print(f" Arguments: {event
|
| 119 |
print(f" Output: {event.tool_output}")
|
| 120 |
-
elif
|
| 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.
|
| 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
|
| 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 |
-
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
try:
|
| 40 |
-
# Get the agent workflow
|
| 41 |
workflow = get_agent_workflow()
|
| 42 |
|
| 43 |
-
#
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
|
| 47 |
-
current_step_messages = []
|
| 48 |
-
final_report = None
|
| 49 |
-
structured_report_data = None
|
| 50 |
-
workflow_state = {}
|
| 51 |
|
| 52 |
-
|
| 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 |
-
|
| 58 |
-
|
| 59 |
-
|
| 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"
|
| 71 |
metadata={"title": f"Agent: {current_agent}"}
|
| 72 |
-
)
|
| 73 |
-
history.
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 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 |
-
|
| 110 |
-
|
| 111 |
-
|
| 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"
|
| 122 |
metadata={"title": f"{current_agent} - Tool Call"}
|
| 123 |
-
)
|
| 124 |
-
|
| 125 |
-
yield history,
|
| 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 |
-
|
| 133 |
-
result_content = str(event.tool_output)
|
| 134 |
-
if len(result_content) > 500:
|
| 135 |
-
result_content = result_content[:500] + "..."
|
| 136 |
|
| 137 |
-
#
|
| 138 |
-
|
|
|
|
|
|
|
| 139 |
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
|
| 160 |
-
|
| 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 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
structured_report_data = {
|
| 181 |
-
"title":
|
| 182 |
-
"
|
| 183 |
-
"
|
| 184 |
-
"
|
| 185 |
-
"
|
| 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 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 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": "
|
| 209 |
-
"content":
|
| 210 |
-
"
|
| 211 |
-
"
|
| 212 |
-
"
|
|
|
|
| 213 |
}
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
|
| 223 |
-
|
| 224 |
-
completion_msg = ChatMessage(
|
| 225 |
role="assistant",
|
| 226 |
-
content="✅ **Workflow completed!**
|
| 227 |
metadata={"title": "Workflow Complete"}
|
| 228 |
-
)
|
| 229 |
-
|
| 230 |
-
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
|
| 234 |
-
|
| 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 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 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="
|
| 286 |
render_markdown=True
|
| 287 |
)
|
| 288 |
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 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 |
-
|
| 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 |
-
|
| 44 |
-
|
| 45 |
-
|
| 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 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
|