dacp 0.3.0__py3-none-any.whl → 0.3.2__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/logging_config.py ADDED
@@ -0,0 +1,129 @@
1
+ """
2
+ DACP Logging Configuration
3
+
4
+ Utilities for configuring logging for DACP components.
5
+ """
6
+
7
+ import logging
8
+ import sys
9
+ from typing import Optional
10
+
11
+
12
+ def setup_dacp_logging(
13
+ level: str = "INFO",
14
+ format_style: str = "detailed",
15
+ include_timestamp: bool = True,
16
+ log_file: Optional[str] = None,
17
+ ) -> None:
18
+ """
19
+ Set up logging for DACP components.
20
+
21
+ Args:
22
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
23
+ format_style: Log format style ('simple', 'detailed', 'emoji')
24
+ include_timestamp: Whether to include timestamps in logs
25
+ log_file: Optional file path to also log to a file
26
+ """
27
+ # Define format styles
28
+ if format_style == "simple":
29
+ if include_timestamp:
30
+ log_format = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
31
+ else:
32
+ log_format = "%(name)s - %(levelname)s - %(message)s"
33
+ elif format_style == "detailed":
34
+ if include_timestamp:
35
+ log_format = (
36
+ "%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s"
37
+ )
38
+ else:
39
+ log_format = "%(name)s:%(lineno)d - %(levelname)s - %(message)s"
40
+ elif format_style == "emoji":
41
+ # Emoji format doesn't include logger name since emojis provide context
42
+ if include_timestamp:
43
+ log_format = "%(asctime)s - %(message)s"
44
+ else:
45
+ log_format = "%(message)s"
46
+ else:
47
+ raise ValueError(f"Unknown format_style: {format_style}")
48
+
49
+ # Configure root logger for DACP components
50
+ logger = logging.getLogger("dacp")
51
+ logger.setLevel(getattr(logging, level.upper()))
52
+
53
+ # Remove existing handlers to avoid duplicates
54
+ for handler in logger.handlers[:]:
55
+ logger.removeHandler(handler)
56
+
57
+ # Create formatter
58
+ formatter = logging.Formatter(
59
+ log_format, datefmt="%Y-%m-%d %H:%M:%S" if include_timestamp else None
60
+ )
61
+
62
+ # Console handler
63
+ console_handler = logging.StreamHandler(sys.stdout)
64
+ console_handler.setFormatter(formatter)
65
+ logger.addHandler(console_handler)
66
+
67
+ # Optional file handler
68
+ if log_file:
69
+ file_handler = logging.FileHandler(log_file)
70
+ file_handler.setFormatter(formatter)
71
+ logger.addHandler(file_handler)
72
+
73
+ # Prevent propagation to root logger to avoid duplicate messages
74
+ logger.propagate = False
75
+
76
+ logger.info(f"🚀 DACP logging configured: level={level}, style={format_style}")
77
+
78
+
79
+ def set_dacp_log_level(level: str) -> None:
80
+ """
81
+ Set the log level for all DACP components.
82
+
83
+ Args:
84
+ level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
85
+ """
86
+ logger = logging.getLogger("dacp")
87
+ logger.setLevel(getattr(logging, level.upper()))
88
+ logger.info(f"📊 DACP log level changed to {level}")
89
+
90
+
91
+ def disable_dacp_logging() -> None:
92
+ """Disable all DACP logging."""
93
+ logger = logging.getLogger("dacp")
94
+ logger.disabled = True
95
+
96
+
97
+ def enable_dacp_logging() -> None:
98
+ """Re-enable DACP logging."""
99
+ logger = logging.getLogger("dacp")
100
+ logger.disabled = False
101
+
102
+
103
+ def get_dacp_logger(name: str) -> logging.Logger:
104
+ """
105
+ Get a logger for a DACP component.
106
+
107
+ Args:
108
+ name: Logger name (usually __name__)
109
+
110
+ Returns:
111
+ Configured logger
112
+ """
113
+ return logging.getLogger(f"dacp.{name}")
114
+
115
+
116
+ # Convenience functions for quick setup
117
+ def enable_debug_logging(log_file: Optional[str] = None) -> None:
118
+ """Enable debug logging with detailed format."""
119
+ setup_dacp_logging(level="DEBUG", format_style="detailed", log_file=log_file)
120
+
121
+
122
+ def enable_info_logging(log_file: Optional[str] = None) -> None:
123
+ """Enable info logging with emoji format."""
124
+ setup_dacp_logging(level="INFO", format_style="emoji", log_file=log_file)
125
+
126
+
127
+ def enable_quiet_logging() -> None:
128
+ """Enable only error and critical logging."""
129
+ setup_dacp_logging(level="ERROR", format_style="simple", include_timestamp=False)
dacp/main.py CHANGED
@@ -1,15 +1,9 @@
1
- from dacp.orchestrator import Orchestrator
1
+ #!/usr/bin/env python3
2
+ """
3
+ DACP Main Entry Point
2
4
 
3
- def main():
4
- orchestrator = Orchestrator()
5
+ This module provides examples and testing functionality for DACP.
6
+ """
5
7
 
6
- # Agent registers itself with the orchestrator
7
- hello_agent = HelloWorldAgent("hello_agent", orchestrator)
8
-
9
- # Orchestrator sends a message to the agent and prints the response
10
- input_message = {"name": "Alice"}
11
- response = orchestrator.call_agent("hello_agent", input_message)
12
- print("Orchestrator received:", response)
13
-
14
- if __name__ == "__main__":
15
- main()
8
+ print("DACP - Declarative Agent Communication Protocol")
9
+ print("For examples, see the examples/ directory")
dacp/orchestrator.py CHANGED
@@ -1,260 +1,284 @@
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
- from typing import Dict, Any, Optional, List, Callable
7
- import uuid
8
- import json
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
- from .tools import run_tool
18
-
19
- log = logging.getLogger(__name__)
9
+ import time
10
+ from typing import Dict, Any, List, Optional
11
+
12
+ from .tools import execute_tool
13
+
14
+ logger = logging.getLogger("dacp.orchestrator")
20
15
 
21
16
 
22
17
  class Agent:
23
- """Base agent interface that all agents should implement."""
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 incoming messages from the orchestrator."""
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.
33
- Handles agent registration, message routing, and tool execution.
40
+
41
+ The orchestrator handles agent registration, message routing, tool execution,
42
+ and conversation history management.
34
43
  """
35
-
36
- def __init__(self):
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 = str(uuid.uuid4())
40
-
41
- def register_agent(self, agent_id: str, agent: Any) -> None:
50
+
51
+ logger.info(f"🎭 Orchestrator initialized with session ID: {self.session_id}")
52
+
53
+ def register_agent(self, name: str, agent: Agent) -> None:
42
54
  """
43
55
  Register an agent with the orchestrator.
44
-
56
+
45
57
  Args:
46
- agent_id: Unique identifier for the agent
47
- agent: Agent instance that implements handle_message method
58
+ name: Unique name for the agent
59
+ agent: Agent instance implementing the Agent interface
48
60
  """
49
- if not hasattr(agent, 'handle_message'):
50
- raise ValueError(f"Agent {agent_id} must implement handle_message method")
51
-
52
- self.agents[agent_id] = agent
53
- log.info(f"Registered agent: {agent_id}")
54
-
55
- def unregister_agent(self, agent_id: str) -> bool:
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
+ )
69
+ logger.debug(f"📊 Total registered agents: {len(self.agents)}")
70
+
71
+ def unregister_agent(self, name: str) -> bool:
56
72
  """
57
73
  Unregister an agent from the orchestrator.
58
-
74
+
59
75
  Args:
60
- agent_id: ID of the agent to unregister
61
-
76
+ name: Name of the agent to unregister
77
+
62
78
  Returns:
63
- True if agent was found and removed, False otherwise
79
+ True if agent was unregistered, False if not found
64
80
  """
65
- if agent_id in self.agents:
66
- del self.agents[agent_id]
67
- log.info(f"Unregistered agent: {agent_id}")
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)}")
68
85
  return True
69
- return False
70
-
71
- def get_agent(self, agent_id: str) -> Optional[Any]:
72
- """Get an agent by ID."""
73
- return self.agents.get(agent_id)
74
-
86
+ else:
87
+ logger.warning(f"⚠️ Agent '{name}' not found for unregistration")
88
+ return False
89
+
75
90
  def list_agents(self) -> List[str]:
76
- """Get list of registered agent IDs."""
91
+ """Get list of registered agent names."""
77
92
  return list(self.agents.keys())
78
-
79
- def send_message(self, agent_id: str, message: Dict[str, Any]) -> Dict[str, Any]:
93
+
94
+ def send_message(self, agent_name: str, message: Dict[str, Any]) -> Dict[str, Any]:
80
95
  """
81
96
  Send a message to a specific agent.
82
-
97
+
83
98
  Args:
84
- agent_id: ID of the target agent
85
- message: Message to send
86
-
99
+ agent_name: Name of the target agent
100
+ message: Message dictionary to send
101
+
87
102
  Returns:
88
- Response from the agent
89
-
90
- Raises:
91
- ValueError: If agent_id is not found
103
+ Response from the agent after processing
92
104
  """
93
- if agent_id not in self.agents:
94
- raise ValueError(f"Agent {agent_id} not found")
95
-
96
- agent = self.agents[agent_id]
97
-
98
- # Add metadata to message
99
- enriched_message = {
100
- "session_id": self.session_id,
101
- "timestamp": self._get_timestamp(),
102
- **message
103
- }
104
-
105
+ start_time = time.time()
106
+
107
+ logger.info(f"📨 Sending message to agent '{agent_name}'")
108
+ logger.debug(f"📋 Message content: {message}")
109
+
110
+ if agent_name not in self.agents:
111
+ error_msg = f"Agent '{agent_name}' not found"
112
+ logger.error(f"❌ {error_msg}")
113
+ return {"error": error_msg}
114
+
115
+ agent = self.agents[agent_name]
116
+
105
117
  try:
106
- # Send message to agent
107
- response = agent.handle_message(enriched_message)
108
-
109
- # Log the interaction
110
- self.conversation_history.append({
111
- "type": "message",
112
- "agent_id": agent_id,
113
- "message": enriched_message,
114
- "response": response,
115
- "timestamp": self._get_timestamp()
116
- })
117
-
118
+ logger.debug(f"🔄 Calling handle_message on agent '{agent_name}'")
119
+
120
+ # Call the agent's message handler
121
+ response = agent.handle_message(message)
122
+
123
+ duration = time.time() - start_time
124
+ logger.info(f"✅ Agent '{agent_name}' responded in {duration:.3f}s")
125
+ logger.debug(f"📤 Agent response: {response}")
126
+
127
+ # Check if agent requested tool execution
128
+ if isinstance(response, dict) and "tool_request" in response:
129
+ logger.info(f"🔧 Agent '{agent_name}' requested tool execution")
130
+ response = self._handle_tool_request(
131
+ agent_name, response["tool_request"]
132
+ )
133
+
134
+ # Log the conversation
135
+ self._log_conversation(agent_name, message, response, duration)
136
+
118
137
  return response
119
-
138
+
120
139
  except Exception as e:
121
- error_response = {
122
- "error": f"Agent {agent_id} failed to handle message: {str(e)}",
123
- "agent_id": agent_id
124
- }
125
- log.error(f"Error sending message to agent {agent_id}: {e}")
140
+ duration = time.time() - start_time
141
+ error_msg = f"Error in agent '{agent_name}': {type(e).__name__}: {e}"
142
+ logger.error(f"❌ {error_msg}")
143
+ logger.debug("💥 Exception details", exc_info=True)
144
+
145
+ error_response = {"error": error_msg}
146
+ self._log_conversation(agent_name, message, error_response, duration)
147
+
126
148
  return error_response
127
-
128
- def broadcast_message(self, message: Dict[str, Any], exclude_agents: List[str] = None) -> Dict[str, Dict[str, Any]]:
149
+
150
+ def broadcast_message(self, message: Dict[str, Any]) -> Dict[str, Any]:
129
151
  """
130
- Broadcast a message to all registered agents.
131
-
152
+ Send a message to all registered agents.
153
+
132
154
  Args:
133
- message: Message to broadcast
134
- exclude_agents: List of agent IDs to exclude from broadcast
135
-
155
+ message: Message dictionary to broadcast
156
+
136
157
  Returns:
137
- Dictionary mapping agent_id to response
158
+ Dictionary mapping agent names to their responses
138
159
  """
139
- exclude_agents = exclude_agents or []
160
+ logger.info(f"📢 Broadcasting message to {len(self.agents)} agents")
161
+ logger.debug(f"📋 Broadcast message: {message}")
162
+
140
163
  responses = {}
141
-
142
- for agent_id in self.agents:
143
- if agent_id not in exclude_agents:
144
- try:
145
- responses[agent_id] = self.send_message(agent_id, message)
146
- except Exception as e:
147
- responses[agent_id] = {"error": str(e)}
148
-
164
+ start_time = time.time()
165
+
166
+ for agent_name in self.agents:
167
+ logger.debug(f"📨 Broadcasting to agent '{agent_name}'")
168
+ responses[agent_name] = self.send_message(agent_name, message)
169
+
170
+ duration = time.time() - start_time
171
+ logger.info(
172
+ f"✅ Broadcast completed in {duration:.3f}s "
173
+ f"({len(responses)} responses)"
174
+ )
175
+
149
176
  return responses
150
-
151
- def handle_tool_request(self, tool_name: str, args: Dict[str, Any]) -> Dict[str, Any]:
152
- """
153
- Handle a tool execution request.
154
-
155
- Args:
156
- tool_name: Name of the tool to execute
157
- args: Arguments for the tool
158
-
159
- Returns:
160
- Tool execution result wrapped in protocol format
161
- """
162
- try:
163
- result = run_tool(tool_name, args)
164
- return wrap_tool_result(tool_name, result)
165
- except Exception as e:
166
- error_result = {
167
- "success": False,
168
- "error": str(e),
169
- "tool_name": tool_name,
170
- "args": args
171
- }
172
- return wrap_tool_result(tool_name, error_result)
173
-
174
- def process_agent_response(self, agent_id: str, response: Any) -> Dict[str, Any]:
177
+
178
+ def _handle_tool_request(
179
+ self, agent_name: str, tool_request: Dict[str, Any]
180
+ ) -> Dict[str, Any]:
175
181
  """
176
- Process an agent response, handling tool requests and final responses.
177
-
182
+ Handle tool execution request from an agent.
183
+
178
184
  Args:
179
- agent_id: ID of the agent that sent the response
180
- response: Agent response (string or dict)
181
-
185
+ agent_name: Name of the requesting agent
186
+ tool_request: Tool request dictionary with 'name' and 'args'
187
+
182
188
  Returns:
183
- Processed response
189
+ Tool execution result
184
190
  """
191
+ tool_name = tool_request.get("name")
192
+ tool_args = tool_request.get("args", {})
193
+
194
+ if not tool_name:
195
+ return {"error": "Tool name is required"}
196
+
197
+ logger.info(f"🛠️ Executing tool: '{tool_name}' with args: {tool_args}")
198
+
199
+ start_time = time.time()
200
+
185
201
  try:
186
- # Parse the agent response
187
- parsed_response = parse_agent_response(response)
188
-
189
- # Check if it's a tool request
190
- if is_tool_request(parsed_response):
191
- tool_name, args = get_tool_request(parsed_response)
192
- log.info(f"Agent {agent_id} requested tool: {tool_name}")
193
-
194
- # Execute the tool
195
- tool_result = self.handle_tool_request(tool_name, args)
196
-
197
- # Log tool execution
198
- self.conversation_history.append({
199
- "type": "tool_execution",
200
- "agent_id": agent_id,
201
- "tool_name": tool_name,
202
- "args": args,
203
- "result": tool_result,
204
- "timestamp": self._get_timestamp()
205
- })
206
-
207
- return tool_result
208
-
209
- # Check if it's a final response
210
- elif is_final_response(parsed_response):
211
- final_response = get_final_response(parsed_response)
212
- log.info(f"Agent {agent_id} sent final response")
213
- return final_response
214
-
215
- else:
216
- # Return the parsed response as-is
217
- return parsed_response
218
-
202
+ result = execute_tool(tool_name, tool_args)
203
+ duration = time.time() - start_time
204
+
205
+ logger.info(
206
+ f"✅ Tool '{tool_name}' executed successfully in {duration:.3f}s"
207
+ )
208
+ logger.debug(f"🔧 Tool result: {result}")
209
+
210
+ return {"tool_result": {"name": tool_name, "result": result}}
211
+
219
212
  except Exception as e:
220
- log.error(f"Error processing agent response from {agent_id}: {e}")
221
- return {
222
- "error": f"Failed to process response: {str(e)}",
223
- "original_response": response
224
- }
225
-
226
- def get_conversation_history(self, agent_id: str = None) -> List[Dict[str, Any]]:
213
+ duration = time.time() - start_time
214
+ error_msg = f"Tool '{tool_name}' failed: {type(e).__name__}: {e}"
215
+ logger.error(f" {error_msg}")
216
+
217
+ return {"error": error_msg}
218
+
219
+ def _log_conversation(
220
+ self,
221
+ agent_name: str,
222
+ message: Dict[str, Any],
223
+ response: Dict[str, Any],
224
+ duration: float,
225
+ ) -> None:
226
+ """Log conversation entry to history."""
227
+ logger.debug("💾 Logging conversation entry")
228
+
229
+ entry = {
230
+ "timestamp": time.time(),
231
+ "session_id": self.session_id,
232
+ "agent_name": agent_name,
233
+ "message": message,
234
+ "response": response,
235
+ "duration": duration,
236
+ }
237
+
238
+ self.conversation_history.append(entry)
239
+
240
+ # Keep history manageable (last 1000 entries)
241
+ if len(self.conversation_history) > 1000:
242
+ self.conversation_history = self.conversation_history[-1000:]
243
+ logger.debug("🗂️ Conversation history trimmed to 1000 entries")
244
+
245
+ def get_conversation_history(
246
+ self, limit: Optional[int] = None
247
+ ) -> List[Dict[str, Any]]:
227
248
  """
228
- Get conversation history, optionally filtered by agent.
229
-
249
+ Get conversation history.
250
+
230
251
  Args:
231
- agent_id: Optional agent ID to filter by
232
-
252
+ limit: Maximum number of entries to return (None for all)
253
+
233
254
  Returns:
234
255
  List of conversation entries
235
256
  """
236
- if agent_id:
237
- return [
238
- entry for entry in self.conversation_history
239
- if entry.get("agent_id") == agent_id
240
- ]
241
- return self.conversation_history.copy()
242
-
243
- def clear_history(self) -> None:
257
+ if limit is None:
258
+ return self.conversation_history.copy()
259
+ else:
260
+ return self.conversation_history[-limit:].copy()
261
+
262
+ def clear_conversation_history(self) -> None:
244
263
  """Clear conversation history."""
245
264
  self.conversation_history.clear()
246
- log.info("Conversation history cleared")
247
-
248
- def get_session_info(self) -> Dict[str, Any]:
249
- """Get current session information."""
265
+ logger.info("🗑️ Conversation history cleared")
266
+
267
+ def get_session_metadata(self) -> Dict[str, Any]:
268
+ """Get session metadata and statistics."""
250
269
  return {
251
270
  "session_id": self.session_id,
252
- "registered_agents": self.list_agents(),
253
- "conversation_length": len(self.conversation_history),
254
- "timestamp": self._get_timestamp()
271
+ "registered_agents": len(self.agents),
272
+ "agent_names": list(self.agents.keys()),
273
+ "conversation_entries": len(self.conversation_history),
274
+ "start_time": (
275
+ self.conversation_history[0]["timestamp"]
276
+ if self.conversation_history
277
+ else None
278
+ ),
279
+ "last_activity": (
280
+ self.conversation_history[-1]["timestamp"]
281
+ if self.conversation_history
282
+ else None
283
+ ),
255
284
  }
256
-
257
- def _get_timestamp(self) -> str:
258
- """Get current timestamp in ISO format."""
259
- from datetime import datetime
260
- return datetime.utcnow().isoformat() + "Z"