dacp 0.3.1__py3-none-any.whl → 0.3.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
dacp/orchestrator.py CHANGED
@@ -1,248 +1,296 @@
1
1
  """
2
- DACP Orchestrator - Manages agent registration and message routing.
2
+ DACP Orchestrator - Agent management and message routing.
3
+
4
+ This module provides the core orchestrator functionality for managing agents
5
+ and routing messages between them.
3
6
  """
4
7
 
5
8
  import logging
6
9
  import time
7
10
  from typing import Dict, Any, List, Optional
8
- from .tools import run_tool, TOOL_REGISTRY
9
- from .protocol import (
10
- parse_agent_response,
11
- is_tool_request,
12
- get_tool_request,
13
- wrap_tool_result,
14
- is_final_response,
15
- get_final_response,
16
- )
17
-
18
- # Set up logger for this module
11
+
12
+ from .tools import execute_tool
13
+
19
14
  logger = logging.getLogger("dacp.orchestrator")
20
15
 
21
16
 
22
17
  class Agent:
23
- """Base class for DACP agents."""
24
-
18
+ """
19
+ Base agent class that all DACP agents should inherit from.
20
+
21
+ This provides the standard interface for agent communication.
22
+ """
23
+
25
24
  def handle_message(self, message: Dict[str, Any]) -> Dict[str, Any]:
26
- """Handle an incoming message. Subclasses should override this method."""
25
+ """
26
+ Handle incoming messages.
27
+
28
+ Args:
29
+ message: Message dictionary containing task and parameters
30
+
31
+ Returns:
32
+ Response dictionary with either 'response', 'tool_request', or 'error'
33
+ """
27
34
  raise NotImplementedError("Agents must implement handle_message method")
28
35
 
29
36
 
30
37
  class Orchestrator:
31
38
  """
32
39
  Central orchestrator for managing agents and routing messages.
40
+
41
+ The orchestrator handles agent registration, message routing, tool execution,
42
+ and conversation history management.
33
43
  """
34
-
35
- def __init__(self):
36
- """Initialize the orchestrator."""
37
- self.agents: Dict[str, Any] = {}
44
+
45
+ def __init__(self, session_id: Optional[str] = None):
46
+ """Initialize orchestrator with optional session ID."""
47
+ self.agents: Dict[str, Agent] = {}
48
+ self.session_id = session_id or f"session_{int(time.time())}"
38
49
  self.conversation_history: List[Dict[str, Any]] = []
39
- self.session_id = f"session_{int(time.time())}"
50
+
40
51
  logger.info(f"🎭 Orchestrator initialized with session ID: {self.session_id}")
41
-
42
- def register_agent(self, agent_id: str, agent: Any) -> None:
52
+
53
+ def register_agent(self, name: str, agent: Agent) -> None:
43
54
  """
44
55
  Register an agent with the orchestrator.
45
-
56
+
46
57
  Args:
47
- agent_id: Unique identifier for the agent
48
- agent: Agent instance that implements handle_message method
58
+ name: Unique name for the agent
59
+ agent: Agent instance implementing the Agent interface
49
60
  """
50
- if not hasattr(agent, 'handle_message'):
51
- logger.error(f"Agent '{agent_id}' does not implement handle_message method")
52
- raise ValueError(f"Agent must implement handle_message method")
53
-
54
- self.agents[agent_id] = agent
55
- logger.info(f"✅ Agent '{agent_id}' registered successfully (type: {type(agent).__name__})")
61
+ if not isinstance(agent, Agent):
62
+ raise ValueError("Agent must inherit from dacp.Agent base class")
63
+
64
+ self.agents[name] = agent
65
+ logger.info(
66
+ f"✅ Agent '{name}' registered successfully "
67
+ f"(type: {type(agent).__name__})"
68
+ )
56
69
  logger.debug(f"📊 Total registered agents: {len(self.agents)}")
57
-
58
- def unregister_agent(self, agent_id: str) -> bool:
70
+
71
+ def unregister_agent(self, name: str) -> bool:
59
72
  """
60
73
  Unregister an agent from the orchestrator.
61
-
74
+
62
75
  Args:
63
- agent_id: Unique identifier for the agent
64
-
76
+ name: Name of the agent to unregister
77
+
65
78
  Returns:
66
- True if agent was successfully unregistered, False if not found
79
+ True if agent was unregistered, False if not found
67
80
  """
68
- if agent_id in self.agents:
69
- agent_type = type(self.agents[agent_id]).__name__
70
- del self.agents[agent_id]
71
- logger.info(f"🗑️ Agent '{agent_id}' unregistered successfully (was type: {agent_type})")
72
- logger.debug(f"📊 Remaining registered agents: {len(self.agents)}")
81
+ if name in self.agents:
82
+ del self.agents[name]
83
+ logger.info(f"🗑️ Agent '{name}' unregistered successfully")
84
+ logger.debug(f"📊 Remaining agents: {len(self.agents)}")
73
85
  return True
74
86
  else:
75
- logger.warning(f"⚠️ Attempted to unregister unknown agent: '{agent_id}'")
87
+ logger.warning(f"⚠️ Agent '{name}' not found for unregistration")
76
88
  return False
77
-
78
- def send_message(self, agent_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
89
+
90
+ def list_agents(self) -> List[str]:
91
+ """Get list of registered agent names."""
92
+ return list(self.agents.keys())
93
+
94
+ def send_message(self, agent_name: str, message: Dict[str, Any]) -> Dict[str, Any]:
79
95
  """
80
96
  Send a message to a specific agent.
81
-
97
+
82
98
  Args:
83
- agent_id: Target agent identifier
84
- message: Message to send
85
-
99
+ agent_name: Name of the target agent
100
+ message: Message dictionary to send
101
+
86
102
  Returns:
87
- Response from the agent (or error if agent not found)
103
+ Response from the agent after processing
88
104
  """
89
- logger.info(f"📨 Sending message to agent '{agent_id}'")
105
+ start_time = time.time()
106
+
107
+ logger.info(f"📨 Sending message to agent '{agent_name}'")
90
108
  logger.debug(f"📋 Message content: {message}")
91
-
92
- if agent_id not in self.agents:
93
- error_msg = f"Agent '{agent_id}' not found"
109
+
110
+ if agent_name not in self.agents:
111
+ error_msg = f"Agent '{agent_name}' not found"
94
112
  logger.error(f"❌ {error_msg}")
95
- logger.debug(f"📊 Available agents: {list(self.agents.keys())}")
96
113
  return {"error": error_msg}
97
-
98
- agent = self.agents[agent_id]
99
-
114
+
115
+ agent = self.agents[agent_name]
116
+
100
117
  try:
101
- start_time = time.time()
102
- logger.debug(f"🔄 Calling handle_message on agent '{agent_id}'")
103
-
118
+ logger.debug(f"🔄 Calling handle_message on agent '{agent_name}'")
119
+
120
+ # Call the agent's message handler
104
121
  response = agent.handle_message(message)
105
-
106
- processing_time = time.time() - start_time
107
- logger.info(f"✅ Agent '{agent_id}' responded in {processing_time:.3f}s")
108
- logger.debug(f"📤 Agent response: {response}")
109
-
110
- # Check if agent requested a tool
111
- if is_tool_request(response):
112
- logger.info(f"🔧 Agent '{agent_id}' requested tool execution")
113
- tool_name, tool_args = get_tool_request(response)
114
- logger.info(f"🛠️ Executing tool: '{tool_name}' with args: {tool_args}")
115
-
116
- if tool_name in TOOL_REGISTRY:
117
- try:
118
- tool_start_time = time.time()
119
- tool_result = run_tool(tool_name, tool_args)
120
- tool_execution_time = time.time() - tool_start_time
121
-
122
- logger.info(f"✅ Tool '{tool_name}' executed successfully in {tool_execution_time:.3f}s")
123
- logger.debug(f"🔧 Tool result: {tool_result}")
124
-
125
- wrapped_result = wrap_tool_result(tool_name, tool_result)
126
-
127
- # Log the conversation
128
- self._log_conversation(agent_id, message, wrapped_result, tool_used=tool_name)
129
-
130
- return wrapped_result
131
-
132
- except Exception as e:
133
- error_msg = f"Tool '{tool_name}' execution failed: {str(e)}"
134
- logger.error(f"❌ {error_msg}")
135
- error_response = {"error": error_msg}
136
- self._log_conversation(agent_id, message, error_response, tool_used=tool_name)
137
- return error_response
122
+
123
+ # Handle Pydantic models by converting to dict
124
+ if hasattr(response, 'model_dump'):
125
+ logger.debug(f"📊 Converting Pydantic model to dict: {type(response).__name__}")
126
+ response = response.model_dump()
127
+ elif not isinstance(response, dict):
128
+ logger.debug(f"📊 Converting response to dict: {type(response)}")
129
+ if hasattr(response, '__dict__'):
130
+ response = response.__dict__
138
131
  else:
139
- error_msg = f"Unknown tool requested: '{tool_name}'"
140
- logger.error(f"❌ {error_msg}")
141
- logger.debug(f"📊 Available tools: {list(TOOL_REGISTRY.keys())}")
142
- error_response = {"error": error_msg}
143
- self._log_conversation(agent_id, message, error_response)
144
- return error_response
145
-
146
- # Log successful conversation
147
- self._log_conversation(agent_id, message, response)
132
+ response = {"result": str(response)}
133
+
134
+ duration = time.time() - start_time
135
+ logger.info(f"✅ Agent '{agent_name}' responded in {duration:.3f}s")
136
+ logger.debug(f"📤 Agent response: {response}")
137
+
138
+ # Check if agent requested tool execution
139
+ if isinstance(response, dict) and "tool_request" in response:
140
+ logger.info(f"🔧 Agent '{agent_name}' requested tool execution")
141
+ response = self._handle_tool_request(
142
+ agent_name, response["tool_request"]
143
+ )
144
+
145
+ # Log the conversation
146
+ self._log_conversation(agent_name, message, response, duration)
147
+
148
148
  return response
149
-
149
+
150
150
  except Exception as e:
151
- error_msg = f"Agent '{agent_id}' error: {str(e)}"
151
+ duration = time.time() - start_time
152
+ error_msg = f"Error in agent '{agent_name}': {type(e).__name__}: {e}"
152
153
  logger.error(f"❌ {error_msg}")
153
- logger.debug(f"🐛 Exception details: {type(e).__name__}: {e}")
154
+ logger.debug("💥 Exception details", exc_info=True)
155
+
154
156
  error_response = {"error": error_msg}
155
- self._log_conversation(agent_id, message, error_response)
157
+ self._log_conversation(agent_name, message, error_response, duration)
158
+
156
159
  return error_response
157
-
158
- def broadcast_message(self, message: Dict[str, Any], exclude_agents: Optional[List[str]] = None) -> Dict[str, Dict[str, Any]]:
160
+
161
+ def broadcast_message(self, message: Dict[str, Any]) -> Dict[str, Any]:
159
162
  """
160
- Send a message to all registered agents (optionally excluding some).
161
-
163
+ Send a message to all registered agents.
164
+
162
165
  Args:
163
- message: Message to broadcast
164
- exclude_agents: List of agent IDs to exclude from broadcast
165
-
166
+ message: Message dictionary to broadcast
167
+
166
168
  Returns:
167
- Dict mapping agent IDs to their responses
169
+ Dictionary mapping agent names to their responses
168
170
  """
169
- exclude_agents = exclude_agents or []
170
- target_agents = [aid for aid in self.agents.keys() if aid not in exclude_agents]
171
-
172
- logger.info(f"📡 Broadcasting message to {len(target_agents)} agents")
173
- logger.debug(f"🎯 Target agents: {target_agents}")
174
- if exclude_agents:
175
- logger.debug(f"🚫 Excluded agents: {exclude_agents}")
176
-
171
+ logger.info(f"📢 Broadcasting message to {len(self.agents)} agents")
172
+ logger.debug(f"📋 Broadcast message: {message}")
173
+
177
174
  responses = {}
178
175
  start_time = time.time()
179
-
180
- for agent_id in target_agents:
181
- logger.debug(f"📨 Broadcasting to agent '{agent_id}'")
182
- responses[agent_id] = self.send_message(agent_id, message)
183
-
184
- broadcast_time = time.time() - start_time
185
- logger.info(f"✅ Broadcast completed in {broadcast_time:.3f}s")
186
-
176
+
177
+ for agent_name in self.agents:
178
+ logger.debug(f"📨 Broadcasting to agent '{agent_name}'")
179
+ responses[agent_name] = self.send_message(agent_name, message)
180
+
181
+ duration = time.time() - start_time
182
+ logger.info(
183
+ f"✅ Broadcast completed in {duration:.3f}s "
184
+ f"({len(responses)} responses)"
185
+ )
186
+
187
187
  return responses
188
-
189
- def get_conversation_history(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]:
188
+
189
+ def _handle_tool_request(
190
+ self, agent_name: str, tool_request: Dict[str, Any]
191
+ ) -> Dict[str, Any]:
190
192
  """
191
- Get conversation history, optionally filtered by agent.
192
-
193
+ Handle tool execution request from an agent.
194
+
193
195
  Args:
194
- agent_id: Optional agent ID to filter by
195
-
196
- Returns:
197
- List of conversation entries
198
- """
199
- if agent_id is None:
200
- logger.debug(f"📚 Retrieving full conversation history ({len(self.conversation_history)} entries)")
201
- return self.conversation_history.copy()
202
- else:
203
- filtered_history = [
204
- entry for entry in self.conversation_history
205
- if entry.get("agent_id") == agent_id
206
- ]
207
- logger.debug(f"📚 Retrieving conversation history for '{agent_id}' ({len(filtered_history)} entries)")
208
- return filtered_history
209
-
210
- def clear_history(self) -> None:
211
- """Clear the conversation history."""
212
- old_count = len(self.conversation_history)
213
- self.conversation_history.clear()
214
- logger.info(f"🗑️ Conversation history cleared ({old_count} entries removed)")
215
-
216
- def get_session_info(self) -> Dict[str, Any]:
217
- """
218
- Get current session information.
219
-
196
+ agent_name: Name of the requesting agent
197
+ tool_request: Tool request dictionary with 'name' and 'args'
198
+
220
199
  Returns:
221
- Dict containing session metadata
200
+ Tool execution result
222
201
  """
223
- info = {
224
- "session_id": self.session_id,
225
- "registered_agents": list(self.agents.keys()),
226
- "conversation_count": len(self.conversation_history),
227
- "available_tools": list(TOOL_REGISTRY.keys())
228
- }
229
- logger.debug(f"📊 Session info requested: {info}")
230
- return info
231
-
232
- def _log_conversation(self, agent_id: str, message: Dict[str, Any], response: Dict[str, Any], tool_used: Optional[str] = None) -> None:
233
- """Log a conversation entry."""
202
+ tool_name = tool_request.get("name")
203
+ tool_args = tool_request.get("args", {})
204
+
205
+ if not tool_name:
206
+ return {"error": "Tool name is required"}
207
+
208
+ logger.info(f"🛠️ Executing tool: '{tool_name}' with args: {tool_args}")
209
+
210
+ start_time = time.time()
211
+
212
+ try:
213
+ result = execute_tool(tool_name, tool_args)
214
+ duration = time.time() - start_time
215
+
216
+ logger.info(
217
+ f"✅ Tool '{tool_name}' executed successfully in {duration:.3f}s"
218
+ )
219
+ logger.debug(f"🔧 Tool result: {result}")
220
+
221
+ return {"tool_result": {"name": tool_name, "result": result}}
222
+
223
+ except Exception as e:
224
+ duration = time.time() - start_time
225
+ error_msg = f"Tool '{tool_name}' failed: {type(e).__name__}: {e}"
226
+ logger.error(f"❌ {error_msg}")
227
+
228
+ return {"error": error_msg}
229
+
230
+ def _log_conversation(
231
+ self,
232
+ agent_name: str,
233
+ message: Dict[str, Any],
234
+ response: Dict[str, Any],
235
+ duration: float,
236
+ ) -> None:
237
+ """Log conversation entry to history."""
238
+ logger.debug("💾 Logging conversation entry")
239
+
234
240
  entry = {
235
241
  "timestamp": time.time(),
236
- "agent_id": agent_id,
242
+ "session_id": self.session_id,
243
+ "agent": agent_name,
244
+ "agent_name": agent_name,
237
245
  "message": message,
238
246
  "response": response,
239
- "session_id": self.session_id
247
+ "duration": duration,
240
248
  }
241
-
242
- if tool_used:
243
- entry["tool_used"] = tool_used
244
- logger.debug(f"💾 Logging conversation with tool usage: {tool_used}")
245
- else:
246
- logger.debug(f"💾 Logging conversation entry")
247
-
249
+
248
250
  self.conversation_history.append(entry)
251
+
252
+ # Keep history manageable (last 1000 entries)
253
+ if len(self.conversation_history) > 1000:
254
+ self.conversation_history = self.conversation_history[-1000:]
255
+ logger.debug("🗂️ Conversation history trimmed to 1000 entries")
256
+
257
+ def get_conversation_history(
258
+ self, limit: Optional[int] = None
259
+ ) -> List[Dict[str, Any]]:
260
+ """
261
+ Get conversation history.
262
+
263
+ Args:
264
+ limit: Maximum number of entries to return (None for all)
265
+
266
+ Returns:
267
+ List of conversation entries
268
+ """
269
+ if limit is None:
270
+ return self.conversation_history.copy()
271
+ else:
272
+ return self.conversation_history[-limit:].copy()
273
+
274
+ def clear_conversation_history(self) -> None:
275
+ """Clear conversation history."""
276
+ self.conversation_history.clear()
277
+ logger.info("🗑️ Conversation history cleared")
278
+
279
+ def get_session_metadata(self) -> Dict[str, Any]:
280
+ """Get session metadata and statistics."""
281
+ return {
282
+ "session_id": self.session_id,
283
+ "registered_agents": len(self.agents),
284
+ "agent_names": list(self.agents.keys()),
285
+ "conversation_entries": len(self.conversation_history),
286
+ "start_time": (
287
+ self.conversation_history[0]["timestamp"]
288
+ if self.conversation_history
289
+ else None
290
+ ),
291
+ "last_activity": (
292
+ self.conversation_history[-1]["timestamp"]
293
+ if self.conversation_history
294
+ else None
295
+ ),
296
+ }
dacp/tools.py CHANGED
@@ -1,86 +1,105 @@
1
+ """
2
+ DACP Tools - Built-in tool implementations.
3
+
4
+ This module provides the core tool functionality including tool registry,
5
+ execution, and built-in tools like file_writer.
6
+ """
7
+
1
8
  import logging
2
- from typing import Dict, Any, Callable
3
9
  from pathlib import Path
10
+ from typing import Dict, Any, Callable
4
11
 
5
- # Set up logger for this module
6
12
  logger = logging.getLogger("dacp.tools")
7
13
 
8
- TOOL_REGISTRY: Dict[str, Callable[..., Dict[str, Any]]] = {}
14
+ # Global tool registry
15
+ TOOL_REGISTRY: Dict[str, Callable[[Dict[str, Any]], Dict[str, Any]]] = {}
9
16
 
10
17
 
11
- def register_tool(tool_id: str, func: Callable[..., Dict[str, Any]]) -> None:
12
- """Register a tool function."""
13
- TOOL_REGISTRY[tool_id] = func
14
- logger.info(f"🔧 Tool '{tool_id}' registered successfully (function: {func.__name__})")
15
- logger.debug(f"📊 Total registered tools: {len(TOOL_REGISTRY)}")
18
+ def register_tool(name: str, func: Callable[[Dict[str, Any]], Dict[str, Any]]) -> None:
19
+ """
20
+ Register a tool function.
21
+
22
+ Args:
23
+ name: Name of the tool
24
+ func: Function that takes args dict and returns result dict
25
+ """
26
+ TOOL_REGISTRY[name] = func
27
+ logger.info(f"🔧 Tool '{name}' registered")
16
28
 
17
29
 
18
- def run_tool(tool_id: str, args: Dict[str, Any]) -> Dict[str, Any]:
19
- """Run a registered tool with the given arguments."""
20
- if tool_id not in TOOL_REGISTRY:
21
- logger.error(f"❌ Unknown tool requested: '{tool_id}'")
22
- logger.debug(f"📊 Available tools: {list(TOOL_REGISTRY.keys())}")
23
- raise ValueError(f"Unknown tool: {tool_id}")
30
+ def execute_tool(name: str, args: Dict[str, Any]) -> Dict[str, Any]:
31
+ """
32
+ Execute a tool by name with given arguments.
33
+
34
+ Args:
35
+ name: Name of the tool to execute
36
+ args: Arguments to pass to the tool
37
+
38
+ Returns:
39
+ Tool execution result
40
+
41
+ Raises:
42
+ ValueError: If tool is not found
43
+ """
44
+ if name not in TOOL_REGISTRY:
45
+ available_tools = list(TOOL_REGISTRY.keys())
46
+ logger.error(
47
+ f"❌ Tool '{name}' not found. " f"Available tools: {available_tools}"
48
+ )
49
+ raise ValueError(f"Tool '{name}' not found. Available tools: {available_tools}")
50
+
51
+ logger.debug(f"🛠️ Executing tool '{name}' with args: {args}")
24
52
 
25
- tool_func = TOOL_REGISTRY[tool_id]
26
- logger.debug(f"🛠️ Executing tool '{tool_id}' with args: {args}")
27
-
28
- import time
29
- start_time = time.time()
30
-
31
53
  try:
32
- result = tool_func(**args)
33
- execution_time = time.time() - start_time
34
- logger.info(f"✅ Tool '{tool_id}' executed successfully in {execution_time:.3f}s")
35
- logger.debug(f"🔧 Tool result: {result}")
54
+ result = TOOL_REGISTRY[name](args)
55
+ logger.debug(f"✅ Tool '{name}' completed successfully")
36
56
  return result
37
57
  except Exception as e:
38
- execution_time = time.time() - start_time
39
- logger.error(f"❌ Tool '{tool_id}' failed after {execution_time:.3f}s: {type(e).__name__}: {e}")
58
+ logger.error(f"❌ Tool '{name}' failed: {type(e).__name__}: {e}")
40
59
  raise
41
60
 
42
61
 
43
- def file_writer(path: str, content: str) -> Dict[str, Any]:
62
+ def file_writer(args: Dict[str, Any]) -> Dict[str, Any]:
44
63
  """
45
- Write content to a file, creating parent directories if they don't exist.
64
+ Write content to a file, creating directories as needed.
46
65
 
47
66
  Args:
48
- path: File path to write to
49
- content: Content to write to the file
67
+ args: Dictionary containing 'path' and 'content'
50
68
 
51
69
  Returns:
52
- Dict with success status and file path
70
+ Success status and file information
53
71
  """
54
- logger.debug(f"📝 Writing to file: {path} ({len(content)} characters)")
55
-
72
+ path = args.get("path")
73
+ content = args.get("content", "")
74
+
75
+ if not path:
76
+ raise ValueError("file_writer requires 'path' argument")
77
+
56
78
  try:
57
79
  # Create parent directories if they don't exist
58
- parent_dir = Path(path).parent
59
- if not parent_dir.exists():
60
- logger.debug(f"📁 Creating parent directories: {parent_dir}")
61
- parent_dir.mkdir(parents=True, exist_ok=True)
80
+ file_path = Path(path)
81
+ file_path.parent.mkdir(parents=True, exist_ok=True)
62
82
 
63
- # Write the content to the file
64
- with open(path, "w", encoding="utf-8") as f:
83
+ # Write content to file
84
+ with open(file_path, "w", encoding="utf-8") as f:
65
85
  f.write(content)
66
86
 
67
87
  logger.info(f"✅ File written successfully: {path}")
88
+
68
89
  return {
69
90
  "success": True,
70
- "path": path,
91
+ "path": str(file_path),
71
92
  "message": f"Successfully wrote {len(content)} characters to {path}",
72
93
  }
94
+
73
95
  except Exception as e:
74
- logger.error(f"❌ Failed to write file {path}: {type(e).__name__}: {e}")
96
+ logger.error(f"❌ Failed to write file {path}: {e}")
75
97
  return {
76
98
  "success": False,
77
99
  "path": path,
78
100
  "error": str(e),
79
- "message": f"Failed to write to {path}: {e}",
80
101
  }
81
102
 
82
103
 
83
- # Register the built-in file_writer tool
84
- logger.debug("🏗️ Registering built-in tools...")
104
+ # Register built-in tools
85
105
  register_tool("file_writer", file_writer)
86
- logger.debug("✅ Built-in tools registration complete")