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/json_parser.py ADDED
@@ -0,0 +1,232 @@
1
+ """
2
+ DACP JSON Parser - Robust JSON parsing for agent responses.
3
+
4
+ This module provides enhanced JSON parsing capabilities that can handle
5
+ various LLM response formats and provide intelligent fallbacks.
6
+ """
7
+
8
+ import json
9
+ import re
10
+ import logging
11
+ from typing import Dict, Any, Optional, Union
12
+ from pydantic import BaseModel
13
+
14
+ logger = logging.getLogger("dacp.json_parser")
15
+
16
+
17
+ def extract_json_from_text(text: str) -> Optional[Dict[str, Any]]:
18
+ """
19
+ Extract JSON from text using multiple strategies.
20
+
21
+ Args:
22
+ text: Raw text that might contain JSON
23
+
24
+ Returns:
25
+ Parsed JSON dict or None if no valid JSON found
26
+ """
27
+ if not isinstance(text, str):
28
+ return None
29
+
30
+ logger.debug(f"🔍 Attempting to extract JSON from text: {text[:100]}...")
31
+
32
+ # Strategy 1: Try parsing the entire text as JSON
33
+ try:
34
+ result = json.loads(text.strip())
35
+ logger.debug("✅ Successfully parsed entire text as JSON")
36
+ return result
37
+ except json.JSONDecodeError:
38
+ logger.debug("❌ Failed to parse entire text as JSON")
39
+
40
+ # Strategy 2: Find JSON between braces
41
+ json_start = text.find('{')
42
+ json_end = text.rfind('}') + 1
43
+ if json_start >= 0 and json_end > json_start:
44
+ json_str = text[json_start:json_end]
45
+ try:
46
+ result = json.loads(json_str)
47
+ logger.debug("✅ Successfully extracted JSON between braces")
48
+ return result
49
+ except json.JSONDecodeError:
50
+ logger.debug("❌ Failed to parse JSON between braces")
51
+
52
+ # Strategy 3: Find JSON in code blocks
53
+ code_block_pattern = r'```(?:json)?\s*(\{.*?\})\s*```'
54
+ matches = re.findall(code_block_pattern, text, re.DOTALL)
55
+ for match in matches:
56
+ try:
57
+ result = json.loads(match)
58
+ logger.debug("✅ Successfully extracted JSON from code block")
59
+ return result
60
+ except json.JSONDecodeError:
61
+ continue
62
+
63
+ # Strategy 4: Find JSON after common prefixes
64
+ prefixes = [
65
+ "json response:",
66
+ "response:",
67
+ "output:",
68
+ "result:",
69
+ "here is the json:",
70
+ "the json is:",
71
+ ]
72
+
73
+ for prefix in prefixes:
74
+ prefix_pos = text.lower().find(prefix.lower())
75
+ if prefix_pos >= 0:
76
+ remaining_text = text[prefix_pos + len(prefix):].strip()
77
+ extracted = extract_json_from_text(remaining_text)
78
+ if extracted:
79
+ logger.debug(f"✅ Successfully extracted JSON after prefix: {prefix}")
80
+ return extracted
81
+
82
+ logger.debug("❌ No valid JSON found in text")
83
+ return None
84
+
85
+
86
+ def create_fallback_response(
87
+ text: str,
88
+ required_fields: Dict[str, Any],
89
+ optional_fields: Dict[str, Any] = None
90
+ ) -> Dict[str, Any]:
91
+ """
92
+ Create a fallback response when JSON parsing fails.
93
+
94
+ Args:
95
+ text: Original LLM response text
96
+ required_fields: Dictionary of required field names and default values
97
+ optional_fields: Dictionary of optional field names and default values
98
+
99
+ Returns:
100
+ Dictionary with required fields filled
101
+ """
102
+ logger.info(f"🔄 Creating fallback response for text: {text[:50]}...")
103
+
104
+ fallback = {}
105
+
106
+ # Fill required fields with defaults or extracted content
107
+ for field_name, default_value in required_fields.items():
108
+ if field_name in ["message", "response_message", "greeting_message"]:
109
+ # Use the original text as the message
110
+ fallback[field_name] = text.strip()
111
+ logger.debug(f"📝 Using text as {field_name}")
112
+ elif field_name in ["agent", "sender_agent", "target_agent"]:
113
+ # Try to extract agent names or use default
114
+ agent_match = re.search(r'agent[:\s]+([a-zA-Z0-9_-]+)', text, re.IGNORECASE)
115
+ if agent_match:
116
+ fallback[field_name] = agent_match.group(1)
117
+ logger.debug(f"🎯 Extracted agent name: {agent_match.group(1)}")
118
+ else:
119
+ fallback[field_name] = default_value or "unknown"
120
+ logger.debug(f"🔧 Using default for {field_name}: {fallback[field_name]}")
121
+ else:
122
+ fallback[field_name] = default_value
123
+ logger.debug(f"⚙️ Setting {field_name} to default: {default_value}")
124
+
125
+ # Fill optional fields if provided
126
+ if optional_fields:
127
+ for field_name, default_value in optional_fields.items():
128
+ fallback[field_name] = default_value
129
+ logger.debug(f"📋 Adding optional field {field_name}: {default_value}")
130
+
131
+ logger.info(f"✅ Created fallback response with {len(fallback)} fields")
132
+ return fallback
133
+
134
+
135
+ def robust_json_parse(
136
+ response: Union[str, dict, BaseModel],
137
+ target_model: type,
138
+ required_fields: Dict[str, Any],
139
+ optional_fields: Dict[str, Any] = None
140
+ ) -> BaseModel:
141
+ """
142
+ Robust JSON parsing with intelligent fallbacks.
143
+
144
+ Args:
145
+ response: LLM response (string, dict, or Pydantic model)
146
+ target_model: Pydantic model class to create
147
+ required_fields: Required fields with default values
148
+ optional_fields: Optional fields with default values
149
+
150
+ Returns:
151
+ Instance of target_model
152
+
153
+ Raises:
154
+ ValueError: If parsing fails completely
155
+ """
156
+ logger.debug(f"🔧 Parsing response of type {type(response).__name__} into {target_model.__name__}")
157
+
158
+ # If already the target model, return as-is
159
+ if isinstance(response, target_model):
160
+ logger.debug("✅ Response is already target model")
161
+ return response
162
+
163
+ # If dict, try to create model directly
164
+ if isinstance(response, dict):
165
+ try:
166
+ result = target_model(**response)
167
+ logger.debug("✅ Successfully created model from dict")
168
+ return result
169
+ except Exception as e:
170
+ logger.debug(f"❌ Failed to create model from dict: {e}")
171
+
172
+ # If string, try JSON extraction
173
+ if isinstance(response, str):
174
+ extracted_json = extract_json_from_text(response)
175
+
176
+ if extracted_json:
177
+ try:
178
+ result = target_model(**extracted_json)
179
+ logger.debug("✅ Successfully created model from extracted JSON")
180
+ return result
181
+ except Exception as e:
182
+ logger.debug(f"❌ Failed to create model from extracted JSON: {e}")
183
+
184
+ # Create fallback response
185
+ logger.info("🔄 Creating fallback response for string input")
186
+ fallback_data = create_fallback_response(
187
+ response,
188
+ required_fields,
189
+ optional_fields
190
+ )
191
+
192
+ try:
193
+ result = target_model(**fallback_data)
194
+ logger.info("✅ Successfully created model from fallback data")
195
+ return result
196
+ except Exception as e:
197
+ logger.error(f"❌ Failed to create fallback response: {e}")
198
+ raise ValueError(f"Failed to create fallback response: {e}")
199
+
200
+ # Unexpected response type
201
+ error_msg = f"Unable to parse response of type {type(response)}: {response}"
202
+ logger.error(f"❌ {error_msg}")
203
+ raise ValueError(error_msg)
204
+
205
+
206
+ def parse_with_fallback(response: Any, model_class: type, **field_defaults) -> BaseModel:
207
+ """
208
+ Convenience function for parsing with automatic field detection.
209
+
210
+ Args:
211
+ response: LLM response to parse
212
+ model_class: Pydantic model class
213
+ **field_defaults: Default values for fields (field_name=default_value)
214
+
215
+ Returns:
216
+ Instance of model_class
217
+ """
218
+ # Extract required fields from model
219
+ required_fields = {}
220
+ optional_fields = {}
221
+
222
+ # Get field info from Pydantic model
223
+ if hasattr(model_class, 'model_fields'):
224
+ for field_name, field_info in model_class.model_fields.items():
225
+ default_value = field_defaults.get(field_name, "")
226
+
227
+ if field_info.is_required():
228
+ required_fields[field_name] = default_value
229
+ else:
230
+ optional_fields[field_name] = field_info.default
231
+
232
+ return robust_json_parse(response, model_class, required_fields, optional_fields)
dacp/llm.py CHANGED
@@ -9,13 +9,13 @@ from .intelligence import invoke_intelligence
9
9
 
10
10
  def call_llm(prompt: str, model: str = "gpt-4") -> str:
11
11
  """
12
- Legacy function for calling LLMs.
12
+ Legacy function for calling LLMs.
13
13
  Maintained for backward compatibility.
14
-
14
+
15
15
  Args:
16
16
  prompt: The input prompt
17
17
  model: The model to use (defaults to gpt-4)
18
-
18
+
19
19
  Returns:
20
20
  Response from the LLM
21
21
  """
@@ -26,7 +26,14 @@ def call_llm(prompt: str, model: str = "gpt-4") -> str:
26
26
  "api_key": os.getenv("OPENAI_API_KEY"),
27
27
  "endpoint": "https://api.openai.com/v1",
28
28
  "temperature": 0.7,
29
- "max_tokens": 150
29
+ "max_tokens": 150,
30
30
  }
31
-
32
- return invoke_intelligence(prompt, config)
31
+
32
+ result = invoke_intelligence(prompt, config)
33
+
34
+ # Ensure we return a string for backward compatibility
35
+ if isinstance(result, str):
36
+ return result
37
+ else:
38
+ # If it's a dict (error response), convert to string
39
+ return str(result.get("error", "Unknown error occurred"))
dacp/logging_config.py CHANGED
@@ -13,11 +13,11 @@ def setup_dacp_logging(
13
13
  level: str = "INFO",
14
14
  format_style: str = "detailed",
15
15
  include_timestamp: bool = True,
16
- log_file: Optional[str] = None
16
+ log_file: Optional[str] = None,
17
17
  ) -> None:
18
18
  """
19
19
  Set up logging for DACP components.
20
-
20
+
21
21
  Args:
22
22
  level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
23
23
  format_style: Log format style ('simple', 'detailed', 'emoji')
@@ -32,7 +32,9 @@ def setup_dacp_logging(
32
32
  log_format = "%(name)s - %(levelname)s - %(message)s"
33
33
  elif format_style == "detailed":
34
34
  if include_timestamp:
35
- log_format = "%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s"
35
+ log_format = (
36
+ "%(asctime)s - %(name)s:%(lineno)d - %(levelname)s - %(message)s"
37
+ )
36
38
  else:
37
39
  log_format = "%(name)s:%(lineno)d - %(levelname)s - %(message)s"
38
40
  elif format_style == "emoji":
@@ -43,42 +45,41 @@ def setup_dacp_logging(
43
45
  log_format = "%(message)s"
44
46
  else:
45
47
  raise ValueError(f"Unknown format_style: {format_style}")
46
-
48
+
47
49
  # Configure root logger for DACP components
48
50
  logger = logging.getLogger("dacp")
49
51
  logger.setLevel(getattr(logging, level.upper()))
50
-
52
+
51
53
  # Remove existing handlers to avoid duplicates
52
54
  for handler in logger.handlers[:]:
53
55
  logger.removeHandler(handler)
54
-
56
+
55
57
  # Create formatter
56
58
  formatter = logging.Formatter(
57
- log_format,
58
- datefmt="%Y-%m-%d %H:%M:%S" if include_timestamp else None
59
+ log_format, datefmt="%Y-%m-%d %H:%M:%S" if include_timestamp else None
59
60
  )
60
-
61
+
61
62
  # Console handler
62
63
  console_handler = logging.StreamHandler(sys.stdout)
63
64
  console_handler.setFormatter(formatter)
64
65
  logger.addHandler(console_handler)
65
-
66
+
66
67
  # Optional file handler
67
68
  if log_file:
68
69
  file_handler = logging.FileHandler(log_file)
69
70
  file_handler.setFormatter(formatter)
70
71
  logger.addHandler(file_handler)
71
-
72
+
72
73
  # Prevent propagation to root logger to avoid duplicate messages
73
74
  logger.propagate = False
74
-
75
+
75
76
  logger.info(f"🚀 DACP logging configured: level={level}, style={format_style}")
76
77
 
77
78
 
78
79
  def set_dacp_log_level(level: str) -> None:
79
80
  """
80
81
  Set the log level for all DACP components.
81
-
82
+
82
83
  Args:
83
84
  level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
84
85
  """
@@ -102,10 +103,10 @@ def enable_dacp_logging() -> None:
102
103
  def get_dacp_logger(name: str) -> logging.Logger:
103
104
  """
104
105
  Get a logger for a DACP component.
105
-
106
+
106
107
  Args:
107
108
  name: Logger name (usually __name__)
108
-
109
+
109
110
  Returns:
110
111
  Configured logger
111
112
  """
@@ -125,4 +126,4 @@ def enable_info_logging(log_file: Optional[str] = None) -> None:
125
126
 
126
127
  def enable_quiet_logging() -> None:
127
128
  """Enable only error and critical logging."""
128
- setup_dacp_logging(level="ERROR", format_style="simple", include_timestamp=False)
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")