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/__init__.py +44 -5
- dacp/intelligence.py +230 -305
- dacp/json_parser.py +232 -0
- dacp/llm.py +13 -6
- dacp/logging_config.py +17 -16
- dacp/main.py +7 -13
- dacp/orchestrator.py +230 -182
- dacp/tools.py +64 -45
- dacp/workflow.py +409 -0
- dacp/workflow_runtime.py +508 -0
- {dacp-0.3.1.dist-info → dacp-0.3.3.dist-info}/METADATA +342 -1
- dacp-0.3.3.dist-info/RECORD +18 -0
- dacp-0.3.1.dist-info/RECORD +0 -15
- {dacp-0.3.1.dist-info → dacp-0.3.3.dist-info}/WHEEL +0 -0
- {dacp-0.3.1.dist-info → dacp-0.3.3.dist-info}/licenses/LICENSE +0 -0
- {dacp-0.3.1.dist-info → dacp-0.3.3.dist-info}/top_level.txt +0 -0
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
|
-
|
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 =
|
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
|
-
|
1
|
+
#!/usr/bin/env python3
|
2
|
+
"""
|
3
|
+
DACP Main Entry Point
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
+
This module provides examples and testing functionality for DACP.
|
6
|
+
"""
|
5
7
|
|
6
|
-
|
7
|
-
|
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")
|