Commit
·
5e4f9b3
1
Parent(s):
9126c2d
output shown properly using pydantic
Browse files- __pycache__/agent.cpython-313.pyc +0 -0
- agent.py +18 -5
- app.py +96 -22
- test_structured_output.py +79 -0
- tools/__pycache__/tavily_search_tool.cpython-313.pyc +0 -0
- tools/tavily_search_tool.py +57 -5
__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,5 +1,5 @@
|
|
| 1 |
from llama_index.llms.openai import OpenAI
|
| 2 |
-
from tools.tavily_search_tool import search_web, record_notes, write_report, review_report
|
| 3 |
from dotenv import load_dotenv
|
| 4 |
import os
|
| 5 |
from llama_index.core.agent.workflow import FunctionAgent
|
|
@@ -36,11 +36,13 @@ class TeacherStudentAgentWorkflow:
|
|
| 36 |
|
| 37 |
self.write_agent = FunctionAgent(
|
| 38 |
name="WriteAgent",
|
| 39 |
-
description="Useful for writing a report on a given topic.",
|
| 40 |
system_prompt=(
|
| 41 |
-
"You are the WriteAgent that can write a 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
|
|
|
|
|
|
|
| 44 |
"Once the report is written ONCE, immediately hand off control to the ReviewAgent for feedback."
|
| 45 |
),
|
| 46 |
llm=self.llm,
|
|
@@ -54,7 +56,9 @@ class TeacherStudentAgentWorkflow:
|
|
| 54 |
system_prompt=(
|
| 55 |
"You are the ReviewAgent that can review the report and provide feedback. "
|
| 56 |
"IMPORTANT: Never make duplicate tool calls. Review the report only ONCE and provide clear feedback. "
|
| 57 |
-
"Your review should either
|
|
|
|
|
|
|
| 58 |
"If you have feedback that requires changes, hand off control to the WriteAgent to implement the changes after submitting the review ONCE."
|
| 59 |
),
|
| 60 |
llm=self.llm,
|
|
@@ -72,6 +76,15 @@ class TeacherStudentAgentWorkflow:
|
|
| 72 |
},
|
| 73 |
)
|
| 74 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
async def run_workflow(self, user_msg=None):
|
| 76 |
if user_msg is None:
|
| 77 |
user_msg = (
|
|
|
|
| 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
|
|
|
|
| 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,
|
|
|
|
| 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,
|
|
|
|
| 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:
|
| 90 |
user_msg = (
|
app.py
CHANGED
|
@@ -4,6 +4,7 @@ import asyncio
|
|
| 4 |
import json
|
| 5 |
import hashlib
|
| 6 |
from agent import TeacherStudentAgentWorkflow
|
|
|
|
| 7 |
from llama_index.core.agent.workflow import (
|
| 8 |
AgentInput,
|
| 9 |
AgentOutput,
|
|
@@ -11,6 +12,7 @@ from llama_index.core.agent.workflow import (
|
|
| 11 |
ToolCallResult,
|
| 12 |
AgentStream,
|
| 13 |
)
|
|
|
|
| 14 |
|
| 15 |
# Initialize the agent workflow
|
| 16 |
agent_workflow = None
|
|
@@ -24,14 +26,15 @@ def get_agent_workflow():
|
|
| 24 |
async def chat_with_agent(message, history):
|
| 25 |
"""
|
| 26 |
Async chat function that runs the agent workflow and streams each step.
|
|
|
|
| 27 |
"""
|
| 28 |
if not message.strip():
|
| 29 |
-
yield history, None
|
| 30 |
return
|
| 31 |
|
| 32 |
# Add user message to history
|
| 33 |
history.append(ChatMessage(role="user", content=message))
|
| 34 |
-
yield history, None
|
| 35 |
|
| 36 |
try:
|
| 37 |
# Get the agent workflow
|
|
@@ -43,6 +46,7 @@ async def chat_with_agent(message, history):
|
|
| 43 |
current_agent = None
|
| 44 |
current_step_messages = []
|
| 45 |
final_report = None
|
|
|
|
| 46 |
workflow_state = {}
|
| 47 |
|
| 48 |
# Track recent tool calls to prevent UI duplicates
|
|
@@ -67,7 +71,7 @@ async def chat_with_agent(message, history):
|
|
| 67 |
metadata={"title": f"Agent: {current_agent}"}
|
| 68 |
)
|
| 69 |
history.append(agent_header)
|
| 70 |
-
yield history, final_report
|
| 71 |
|
| 72 |
# Handle different event types
|
| 73 |
if isinstance(event, AgentOutput):
|
|
@@ -79,7 +83,7 @@ async def chat_with_agent(message, history):
|
|
| 79 |
metadata={"title": f"{current_agent} - Output"}
|
| 80 |
)
|
| 81 |
history.append(output_msg)
|
| 82 |
-
yield history, final_report
|
| 83 |
|
| 84 |
if event.tool_calls:
|
| 85 |
# Show planned tools
|
|
@@ -90,7 +94,7 @@ async def chat_with_agent(message, history):
|
|
| 90 |
metadata={"title": f"{current_agent} - Tool Planning"}
|
| 91 |
)
|
| 92 |
history.append(tools_msg)
|
| 93 |
-
yield history, final_report
|
| 94 |
|
| 95 |
elif isinstance(event, ToolCall):
|
| 96 |
# Create a unique identifier for this tool call using a more robust approach
|
|
@@ -118,7 +122,7 @@ async def chat_with_agent(message, history):
|
|
| 118 |
metadata={"title": f"{current_agent} - Tool Call"}
|
| 119 |
)
|
| 120 |
history.append(tool_msg)
|
| 121 |
-
yield history, final_report
|
| 122 |
else:
|
| 123 |
# Debug: Log duplicate detection (remove this in production)
|
| 124 |
print(f"🚫 Duplicate tool call detected and skipped: {event.tool_name} with args {event.tool_kwargs}")
|
|
@@ -154,24 +158,62 @@ async def chat_with_agent(message, history):
|
|
| 154 |
workflow_state["has_report"] = True
|
| 155 |
elif event.tool_name == "review_report" and current_agent == "ReviewAgent":
|
| 156 |
workflow_state["has_review"] = True
|
| 157 |
-
# Check if review indicates approval
|
| 158 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 159 |
workflow_state["review_approved"] = True
|
| 160 |
|
| 161 |
-
yield history, final_report
|
| 162 |
|
| 163 |
-
# Get the final state to extract the report
|
| 164 |
try:
|
| 165 |
final_state = await handler.ctx.get("state")
|
| 166 |
-
if final_state
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
elif workflow_state.get("has_report"):
|
| 173 |
-
# Show report even if not reviewed yet
|
| 174 |
-
final_report = gr.Markdown(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
except Exception as state_error:
|
| 176 |
print(f"Could not extract final state: {state_error}")
|
| 177 |
# Try to show any report that was generated during the conversation
|
|
@@ -185,7 +227,19 @@ async def chat_with_agent(message, history):
|
|
| 185 |
metadata={"title": "Workflow Complete"}
|
| 186 |
)
|
| 187 |
history.append(completion_msg)
|
| 188 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 189 |
|
| 190 |
except Exception as e:
|
| 191 |
# Handle errors gracefully
|
|
@@ -195,12 +249,22 @@ async def chat_with_agent(message, history):
|
|
| 195 |
metadata={"title": "Error"}
|
| 196 |
)
|
| 197 |
history.append(error_msg)
|
| 198 |
-
yield history, None
|
| 199 |
|
| 200 |
def like_feedback(evt: gr.LikeData):
|
| 201 |
"""Handle user feedback on messages."""
|
| 202 |
print(f"User feedback - Index: {evt.index}, Liked: {evt.liked}, Value: {evt.value}")
|
| 203 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
# Create the Gradio interface
|
| 205 |
with gr.Blocks(title="Teacher-Student Agent Workflow", theme=gr.themes.Soft()) as demo:
|
| 206 |
gr.Markdown("""
|
|
@@ -235,6 +299,13 @@ with gr.Blocks(title="Teacher-Student Agent Workflow", theme=gr.themes.Soft()) a
|
|
| 235 |
render=False
|
| 236 |
)
|
| 237 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
# Set up the chat interface with additional outputs
|
| 239 |
chat_interface = gr.ChatInterface(
|
| 240 |
fn=chat_with_agent,
|
|
@@ -254,7 +325,7 @@ with gr.Blocks(title="Teacher-Student Agent Workflow", theme=gr.themes.Soft()) a
|
|
| 254 |
"Space Exploration Report"
|
| 255 |
],
|
| 256 |
cache_examples=False,
|
| 257 |
-
additional_outputs=[final_report_output]
|
| 258 |
)
|
| 259 |
|
| 260 |
# Add feedback handling
|
|
@@ -262,9 +333,12 @@ with gr.Blocks(title="Teacher-Student Agent Workflow", theme=gr.themes.Soft()) a
|
|
| 262 |
|
| 263 |
# Render the final report output in a separate section
|
| 264 |
with gr.Row():
|
| 265 |
-
with gr.Column():
|
| 266 |
gr.Markdown("### 📋 Final Report")
|
| 267 |
final_report_output.render()
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
gr.Markdown("""
|
| 270 |
### How it works:
|
|
|
|
| 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 |
ToolCallResult,
|
| 13 |
AgentStream,
|
| 14 |
)
|
| 15 |
+
from datetime import datetime
|
| 16 |
|
| 17 |
# Initialize the agent workflow
|
| 18 |
agent_workflow = None
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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):
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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}")
|
|
|
|
| 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
|
|
|
|
| 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
|
|
|
|
| 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."""
|
| 256 |
print(f"User feedback - Index: {evt.index}, Liked: {evt.liked}, Value: {evt.value}")
|
| 257 |
|
| 258 |
+
def format_structured_report_display(structured_report_data):
|
| 259 |
+
"""Format structured report data for JSON display component."""
|
| 260 |
+
if not structured_report_data:
|
| 261 |
+
return gr.JSON(visible=False)
|
| 262 |
+
|
| 263 |
+
return gr.JSON(
|
| 264 |
+
value=structured_report_data,
|
| 265 |
+
visible=True
|
| 266 |
+
)
|
| 267 |
+
|
| 268 |
# Create the Gradio interface
|
| 269 |
with gr.Blocks(title="Teacher-Student Agent Workflow", theme=gr.themes.Soft()) as demo:
|
| 270 |
gr.Markdown("""
|
|
|
|
| 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,
|
|
|
|
| 325 |
"Space Exploration Report"
|
| 326 |
],
|
| 327 |
cache_examples=False,
|
| 328 |
+
additional_outputs=[final_report_output, structured_report_json]
|
| 329 |
)
|
| 330 |
|
| 331 |
# Add feedback handling
|
|
|
|
| 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:
|
test_structured_output.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Test script to verify the Pydantic structured output functionality.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import asyncio
|
| 7 |
+
import sys
|
| 8 |
+
import os
|
| 9 |
+
|
| 10 |
+
# Add the current directory to the path so we can import modules
|
| 11 |
+
sys.path.append(os.path.dirname(os.path.abspath(__file__)))
|
| 12 |
+
|
| 13 |
+
from tools.tavily_search_tool import ReportOutput, get_structured_report_from_state
|
| 14 |
+
from datetime import datetime
|
| 15 |
+
|
| 16 |
+
async def test_structured_output():
|
| 17 |
+
"""Test the structured report output functionality."""
|
| 18 |
+
|
| 19 |
+
print("🧪 Testing Pydantic ReportOutput model...")
|
| 20 |
+
|
| 21 |
+
# Test creating a ReportOutput instance
|
| 22 |
+
test_report = ReportOutput(
|
| 23 |
+
title="Test Report",
|
| 24 |
+
abstract="This is a test abstract for our report.",
|
| 25 |
+
content="""# Test Report
|
| 26 |
+
|
| 27 |
+
## Introduction
|
| 28 |
+
This is the introduction section.
|
| 29 |
+
|
| 30 |
+
## Main Content
|
| 31 |
+
This is the main content section with some details.
|
| 32 |
+
|
| 33 |
+
## Conclusion
|
| 34 |
+
This is the conclusion section.
|
| 35 |
+
""",
|
| 36 |
+
sections=["Introduction", "Main Content", "Conclusion"],
|
| 37 |
+
word_count=25,
|
| 38 |
+
sources_used=["Source 1", "Source 2"]
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
print(f"✅ Created ReportOutput successfully!")
|
| 42 |
+
print(f"📋 Title: {test_report.title}")
|
| 43 |
+
print(f"📝 Abstract: {test_report.abstract}")
|
| 44 |
+
print(f"📊 Word Count: {test_report.word_count}")
|
| 45 |
+
print(f"🗂️ Sections: {test_report.sections}")
|
| 46 |
+
print(f"📚 Sources: {test_report.sources_used}")
|
| 47 |
+
print(f"⏰ Generated at: {test_report.generated_at}")
|
| 48 |
+
|
| 49 |
+
# Test serialization
|
| 50 |
+
print("\n🔄 Testing serialization...")
|
| 51 |
+
serialized = test_report.model_dump()
|
| 52 |
+
print(f"✅ Serialized data keys: {list(serialized.keys())}")
|
| 53 |
+
|
| 54 |
+
# Test state extraction
|
| 55 |
+
print("\n🔍 Testing state extraction...")
|
| 56 |
+
mock_state = {
|
| 57 |
+
"structured_report": serialized,
|
| 58 |
+
"other_data": "some value"
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
extracted_report = get_structured_report_from_state(mock_state)
|
| 62 |
+
if extracted_report:
|
| 63 |
+
print(f"✅ Successfully extracted report from state!")
|
| 64 |
+
print(f"📋 Extracted title: {extracted_report.title}")
|
| 65 |
+
print(f"📊 Extracted word count: {extracted_report.word_count}")
|
| 66 |
+
else:
|
| 67 |
+
print("❌ Failed to extract report from state")
|
| 68 |
+
|
| 69 |
+
# Test with empty state
|
| 70 |
+
empty_report = get_structured_report_from_state({})
|
| 71 |
+
if empty_report is None:
|
| 72 |
+
print("✅ Correctly returned None for empty state")
|
| 73 |
+
else:
|
| 74 |
+
print("❌ Should have returned None for empty state")
|
| 75 |
+
|
| 76 |
+
print("\n🎉 All tests completed!")
|
| 77 |
+
|
| 78 |
+
if __name__ == "__main__":
|
| 79 |
+
asyncio.run(test_structured_output())
|
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/tavily_search_tool.py
CHANGED
|
@@ -5,6 +5,9 @@ import os
|
|
| 5 |
import time
|
| 6 |
import hashlib
|
| 7 |
import json
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
load_dotenv(os.path.join(os.path.dirname(__file__), '../env.local'))
|
| 10 |
|
|
@@ -61,16 +64,58 @@ async def record_notes(ctx: Context, notes: str, notes_title: str) -> str:
|
|
| 61 |
return "Notes recorded."
|
| 62 |
|
| 63 |
|
| 64 |
-
|
| 65 |
-
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 66 |
# Check for duplicate calls
|
| 67 |
-
if not _should_execute_call("write_report", report_content=report_content):
|
| 68 |
return "Duplicate report writing detected. Skipping to avoid redundant report generation."
|
| 69 |
|
| 70 |
current_state = await ctx.get("state")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
current_state["report_content"] = report_content
|
|
|
|
| 72 |
await ctx.set("state", current_state)
|
| 73 |
-
|
|
|
|
| 74 |
|
| 75 |
|
| 76 |
async def review_report(ctx: Context, review: str) -> str:
|
|
@@ -82,4 +127,11 @@ async def review_report(ctx: Context, review: str) -> str:
|
|
| 82 |
current_state = await ctx.get("state")
|
| 83 |
current_state["review"] = review
|
| 84 |
await ctx.set("state", current_state)
|
| 85 |
-
return "Report reviewed."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
import time
|
| 6 |
import hashlib
|
| 7 |
import json
|
| 8 |
+
from pydantic import BaseModel, Field
|
| 9 |
+
from typing import List, Optional
|
| 10 |
+
from datetime import datetime
|
| 11 |
|
| 12 |
load_dotenv(os.path.join(os.path.dirname(__file__), '../env.local'))
|
| 13 |
|
|
|
|
| 64 |
return "Notes recorded."
|
| 65 |
|
| 66 |
|
| 67 |
+
class ReportOutput(BaseModel):
|
| 68 |
+
"""Structured output for the writer agent's report."""
|
| 69 |
+
title: str = Field(description="The title of the report")
|
| 70 |
+
abstract: str = Field(description="A brief abstract or summary of the report")
|
| 71 |
+
content: str = Field(description="The full markdown content of the report")
|
| 72 |
+
sections: List[str] = Field(description="List of main section titles in the report")
|
| 73 |
+
word_count: int = Field(description="Approximate word count of the report")
|
| 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:
|
|
|
|
| 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
|