drizzlezyk commited on
Commit
ed3ebc2
·
verified ·
1 Parent(s): 1fbe804

Upload deepdiver_v2/src/agents/base_agent.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. deepdiver_v2/src/agents/base_agent.py +692 -0
deepdiver_v2/src/agents/base_agent.py ADDED
@@ -0,0 +1,692 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) 2025 Huawei Technologies Co., Ltd. All rights reserved.
2
+ import logging
3
+ import time
4
+ from abc import ABC, abstractmethod
5
+ from typing import Dict, Any, List, Optional
6
+ from dataclasses import dataclass, field
7
+
8
+ # Import MCP client availability flag without binding unused symbols
9
+ try:
10
+ from ..tools import mcp_client as _mcp_client_module # noqa: F401
11
+ MCP_CLIENT_AVAILABLE = True
12
+ except ImportError:
13
+ MCP_CLIENT_AVAILABLE = False
14
+
15
+
16
+ @dataclass
17
+ class AgentConfig:
18
+ """Configuration for agents - session management handled entirely by MCP server"""
19
+ agent_name: str = "base_agent"
20
+ planner_mode: str = "auto"
21
+ model: Optional[str] = None
22
+ max_iterations: int = 10
23
+ temperature: Optional[float] = None
24
+ max_tokens: Optional[int] = None
25
+ # Paths used by writer and other agents
26
+ trajectory_storage_path: Optional[str] = None
27
+ report_output_path: Optional[str] = None
28
+ document_analysis_path: Optional[str] = None
29
+
30
+
31
+ @dataclass
32
+ class AgentResponse:
33
+ """Standardized response format for all agents"""
34
+ success: bool
35
+ result: Optional[Dict[str, Any]] = None
36
+ error: Optional[str] = None
37
+ iterations: int = 0
38
+ reasoning_trace: List[Dict[str, Any]] = field(default_factory=list)
39
+ agent_name: str = ""
40
+ execution_time: float = 0.0
41
+
42
+
43
+ @dataclass
44
+ class TaskInput:
45
+ """Standardized task input format for all agents"""
46
+ task_content: str # The specific task content
47
+ task_steps_for_reference: Optional[str] = None # Reference steps for execution
48
+ deliverable_contents: Optional[str] = None # Format of final deliverable
49
+ current_task_status: Optional[str] = None # Description of current task status
50
+ task_executor: str = "info_seeker" # Name of task executor (info_seeker, writer)
51
+ workspace_id: Optional[str] = None # Workspace ID for stored files and memory
52
+ acceptance_checking_criteria: Optional[str] = None # Criteria for determining task completion and quality
53
+
54
+ def to_dict(self) -> Dict[str, Any]:
55
+ """Convert TaskInput to dictionary format"""
56
+ return {
57
+ "task_content": self.task_content,
58
+ "task_steps_for_reference": self.task_steps_for_reference,
59
+ "deliverable_contents": self.deliverable_contents,
60
+ "current_task_status": self.current_task_status,
61
+ "task_executor": self.task_executor,
62
+ "workspace_id": self.workspace_id,
63
+ "acceptance_checking_criteria": self.acceptance_checking_criteria
64
+ }
65
+
66
+ @classmethod
67
+ def from_dict(cls, data: Dict[str, Any]) -> 'TaskInput':
68
+ """Create TaskInput from dictionary"""
69
+ return cls(
70
+ task_content=data.get("task_content", ""),
71
+ task_steps_for_reference=data.get("task_steps_for_reference"),
72
+ deliverable_contents=data.get("deliverable_contents"),
73
+ current_task_status=data.get("current_task_status"),
74
+ task_executor=data.get("task_executor", "info_seeker"),
75
+ workspace_id=data.get("workspace_id"),
76
+ acceptance_checking_criteria=data.get("acceptance_checking_criteria")
77
+ )
78
+
79
+ def format_for_prompt(self) -> str:
80
+ """Format the task input for use in prompts"""
81
+ prompt = f"Task Content:\n{self.task_content}\n\n"
82
+
83
+ if self.task_steps_for_reference:
84
+ prompt += f"Task Steps for Reference:\n{self.task_steps_for_reference}\n\n"
85
+
86
+ if self.deliverable_contents:
87
+ prompt += f"Deliverable Contents:\n{self.deliverable_contents}\n\n"
88
+
89
+ if self.current_task_status:
90
+ prompt += f"Current Task Status:\n{self.current_task_status}\n\n"
91
+
92
+ if self.acceptance_checking_criteria:
93
+ prompt += f"Acceptance Checking Criteria:\n{self.acceptance_checking_criteria}\n\n"
94
+
95
+ prompt += f"Task Executor: {self.task_executor}\n"
96
+
97
+ if self.workspace_id:
98
+ prompt += f"Workspace ID: {self.workspace_id}\n"
99
+
100
+ return prompt
101
+
102
+
103
+ class SectionWriterTaskInput(TaskInput):
104
+ """
105
+ Specialized TaskInput for section writing tasks
106
+
107
+ Only stores the essential parameters. The section_writer agent
108
+ will handle prompt assembly internally.
109
+ """
110
+
111
+ def __init__(
112
+ self,
113
+ task_content: str,
114
+ user_query: str,
115
+ write_file_path: str,
116
+ overall_outline: str,
117
+ current_chapter_outline: str,
118
+ key_files: List[Dict[str, Any]],
119
+ written_chapters: str = "",
120
+ workspace_id: Optional[str] = None
121
+ ):
122
+ # Store the section writer specific parameters
123
+ self.write_file_path = write_file_path
124
+ self.user_query = user_query
125
+ self.current_chapter_outline = current_chapter_outline
126
+ self.key_files = key_files
127
+ self.written_chapters = written_chapters
128
+ self.overall_outline = overall_outline
129
+
130
+ # Initialize parent TaskInput with minimal required fields
131
+ super().__init__(
132
+ task_content=task_content,
133
+ task_executor="section_writer",
134
+ workspace_id=workspace_id,
135
+ )
136
+
137
+
138
+ class WriterAgentTaskInput(TaskInput):
139
+ """
140
+ Specialized TaskInput for section writing tasks
141
+
142
+ Only stores the 4 essential parameters. The section_writer agent
143
+ will handle prompt assembly internally.
144
+ """
145
+
146
+ def __init__(
147
+ self,
148
+ task_content: str,
149
+ user_query: str,
150
+ key_files: List[Dict[str, Any]],
151
+ workspace_id: Optional[str] = None
152
+ ):
153
+ # Store the section writer specific parameters
154
+ self.user_query = user_query
155
+ self.key_files = key_files
156
+
157
+ # Initialize parent TaskInput with minimal required fields
158
+ super().__init__(
159
+ task_content=task_content,
160
+ task_executor="writer_agent",
161
+ workspace_id=workspace_id,
162
+ )
163
+
164
+
165
+ class BaseAgent(ABC):
166
+ """
167
+ Base class for all agents with MCP server-managed sessions.
168
+
169
+ Session management is now entirely handled by the MCP server:
170
+ - Server assigns session IDs on connection
171
+ - Server creates workspace folders with UUID names
172
+ - All tool operations are performed in server-managed workspaces
173
+ """
174
+
175
+ def __init__(self, config: AgentConfig, shared_mcp_client=None):
176
+ self.execution_stats = None
177
+ self.reasoning_trace = None
178
+ self.config = config
179
+ self.logger = logging.getLogger(f"{__name__}.{config.agent_name}")
180
+
181
+ # Session info is populated by the MCP server
182
+ self.session_info = None
183
+
184
+ # Tool management
185
+ self.mcp_tools = None
186
+ self.available_tools = {}
187
+
188
+ self.reset_trace()
189
+
190
+ # Initialize MCP tools (server will handle session creation or use shared client)
191
+ self._initialize(shared_mcp_client)
192
+
193
+ def _initialize(self, shared_mcp_client=None):
194
+ """Initialize agent with MCP server connection or shared client"""
195
+ try:
196
+ self.logger.info(f"Initializing agent {self.config.agent_name}")
197
+
198
+ if shared_mcp_client:
199
+ # Use shared MCP client with agent-specific tool filtering
200
+ agent_type = self._get_agent_type()
201
+ self.mcp_tools = self._create_filtered_mcp_tools(shared_mcp_client, agent_type)
202
+ self.logger.info(f"Agent {self.config.agent_name} using shared MCP client with {agent_type} tools")
203
+ else:
204
+ # Create MCP tools with agent-specific filtering (no more unfiltered access)
205
+ self.mcp_tools = self._create_filtered_mcp_tools_standalone()
206
+
207
+ # Discover available tools
208
+ self.available_tools = self._discover_mcp_tools()
209
+
210
+ # Build tool schemas for function calling
211
+ self.tool_schemas = self._build_tool_schemas()
212
+
213
+ self.logger.info(f"Agent {self.config.agent_name} initialized successfully")
214
+ self.logger.info(f"Available tools: {list(self.available_tools.keys())}")
215
+
216
+ except Exception as e:
217
+ self.logger.error(f"Failed to initialize agent {self.config.agent_name}: {e}")
218
+ raise
219
+
220
+ def _discover_mcp_tools(self) -> Dict[str, Any]:
221
+ """Discover available tools from MCP server or fallback tools"""
222
+ available_tools = {}
223
+
224
+ # Try to get tools from MCP client first
225
+ if hasattr(self.mcp_tools, 'get_available_tools'):
226
+ try:
227
+ mcp_tools_dict = self.mcp_tools.get_available_tools()
228
+ for tool_name, tool_info in mcp_tools_dict.items():
229
+ # For proper MCP architecture, store tool info for direct client calls
230
+ # instead of creating wrapper lambda functions
231
+ available_tools[tool_name] = tool_info
232
+
233
+ if available_tools:
234
+ self.logger.info(f"Discovered {len(available_tools)} tools from MCP server")
235
+ return available_tools
236
+ except Exception as e:
237
+ self.logger.warning(f"Failed to discover MCP tools: {e}")
238
+
239
+ # Fallback: if MCP client not available, use direct method access
240
+ # This should rarely be needed with proper MCP setup
241
+ if hasattr(self.mcp_tools, '__dict__'):
242
+ for attr_name in dir(self.mcp_tools):
243
+ if not attr_name.startswith('_') and callable(getattr(self.mcp_tools, attr_name)):
244
+ available_tools[attr_name] = getattr(self.mcp_tools, attr_name)
245
+
246
+ return available_tools
247
+
248
+ def _get_agent_type(self) -> str:
249
+ """Get agent type for tool filtering"""
250
+ agent_name = self.config.agent_name.lower()
251
+ if "planner" in agent_name:
252
+ return "planner"
253
+ elif "information" in agent_name or "seeker" in agent_name:
254
+ return "information_seeker"
255
+ elif "writer" in agent_name:
256
+ return "writer"
257
+ else:
258
+ # Default to planner tools for unknown agent types
259
+ return "planner"
260
+
261
+ def _create_filtered_mcp_tools(self, shared_client, agent_type: str):
262
+ """Create filtered MCP tools adapter using shared client"""
263
+ try:
264
+ from src.tools.mcp_client import create_filtered_mcp_tools_adapter
265
+ return create_filtered_mcp_tools_adapter(shared_client, agent_type)
266
+ except ImportError:
267
+ # Fallback if FilteredMCPToolsAdapter not available
268
+ self.logger.warning("FilteredMCPToolsAdapter not available, using regular adapter")
269
+ from src.tools.mcp_client import MCPToolsAdapter
270
+ adapter = MCPToolsAdapter.__new__(MCPToolsAdapter)
271
+ adapter.client = shared_client
272
+ return adapter
273
+
274
+ def _create_filtered_mcp_tools_standalone(self):
275
+ """Create filtered MCP tools adapter with its own client connection"""
276
+ try:
277
+ # Get agent type for filtering
278
+ agent_type = self._get_agent_type()
279
+
280
+ # Create a new MCP client
281
+ client = self._create_new_mcp_client()
282
+
283
+ # Apply filtering based on agent type
284
+ from src.tools.mcp_client import create_filtered_mcp_tools_adapter
285
+ filtered_adapter = create_filtered_mcp_tools_adapter(client, agent_type)
286
+
287
+ self.logger.info(f"Agent {self.config.agent_name} created filtered MCP adapter with {agent_type} tools")
288
+ return filtered_adapter
289
+
290
+ except Exception as e:
291
+ self.logger.error(f"Failed to create filtered MCP tools: {e}")
292
+ raise RuntimeError(f"Failed to create filtered MCP client for {self.config.agent_name}: {e}")
293
+
294
+ def _create_new_mcp_client(self):
295
+ """Create a new MCP client connection"""
296
+ try:
297
+ # Get MCP configuration
298
+ from config.config import get_mcp_config
299
+ mcp_config = get_mcp_config()
300
+
301
+ # Create MCP client
302
+ from src.tools.mcp_client import MCPClient
303
+
304
+ if mcp_config.get("server_url") and not mcp_config.get("use_stdio", True):
305
+ # HTTP-based MCP server
306
+ client = MCPClient(server_url=mcp_config["server_url"])
307
+ self.logger.info(
308
+ f"Agent {self.config.agent_name} connected to HTTP MCP server: {mcp_config['server_url']}")
309
+ else:
310
+ # Default to the expected HTTP MCP server on port 6274
311
+ client = MCPClient(server_url="http://localhost:6274/mcp")
312
+ self.logger.info(
313
+ f"Agent {self.config.agent_name} connected to default HTTP MCP server: http://localhost:6274/mcp")
314
+
315
+ return client
316
+
317
+ except Exception as e:
318
+ self.logger.error(f"Failed to create MCP client: {e}")
319
+ raise RuntimeError(f"MCP client creation failed for {self.config.agent_name}: {e}")
320
+
321
+ # NOTE: _create_mcp_tools() method removed to prevent unfiltered tool access.
322
+ # All agents now use _create_filtered_mcp_tools_standalone() or _create_filtered_mcp_tools()
323
+ # to ensure proper tool isolation and security.
324
+
325
+ def get_session_info(self) -> Optional[Dict[str, Any]]:
326
+ """Get information about the current server-managed session"""
327
+ try:
328
+ # First try the adapter's get_session_info method if available
329
+ if hasattr(self.mcp_tools, 'get_session_info'):
330
+ session_info = self.mcp_tools.get_session_info()
331
+ if session_info:
332
+ # Add agent-specific information
333
+ session_info.update({
334
+ "server_managed": True,
335
+ "agent_name": self.config.agent_name
336
+ })
337
+ return session_info
338
+
339
+ # Fallback: Check if we have an MCP tools adapter with a client
340
+ if hasattr(self.mcp_tools, 'client'):
341
+ client = self.mcp_tools.client
342
+
343
+ # Check if client has session ID and connection status
344
+ if hasattr(client, '_session_id') and hasattr(client, 'is_connected'):
345
+ return {
346
+ "session_id": client._session_id,
347
+ "server_managed": True,
348
+ "agent_name": self.config.agent_name,
349
+ "connected": client.is_connected()
350
+ }
351
+
352
+ # Fallback: check if mcp_tools has session info directly
353
+ if hasattr(self.mcp_tools, '_session_id'):
354
+ return {
355
+ "session_id": self.mcp_tools._session_id,
356
+ "server_managed": True,
357
+ "agent_name": self.config.agent_name,
358
+ "connected": getattr(self.mcp_tools, 'is_connected', lambda: True)()
359
+ }
360
+
361
+ # If no session info available, return basic info
362
+ return {
363
+ "session_id": None,
364
+ "server_managed": True,
365
+ "agent_name": self.config.agent_name,
366
+ "connected": hasattr(self.mcp_tools, 'client') and getattr(self.mcp_tools.client, 'is_connected',
367
+ lambda: False)()
368
+ }
369
+
370
+ except Exception as e:
371
+ self.logger.warning(f"Failed to get session info: {e}")
372
+ return {
373
+ "session_id": None,
374
+ "server_managed": True,
375
+ "agent_name": self.config.agent_name,
376
+ "connected": False,
377
+ "error": str(e)
378
+ }
379
+
380
+ def _build_tool_schemas(self) -> List[Dict[str, Any]]:
381
+ """Build tool schemas for function calling"""
382
+ schemas = []
383
+
384
+ # Get agent-specific tool schemas
385
+ agent_schemas = self._build_agent_specific_tool_schemas()
386
+ schemas.extend(agent_schemas)
387
+
388
+ return schemas
389
+
390
+ def _build_agent_specific_tool_schemas(self) -> List[Dict[str, Any]]:
391
+ """
392
+ Build agent-specific tool schemas using proper MCP architecture.
393
+ Schemas come from MCP server via client, not direct imports.
394
+ """
395
+ schemas = []
396
+
397
+ # Proper MCP way: Get schemas from MCP client (which got them from server)
398
+ try:
399
+ if hasattr(self.mcp_tools, 'get_tool_schemas'):
400
+ # Use the MCP client to get schemas (proper MCP architecture)
401
+ schemas = self.mcp_tools.get_tool_schemas()
402
+ self.logger.info(f"Retrieved {len(schemas)} tool schemas from MCP server")
403
+ else:
404
+ # Fallback for adapters that don't have the new method yet
405
+ self.logger.warning("MCP adapter doesn't support get_tool_schemas, using fallback")
406
+ schemas = self._build_fallback_schemas()
407
+ except Exception as e:
408
+ self.logger.warning(f"Failed to get schemas from MCP client: {e}, using fallback")
409
+ schemas = self._build_fallback_schemas()
410
+
411
+ return schemas
412
+
413
+ def _build_fallback_schemas(self) -> List[Dict[str, Any]]:
414
+ """Fallback schema building if MCP client method fails"""
415
+ schemas = []
416
+
417
+ # Try to get tool info from MCP client
418
+ if hasattr(self.mcp_tools, 'get_available_tools'):
419
+ try:
420
+ available_tools = self.mcp_tools.get_available_tools()
421
+ for tool_name, tool_info in available_tools.items():
422
+ schema = {
423
+ "type": "function",
424
+ "function": {
425
+ "name": tool_name,
426
+ "description": getattr(tool_info, 'description', f"Tool: {tool_name}"),
427
+ "parameters": getattr(tool_info, 'input_schema', {"type": "object", "properties": {}, "required": []})
428
+ }
429
+ }
430
+ schemas.append(schema)
431
+ self.logger.info(f"Built {len(schemas)} schemas using fallback method")
432
+ except Exception as e:
433
+ self.logger.warning(f"Fallback schema building failed: {e}")
434
+
435
+ return schemas
436
+
437
+ def execute_tool_call(self, tool_call) -> Dict[str, Any]:
438
+ """Execute a tool call and return results using proper MCP architecture"""
439
+ tool_name = tool_call["name"]
440
+
441
+ try:
442
+ # Parse arguments
443
+ arguments = tool_call["arguments"]
444
+
445
+ # Check if tool is available
446
+ if tool_name not in self.available_tools:
447
+ return {
448
+ "success": False,
449
+ "error": f"Tool '{tool_name}' not available for this agent"
450
+ }
451
+
452
+ # Route tool execution based on tool type
453
+ # Built-in tools (like assign_task_to_*) are callable methods, not MCP server tools
454
+ if callable(self.available_tools[tool_name]):
455
+ # Built-in tool: execute locally
456
+ tool_function = self.available_tools[tool_name]
457
+ result = tool_function(**arguments)
458
+
459
+ # Convert result to standard format
460
+ if hasattr(result, 'to_dict'):
461
+ return result.to_dict()
462
+ elif isinstance(result, dict):
463
+ return result
464
+ else:
465
+ return {
466
+ "success": True,
467
+ "data": result,
468
+ "error": None,
469
+ "metadata": {}
470
+ }
471
+
472
+ elif hasattr(self.mcp_tools, 'client') and hasattr(self.mcp_tools.client, 'call_tool'):
473
+ # MCP server tool: execute via client
474
+ result = self.mcp_tools.client.call_tool(tool_name, arguments)
475
+
476
+ # Convert MCPClientResult to standard format
477
+ if hasattr(result, 'success'):
478
+ return {
479
+ "success": result.success,
480
+ "data": result.data,
481
+ "error": result.error,
482
+ "metadata": getattr(result, 'metadata', {})
483
+ }
484
+ else:
485
+ return result
486
+ else:
487
+ return {
488
+ "success": False,
489
+ "error": f"Tool '{tool_name}' is not executable (neither built-in nor MCP)"
490
+ }
491
+
492
+ except Exception as e:
493
+ self.logger.error(f"Error executing tool {tool_name}: {e}")
494
+ return {
495
+ "success": False,
496
+ "error": f"Tool execution failed: {str(e)}"
497
+ }
498
+
499
+ def log_reasoning(self, iteration: int, reasoning: str):
500
+ """Log reasoning step in the trace"""
501
+ self.reasoning_trace.append({
502
+ "type": "reasoning",
503
+ "iteration": iteration,
504
+ "content": reasoning,
505
+ "timestamp": time.time()
506
+ })
507
+ self.execution_stats["reasoning_steps"] += 1
508
+ self.execution_stats["total_steps"] += 1
509
+ self.logger.info(f"Reasoning (Iter {iteration}): {reasoning[:100]}...")
510
+
511
+ def log_action(self, iteration: int, tool: str, arguments: Dict[str, Any], result: Dict[str, Any]):
512
+ """Log action step in the trace"""
513
+ self.reasoning_trace.append({
514
+ "type": "action",
515
+ "iteration": iteration,
516
+ "tool": tool,
517
+ "arguments": arguments,
518
+ "result": result,
519
+ "timestamp": time.time()
520
+ })
521
+ self.execution_stats["action_steps"] += 1
522
+ self.execution_stats["total_steps"] += 1
523
+
524
+ # Log success/failure
525
+ success = result.get("success", True)
526
+ status = "Success" if success else "Failed"
527
+ self.logger.info(f"Action (Iter {iteration}): {tool} -> {status} -> {str(arguments)[:400]}...")
528
+
529
+ def log_error(self, iteration: int, error: str):
530
+ """Log error in the trace"""
531
+ self.reasoning_trace.append({
532
+ "type": "error",
533
+ "iteration": iteration,
534
+ "error": error,
535
+ "timestamp": time.time()
536
+ })
537
+ self.execution_stats["error_steps"] += 1
538
+ self.execution_stats["total_steps"] += 1
539
+ self.logger.error(f"Error (Iter {iteration}): {error}")
540
+
541
+ def reset_trace(self):
542
+ """Reset the reasoning trace for a new task"""
543
+ self.reasoning_trace = []
544
+ self.execution_stats = {
545
+ "total_steps": 0,
546
+ "reasoning_steps": 0,
547
+ "action_steps": 0,
548
+ "error_steps": 0,
549
+ "tool_usage": {},
550
+ "success_rate": 1.0
551
+ }
552
+
553
+ def get_execution_stats(self) -> Dict[str, Any]:
554
+ """Get execution statistics"""
555
+ # Calculate success rate
556
+ if self.execution_stats["action_steps"] > 0:
557
+ failed_actions = sum(1 for step in self.reasoning_trace
558
+ if step.get("type") == "action"
559
+ and not step.get("result", {}).get("success", True))
560
+ self.execution_stats["success_rate"] = (
561
+ (self.execution_stats["action_steps"] - failed_actions) /
562
+ self.execution_stats["action_steps"]
563
+ )
564
+
565
+ return self.execution_stats.copy()
566
+
567
+ def create_response(self, success: bool, result: Dict[str, Any] = None,
568
+ error: str = None, iterations: int = 0,
569
+ execution_time: float = 0.0) -> AgentResponse:
570
+ """Create a standardized agent response"""
571
+ return AgentResponse(
572
+ success=success,
573
+ result=result,
574
+ error=error,
575
+ iterations=iterations,
576
+ reasoning_trace=self.reasoning_trace.copy(),
577
+ agent_name=self.config.agent_name,
578
+ execution_time=execution_time
579
+ )
580
+
581
+ def validate_config(self) -> bool:
582
+ """Validate agent configuration"""
583
+ try:
584
+ # Check required fields
585
+ if not self.config.agent_name:
586
+ return False
587
+ if not self.config.model:
588
+ return False
589
+ if self.config.max_iterations <= 0:
590
+ return False
591
+ if not (0.0 <= self.config.temperature <= 2.0):
592
+ return False
593
+ if self.config.max_tokens <= 0:
594
+ return False
595
+
596
+ return True
597
+ except Exception:
598
+ return False
599
+
600
+ @abstractmethod
601
+ def execute_task(self, task_input: TaskInput) -> AgentResponse:
602
+ """
603
+ Execute a task using the standardized TaskInput format
604
+
605
+ Args:
606
+ task_input: TaskInput object with standardized task information
607
+
608
+ Returns:
609
+ AgentResponse with results and process trace
610
+ """
611
+ pass
612
+
613
+ @abstractmethod
614
+ def _build_system_prompt(self) -> str:
615
+ """Build the system prompt for this agent"""
616
+ pass
617
+
618
+
619
+ # Simple factory function for creating agent configurations
620
+
621
+ def create_agent_config(
622
+ agent_name: str,
623
+ model: Optional[str] = None,
624
+ max_iterations: Optional[int] = None,
625
+ temperature: Optional[float] = None,
626
+ max_tokens: Optional[int] = None
627
+ ) -> AgentConfig:
628
+ """
629
+ Create an AgentConfig instance for server-managed sessions.
630
+
631
+ Args:
632
+ agent_name: Name of the agent
633
+ model: LLM model to use
634
+ max_iterations: Maximum number of iterations
635
+ temperature: LLM temperature setting
636
+ max_tokens: Maximum tokens for LLM response
637
+
638
+ Returns:
639
+ Configured AgentConfig instance
640
+ """
641
+ # Load env-backed defaults
642
+ try:
643
+ from config.config import get_config
644
+ api_cfg = get_config()
645
+ except Exception as e:
646
+ raise ValueError(f"Failed to load global configuration: {e}")
647
+
648
+ planner_mode = getattr(api_cfg, "planner_mode", "auto")
649
+
650
+ resolved_model = model if model is not None else getattr(api_cfg, "model_name", None)
651
+ if not resolved_model:
652
+ raise ValueError("Model is not specified and MODEL_NAME is not set in environment")
653
+
654
+ resolved_temperature = temperature if temperature is not None else getattr(api_cfg, "model_temperature", None)
655
+ if resolved_temperature is None:
656
+ raise ValueError("Temperature is not specified and MODEL_TEMPERATURE is not set in environment")
657
+
658
+ resolved_max_tokens = max_tokens if max_tokens is not None else getattr(api_cfg, "model_max_tokens", None)
659
+ if resolved_max_tokens is None:
660
+ raise ValueError("Max tokens is not specified and MODEL_MAX_TOKENS is not set in environment")
661
+
662
+ # Optional paths used by writer and others
663
+ trajectory_storage_path = getattr(api_cfg, "trajectory_storage_path", None)
664
+ report_output_path = getattr(api_cfg, "report_output_path", None)
665
+ document_analysis_path = getattr(api_cfg, "document_analysis_path", None)
666
+
667
+ # Resolve max_iterations per agent type
668
+ if max_iterations is None:
669
+ agent_lower = (agent_name or "").lower()
670
+ resolved_max_iterations = None
671
+ if "planner" in agent_lower:
672
+ resolved_max_iterations = getattr(api_cfg, "planner_max_iterations", None)
673
+ elif "writer" in agent_lower:
674
+ resolved_max_iterations = getattr(api_cfg, "writer_max_iterations", None)
675
+ elif "information" in agent_lower or "seeker" in agent_lower:
676
+ resolved_max_iterations = getattr(api_cfg, "information_seeker_max_iterations", None)
677
+ # if not found in env, raise
678
+ if resolved_max_iterations is None:
679
+ raise ValueError("Max iterations not specified and no env override (PLANNER_MAX_ITERATION/WRITER_MAX_ITERATION/INFORMATION_SEEKER_MAX_ITERATION)")
680
+ max_iterations = resolved_max_iterations
681
+
682
+ return AgentConfig(
683
+ agent_name=agent_name,
684
+ planner_mode=planner_mode,
685
+ model=resolved_model,
686
+ max_iterations=int(max_iterations),
687
+ temperature=resolved_temperature,
688
+ max_tokens=resolved_max_tokens,
689
+ trajectory_storage_path=trajectory_storage_path,
690
+ report_output_path=report_output_path,
691
+ document_analysis_path=document_analysis_path
692
+ )