Humanlearning commited on
Commit
5e4f9b3
·
1 Parent(s): 9126c2d

output shown properly using pydantic

Browse files
__pycache__/agent.cpython-313.pyc CHANGED
Binary files a/__pycache__/agent.cpython-313.pyc and b/__pycache__/agent.cpython-313.pyc differ
 
agent.py CHANGED
@@ -1,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 a markdown format. The content should be grounded in the research notes. "
 
 
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 approve the current report or request specific changes for the WriteAgent to implement. "
 
 
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
- if any(word in result_content.lower() for word in ["approved", "ready", "good", "excellent"]):
 
 
 
 
 
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 and "report_content" in final_state:
167
- report_content = final_state["report_content"]
168
- if report_content and report_content != "Not written yet.":
169
- # Show the final report if we have one and it's been reviewed
170
- if workflow_state.get("has_report") and workflow_state.get("has_review"):
171
- final_report = gr.Markdown(report_content, visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  elif workflow_state.get("has_report"):
173
- # Show report even if not reviewed yet
174
- final_report = gr.Markdown(report_content, visible=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- yield history, final_report
 
 
 
 
 
 
 
 
 
 
 
 
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
- async def write_report(ctx: Context, report_content: str) -> str:
65
- """Useful for writing a report on a given topic. Your input should be a markdown formatted report."""
 
 
 
 
 
 
 
 
 
 
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
- return "Report written."
 
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