Skip to content

Commit 995cc17

Browse files
committed
feat: integrate research workflow into canvas
1 parent 61b4382 commit 995cc17

File tree

18 files changed

+1255
-998
lines changed

18 files changed

+1255
-998
lines changed

agent-blueprint/agentcore-runtime-a2a-stack/research-agent/src/main.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ async def _execute_streaming(self, context: RequestContext, updater: TaskUpdater
157157
self._current_tool_use_id = None
158158
self._step_counter = 0
159159

160-
# Extract metadata from RequestContext
160+
# Extract metadata from RequestContext (need session_id early for file cleanup)
161161
# Try both params.metadata (MessageSendParams) and message.metadata (Message)
162162
# Streaming client may put metadata in Message.metadata
163163
metadata = context.metadata # MessageSendParams.metadata
@@ -170,6 +170,18 @@ async def _execute_streaming(self, context: RequestContext, updater: TaskUpdater
170170

171171
logger.info(f"[MetadataAwareExecutor] Extracted metadata - model_id: {model_id}, session_id: {session_id}, user_id: {user_id}")
172172

173+
# Clear previous research file for this session (prevent cumulative results)
174+
if session_id:
175+
try:
176+
from report_manager import get_report_manager
177+
manager = get_report_manager(session_id, user_id)
178+
markdown_file = os.path.join(manager.workspace, "research_report.md")
179+
if os.path.exists(markdown_file):
180+
os.remove(markdown_file)
181+
logger.info(f"[MetadataAwareExecutor] Cleared previous research file: {markdown_file}")
182+
except Exception as e:
183+
logger.warning(f"[MetadataAwareExecutor] Failed to clear previous research file: {e}")
184+
173185
# Get or create agent with specified model_id
174186
if model_id and model_id in self.agent_cache:
175187
agent = self.agent_cache[model_id]

agent-blueprint/agentcore-runtime-a2a-stack/research-agent/src/tools/markdown_writer.py

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -139,23 +139,22 @@ async def write_markdown_section(
139139
# Prepare section content
140140
section_content = f"{prefix}{heading}\n\n{content}\n\n"
141141

142-
# Add citations if provided (simple icon links only)
142+
# Add citations if provided (domain-based markdown links)
143143
if citations and len(citations) > 0:
144-
section_content += '<div class="section-citations">\n'
144+
from urllib.parse import urlparse
145+
citation_links = []
145146
for citation in citations:
146-
title = citation.get('title', 'Unknown Source')
147147
url = citation.get('url', '#')
148-
149-
# Extract domain from URL for tooltip
150-
from urllib.parse import urlparse
148+
# Extract domain from URL
151149
try:
152150
domain = urlparse(url).netloc or url
151+
domain = domain.replace('www.', '')
153152
except:
154153
domain = url
154+
# Use markdown link format with domain as text
155+
citation_links.append(f'[{domain}]({url})')
155156

156-
# Generate simple icon link with domain tooltip
157-
section_content += f'<span class="citation-chip"><a href="{url}" target="_blank" rel="noopener noreferrer" title="{domain}" aria-label="{title}">🔗</a></span> '
158-
section_content += '\n</div>\n\n'
157+
section_content += ' '.join(citation_links) + '\n\n'
159158
logger.info(f"Added {len(citations)} citations to section: {heading}")
160159

161160
# Append to file (create if doesn't exist)

chatbot-app/agentcore/src/a2a_tools.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -692,11 +692,74 @@ async def tool_impl(plan: str, tool_context: ToolContext = None) -> AsyncGenerat
692692

693693
logger.info(f"[{agent_id}] Sending to A2A with metadata: {metadata}")
694694

695+
# Track final result for artifact saving
696+
final_result_text = None
697+
695698
# Stream events from A2A agent (including research_step events for real-time UI updates)
696699
async for event in send_a2a_message(agent_id, plan, session_id, region, metadata=metadata):
700+
# Capture final result for artifact saving
701+
if isinstance(event, dict) and event.get("status") == "success":
702+
content = event.get("content", [])
703+
if content and len(content) > 0:
704+
final_result_text = content[0].get("text", "")
705+
697706
# Yield all events to allow real-time streaming (research_step, etc.)
698707
yield event
699708

709+
# After streaming completes, save research result to agent.state
710+
if final_result_text and tool_context and tool_context.agent:
711+
try:
712+
from datetime import datetime, timezone
713+
714+
# Extract title from research content (first H1 heading)
715+
import re
716+
title_match = re.search(r'^#\s+(.+)$', final_result_text, re.MULTILINE)
717+
title = title_match.group(1).strip() if title_match else "Research Results"
718+
719+
# Generate artifact ID using toolUseId for frontend mapping
720+
tool_use_id = tool_context.tool_use.get('toolUseId', '')
721+
artifact_id = f"research-{tool_use_id}" if tool_use_id else f"research-{session_id}-{datetime.now(timezone.utc).strftime('%Y%m%d%H%M%S')}"
722+
723+
# Get current artifacts from agent.state
724+
artifacts = tool_context.agent.state.get("artifacts") or {}
725+
726+
# Calculate word count
727+
word_count = len(final_result_text.split())
728+
729+
# Add new artifact
730+
artifacts[artifact_id] = {
731+
"id": artifact_id,
732+
"type": "research",
733+
"title": title,
734+
"content": final_result_text,
735+
"tool_name": "research_agent",
736+
"metadata": {
737+
"word_count": word_count,
738+
"description": f"Research report: {title}"
739+
},
740+
"created_at": datetime.now(timezone.utc).isoformat(),
741+
"updated_at": datetime.now(timezone.utc).isoformat()
742+
}
743+
744+
# Save to agent.state
745+
tool_context.agent.state.set("artifacts", artifacts)
746+
747+
# Sync agent state to file system / AgentCore Memory
748+
# Try session_manager from invocation_state first (set by ChatAgent)
749+
session_manager = tool_context.invocation_state.get("session_manager")
750+
751+
if not session_manager and hasattr(tool_context.agent, 'session_manager'):
752+
session_manager = tool_context.agent.session_manager
753+
754+
if session_manager:
755+
session_manager.sync_agent(tool_context.agent)
756+
logger.info(f"[Research] ✅ Saved research artifact to agent.state: {artifact_id}")
757+
else:
758+
logger.warning(f"[Research] ⚠️ No session_manager found, artifact not persisted")
759+
760+
except Exception as e:
761+
logger.error(f"[Research] ❌ Failed to save artifact: {e}")
762+
700763
# Set correct function name and docstring BEFORE decorating
701764
tool_impl.__name__ = correct_name
702765
tool_impl.__doc__ = agent_description

chatbot-app/agentcore/src/agent/config/swarm_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@
159159
"responder": """Responder - write the final user-facing response.
160160
161161
Use create_visualization tool for simple charts if needed.
162-
Citation: When citations in shared context, wrap claims with <cite source="SOURCE" url="URL">text</cite>""",
162+
Citation: When citing sources from shared context, use markdown links: [source name](URL)""",
163163
}
164164

165165

chatbot-app/agentcore/src/agents/chat_agent.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,11 +281,12 @@ async def stream_async(self, message: str, session_id: str = None, files: Option
281281
else:
282282
logger.debug(f"Prompt is string: {prompt[:100]}")
283283

284-
# Prepare invocation_state with model_id, user_id, session_id, and uploaded files
284+
# Prepare invocation_state with model_id, user_id, session_id, session_manager, and uploaded files
285285
invocation_state = {
286286
"session_id": self.session_id,
287287
"user_id": self.user_id,
288-
"model_id": self.model_id
288+
"model_id": self.model_id,
289+
"session_manager": self.session_manager # For tools that need to persist state (e.g., research artifacts)
289290
}
290291

291292
# Add uploaded files to invocation_state (for tool access)

chatbot-app/frontend/src/app/api/s3/presigned-url/route.ts

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,12 @@ export async function POST(request: NextRequest) {
3838
expiresIn: 3600 // 1 hour
3939
})
4040

41-
console.log(`[S3] Generated pre-signed URL for ${s3Key}`)
42-
4341
return NextResponse.json({ url })
4442

4543
} catch (error) {
4644
console.error('[S3] Error generating pre-signed URL:', error)
4745
return NextResponse.json(
48-
{
49-
error: 'Failed to generate pre-signed URL',
50-
details: error instanceof Error ? error.message : 'Unknown error'
51-
},
46+
{ error: 'Failed to generate pre-signed URL' },
5247
{ status: 500 }
5348
)
5449
}

0 commit comments

Comments
 (0)