dacp 0.1.0__py3-none-any.whl → 0.3.1__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 ADDED
@@ -0,0 +1,248 @@
1
+ """
2
+ DACP Orchestrator - Manages agent registration and message routing.
3
+ """
4
+
5
+ import logging
6
+ import time
7
+ 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
19
+ logger = logging.getLogger("dacp.orchestrator")
20
+
21
+
22
+ class Agent:
23
+ """Base class for DACP agents."""
24
+
25
+ def handle_message(self, message: Dict[str, Any]) -> Dict[str, Any]:
26
+ """Handle an incoming message. Subclasses should override this method."""
27
+ raise NotImplementedError("Agents must implement handle_message method")
28
+
29
+
30
+ class Orchestrator:
31
+ """
32
+ Central orchestrator for managing agents and routing messages.
33
+ """
34
+
35
+ def __init__(self):
36
+ """Initialize the orchestrator."""
37
+ self.agents: Dict[str, Any] = {}
38
+ self.conversation_history: List[Dict[str, Any]] = []
39
+ self.session_id = f"session_{int(time.time())}"
40
+ logger.info(f"🎭 Orchestrator initialized with session ID: {self.session_id}")
41
+
42
+ def register_agent(self, agent_id: str, agent: Any) -> None:
43
+ """
44
+ Register an agent with the orchestrator.
45
+
46
+ Args:
47
+ agent_id: Unique identifier for the agent
48
+ agent: Agent instance that implements handle_message method
49
+ """
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__})")
56
+ logger.debug(f"📊 Total registered agents: {len(self.agents)}")
57
+
58
+ def unregister_agent(self, agent_id: str) -> bool:
59
+ """
60
+ Unregister an agent from the orchestrator.
61
+
62
+ Args:
63
+ agent_id: Unique identifier for the agent
64
+
65
+ Returns:
66
+ True if agent was successfully unregistered, False if not found
67
+ """
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)}")
73
+ return True
74
+ else:
75
+ logger.warning(f"⚠️ Attempted to unregister unknown agent: '{agent_id}'")
76
+ return False
77
+
78
+ def send_message(self, agent_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
79
+ """
80
+ Send a message to a specific agent.
81
+
82
+ Args:
83
+ agent_id: Target agent identifier
84
+ message: Message to send
85
+
86
+ Returns:
87
+ Response from the agent (or error if agent not found)
88
+ """
89
+ logger.info(f"📨 Sending message to agent '{agent_id}'")
90
+ logger.debug(f"📋 Message content: {message}")
91
+
92
+ if agent_id not in self.agents:
93
+ error_msg = f"Agent '{agent_id}' not found"
94
+ logger.error(f"❌ {error_msg}")
95
+ logger.debug(f"📊 Available agents: {list(self.agents.keys())}")
96
+ return {"error": error_msg}
97
+
98
+ agent = self.agents[agent_id]
99
+
100
+ try:
101
+ start_time = time.time()
102
+ logger.debug(f"🔄 Calling handle_message on agent '{agent_id}'")
103
+
104
+ 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
138
+ 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)
148
+ return response
149
+
150
+ except Exception as e:
151
+ error_msg = f"Agent '{agent_id}' error: {str(e)}"
152
+ logger.error(f"❌ {error_msg}")
153
+ logger.debug(f"🐛 Exception details: {type(e).__name__}: {e}")
154
+ error_response = {"error": error_msg}
155
+ self._log_conversation(agent_id, message, error_response)
156
+ return error_response
157
+
158
+ def broadcast_message(self, message: Dict[str, Any], exclude_agents: Optional[List[str]] = None) -> Dict[str, Dict[str, Any]]:
159
+ """
160
+ Send a message to all registered agents (optionally excluding some).
161
+
162
+ Args:
163
+ message: Message to broadcast
164
+ exclude_agents: List of agent IDs to exclude from broadcast
165
+
166
+ Returns:
167
+ Dict mapping agent IDs to their responses
168
+ """
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
+
177
+ responses = {}
178
+ 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
+
187
+ return responses
188
+
189
+ def get_conversation_history(self, agent_id: Optional[str] = None) -> List[Dict[str, Any]]:
190
+ """
191
+ Get conversation history, optionally filtered by agent.
192
+
193
+ 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
+
220
+ Returns:
221
+ Dict containing session metadata
222
+ """
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."""
234
+ entry = {
235
+ "timestamp": time.time(),
236
+ "agent_id": agent_id,
237
+ "message": message,
238
+ "response": response,
239
+ "session_id": self.session_id
240
+ }
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
+
248
+ self.conversation_history.append(entry)
dacp/tools.py CHANGED
@@ -1,4 +1,9 @@
1
+ import logging
1
2
  from typing import Dict, Any, Callable
3
+ from pathlib import Path
4
+
5
+ # Set up logger for this module
6
+ logger = logging.getLogger("dacp.tools")
2
7
 
3
8
  TOOL_REGISTRY: Dict[str, Callable[..., Dict[str, Any]]] = {}
4
9
 
@@ -6,25 +11,76 @@ TOOL_REGISTRY: Dict[str, Callable[..., Dict[str, Any]]] = {}
6
11
  def register_tool(tool_id: str, func: Callable[..., Dict[str, Any]]) -> None:
7
12
  """Register a tool function."""
8
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)}")
9
16
 
10
17
 
11
18
  def run_tool(tool_id: str, args: Dict[str, Any]) -> Dict[str, Any]:
12
19
  """Run a registered tool with the given arguments."""
13
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())}")
14
23
  raise ValueError(f"Unknown tool: {tool_id}")
24
+
15
25
  tool_func = TOOL_REGISTRY[tool_id]
16
- return tool_func(**args)
26
+ logger.debug(f"🛠️ Executing tool '{tool_id}' with args: {args}")
27
+
28
+ import time
29
+ start_time = time.time()
30
+
31
+ 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}")
36
+ return result
37
+ 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}")
40
+ raise
41
+
42
+
43
+ def file_writer(path: str, content: str) -> Dict[str, Any]:
44
+ """
45
+ Write content to a file, creating parent directories if they don't exist.
46
+
47
+ Args:
48
+ path: File path to write to
49
+ content: Content to write to the file
50
+
51
+ Returns:
52
+ Dict with success status and file path
53
+ """
54
+ logger.debug(f"📝 Writing to file: {path} ({len(content)} characters)")
55
+
56
+ try:
57
+ # 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)
17
62
 
63
+ # Write the content to the file
64
+ with open(path, "w", encoding="utf-8") as f:
65
+ f.write(content)
18
66
 
19
- # --- Example tool ---
20
- def file_writer(path: str, content: str) -> dict:
21
- # Only allow paths in /tmp or ./output for now!
22
- allowed_prefixes = ["./output/", "/tmp/"]
23
- if not any(path.startswith(prefix) for prefix in allowed_prefixes):
24
- raise ValueError("Path not allowed")
25
- with open(path, "w") as f:
26
- f.write(content)
27
- return {"result": f"Written to {path}"}
67
+ logger.info(f"✅ File written successfully: {path}")
68
+ return {
69
+ "success": True,
70
+ "path": path,
71
+ "message": f"Successfully wrote {len(content)} characters to {path}",
72
+ }
73
+ except Exception as e:
74
+ logger.error(f"❌ Failed to write file {path}: {type(e).__name__}: {e}")
75
+ return {
76
+ "success": False,
77
+ "path": path,
78
+ "error": str(e),
79
+ "message": f"Failed to write to {path}: {e}",
80
+ }
28
81
 
29
82
 
83
+ # Register the built-in file_writer tool
84
+ logger.debug("🏗️ Registering built-in tools...")
30
85
  register_tool("file_writer", file_writer)
86
+ logger.debug("✅ Built-in tools registration complete")