sondera-harness 0.6.0__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.
- sondera/__init__.py +111 -0
- sondera/__main__.py +4 -0
- sondera/adk/__init__.py +3 -0
- sondera/adk/analyze.py +222 -0
- sondera/adk/plugin.py +387 -0
- sondera/cli.py +22 -0
- sondera/exceptions.py +167 -0
- sondera/harness/__init__.py +6 -0
- sondera/harness/abc.py +102 -0
- sondera/harness/cedar/__init__.py +0 -0
- sondera/harness/cedar/harness.py +363 -0
- sondera/harness/cedar/schema.py +225 -0
- sondera/harness/sondera/__init__.py +0 -0
- sondera/harness/sondera/_grpc.py +354 -0
- sondera/harness/sondera/harness.py +890 -0
- sondera/langgraph/__init__.py +15 -0
- sondera/langgraph/analyze.py +543 -0
- sondera/langgraph/exceptions.py +19 -0
- sondera/langgraph/graph.py +210 -0
- sondera/langgraph/middleware.py +454 -0
- sondera/proto/google/protobuf/any_pb2.py +37 -0
- sondera/proto/google/protobuf/any_pb2.pyi +14 -0
- sondera/proto/google/protobuf/any_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/duration_pb2.py +37 -0
- sondera/proto/google/protobuf/duration_pb2.pyi +14 -0
- sondera/proto/google/protobuf/duration_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/empty_pb2.py +37 -0
- sondera/proto/google/protobuf/empty_pb2.pyi +9 -0
- sondera/proto/google/protobuf/empty_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/struct_pb2.py +47 -0
- sondera/proto/google/protobuf/struct_pb2.pyi +49 -0
- sondera/proto/google/protobuf/struct_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/timestamp_pb2.py +37 -0
- sondera/proto/google/protobuf/timestamp_pb2.pyi +14 -0
- sondera/proto/google/protobuf/timestamp_pb2_grpc.py +24 -0
- sondera/proto/google/protobuf/wrappers_pb2.py +53 -0
- sondera/proto/google/protobuf/wrappers_pb2.pyi +59 -0
- sondera/proto/google/protobuf/wrappers_pb2_grpc.py +24 -0
- sondera/proto/sondera/__init__.py +0 -0
- sondera/proto/sondera/core/__init__.py +0 -0
- sondera/proto/sondera/core/v1/__init__.py +0 -0
- sondera/proto/sondera/core/v1/primitives_pb2.py +88 -0
- sondera/proto/sondera/core/v1/primitives_pb2.pyi +259 -0
- sondera/proto/sondera/core/v1/primitives_pb2_grpc.py +24 -0
- sondera/proto/sondera/harness/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/__init__.py +0 -0
- sondera/proto/sondera/harness/v1/harness_pb2.py +81 -0
- sondera/proto/sondera/harness/v1/harness_pb2.pyi +192 -0
- sondera/proto/sondera/harness/v1/harness_pb2_grpc.py +498 -0
- sondera/py.typed +0 -0
- sondera/settings.py +20 -0
- sondera/strands/__init__.py +5 -0
- sondera/strands/analyze.py +244 -0
- sondera/strands/harness.py +333 -0
- sondera/tui/__init__.py +0 -0
- sondera/tui/app.py +309 -0
- sondera/tui/screens/__init__.py +5 -0
- sondera/tui/screens/adjudication.py +184 -0
- sondera/tui/screens/agent.py +158 -0
- sondera/tui/screens/trajectory.py +158 -0
- sondera/tui/widgets/__init__.py +23 -0
- sondera/tui/widgets/agent_card.py +94 -0
- sondera/tui/widgets/agent_list.py +73 -0
- sondera/tui/widgets/recent_adjudications.py +52 -0
- sondera/tui/widgets/recent_trajectories.py +54 -0
- sondera/tui/widgets/summary.py +57 -0
- sondera/tui/widgets/tool_card.py +33 -0
- sondera/tui/widgets/violation_panel.py +72 -0
- sondera/tui/widgets/violations_list.py +78 -0
- sondera/tui/widgets/violations_summary.py +104 -0
- sondera/types.py +346 -0
- sondera_harness-0.6.0.dist-info/METADATA +323 -0
- sondera_harness-0.6.0.dist-info/RECORD +77 -0
- sondera_harness-0.6.0.dist-info/WHEEL +5 -0
- sondera_harness-0.6.0.dist-info/entry_points.txt +2 -0
- sondera_harness-0.6.0.dist-info/licenses/LICENSE +21 -0
- sondera_harness-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""LangChain/LangGraph integration package for the Sondera SDK."""
|
|
2
|
+
|
|
3
|
+
from .analyze import analyze_langchain_tools, create_agent_from_langchain_tools
|
|
4
|
+
from .exceptions import GuardrailViolationError
|
|
5
|
+
from .graph import SonderaGraph
|
|
6
|
+
from .middleware import SonderaHarnessMiddleware, Strategy
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"GuardrailViolationError",
|
|
10
|
+
"SonderaHarnessMiddleware",
|
|
11
|
+
"SonderaGraph",
|
|
12
|
+
"Strategy",
|
|
13
|
+
"analyze_langchain_tools",
|
|
14
|
+
"create_agent_from_langchain_tools",
|
|
15
|
+
]
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
"""LangGraph agent analysis and automatic Agent message generation."""
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import inspect
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any, get_type_hints
|
|
9
|
+
|
|
10
|
+
from langchain_core.tools import BaseTool
|
|
11
|
+
|
|
12
|
+
from sondera.types import Agent, Parameter, SourceCode, Tool
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _python_type_to_json_schema_type(python_type: str) -> str:
|
|
18
|
+
"""Convert Python type name to JSON Schema type."""
|
|
19
|
+
type_mapping = {
|
|
20
|
+
"str": "string",
|
|
21
|
+
"int": "integer",
|
|
22
|
+
"float": "number",
|
|
23
|
+
"bool": "boolean",
|
|
24
|
+
"list": "array",
|
|
25
|
+
"dict": "object",
|
|
26
|
+
"None": "null",
|
|
27
|
+
"NoneType": "null",
|
|
28
|
+
}
|
|
29
|
+
return type_mapping.get(python_type, "string")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _extract_json_schema_from_pydantic(schema_class: Any) -> str | None:
|
|
33
|
+
"""Extract JSON schema from a Pydantic model class.
|
|
34
|
+
|
|
35
|
+
Works with both Pydantic v1 and v2.
|
|
36
|
+
"""
|
|
37
|
+
if schema_class is None:
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
# Try Pydantic v2 style first
|
|
42
|
+
if hasattr(schema_class, "model_json_schema"):
|
|
43
|
+
return json.dumps(schema_class.model_json_schema())
|
|
44
|
+
# Fallback to Pydantic v1 style
|
|
45
|
+
elif hasattr(schema_class, "schema"):
|
|
46
|
+
return json.dumps(schema_class.schema())
|
|
47
|
+
except Exception as e:
|
|
48
|
+
logger.debug(f"Could not extract JSON schema from Pydantic model: {e}")
|
|
49
|
+
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _extract_tool_json_schemas(tool: Any) -> tuple[str | None, str | None]:
|
|
54
|
+
"""Extract parameters and response JSON schemas from a LangChain tool.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
tool: A LangChain tool (BaseTool instance or decorated function)
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Tuple of (parameters_json_schema, response_json_schema)
|
|
61
|
+
"""
|
|
62
|
+
parameters_json_schema = None
|
|
63
|
+
response_json_schema = None
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
# For BaseTool instances, extract from args_schema
|
|
67
|
+
if hasattr(tool, "args_schema") and tool.args_schema is not None:
|
|
68
|
+
parameters_json_schema = _extract_json_schema_from_pydantic(
|
|
69
|
+
tool.args_schema
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Try to get the tool's input schema directly (LangChain provides this)
|
|
73
|
+
if parameters_json_schema is None and hasattr(tool, "get_input_schema"):
|
|
74
|
+
try:
|
|
75
|
+
input_schema = tool.get_input_schema()
|
|
76
|
+
if input_schema is not None:
|
|
77
|
+
parameters_json_schema = _extract_json_schema_from_pydantic(
|
|
78
|
+
input_schema
|
|
79
|
+
)
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
# For decorated functions, try to build schema from function signature
|
|
84
|
+
func = None
|
|
85
|
+
if inspect.isfunction(tool):
|
|
86
|
+
func = tool
|
|
87
|
+
elif hasattr(tool, "func") and inspect.isfunction(tool.func):
|
|
88
|
+
func = tool.func
|
|
89
|
+
|
|
90
|
+
if func is not None and parameters_json_schema is None:
|
|
91
|
+
parameters_json_schema = _build_json_schema_from_function(func)
|
|
92
|
+
|
|
93
|
+
# Extract response schema from return type
|
|
94
|
+
if func is not None:
|
|
95
|
+
response_json_schema = _build_response_schema_from_function(func)
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.debug(f"Could not extract JSON schemas from tool: {e}")
|
|
99
|
+
|
|
100
|
+
return parameters_json_schema, response_json_schema
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _build_json_schema_from_function(func: Callable) -> str | None:
|
|
104
|
+
"""Build a JSON schema from function signature and type hints."""
|
|
105
|
+
try:
|
|
106
|
+
sig = inspect.signature(func)
|
|
107
|
+
type_hints = {}
|
|
108
|
+
with contextlib.suppress(Exception):
|
|
109
|
+
type_hints = get_type_hints(func)
|
|
110
|
+
|
|
111
|
+
properties = {}
|
|
112
|
+
required = []
|
|
113
|
+
|
|
114
|
+
for param_name, param in sig.parameters.items():
|
|
115
|
+
# Skip special parameters
|
|
116
|
+
if param_name in ["self", "cls", "callbacks", "run_manager"]:
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
# Determine the type
|
|
120
|
+
param_type = "string" # default
|
|
121
|
+
if param.annotation != inspect.Parameter.empty:
|
|
122
|
+
if isinstance(param.annotation, type):
|
|
123
|
+
param_type = _python_type_to_json_schema_type(
|
|
124
|
+
param.annotation.__name__
|
|
125
|
+
)
|
|
126
|
+
else:
|
|
127
|
+
type_str = str(param.annotation)
|
|
128
|
+
# Handle common typing module types
|
|
129
|
+
if "str" in type_str.lower():
|
|
130
|
+
param_type = "string"
|
|
131
|
+
elif "int" in type_str.lower():
|
|
132
|
+
param_type = "integer"
|
|
133
|
+
elif "float" in type_str.lower():
|
|
134
|
+
param_type = "number"
|
|
135
|
+
elif "bool" in type_str.lower():
|
|
136
|
+
param_type = "boolean"
|
|
137
|
+
elif "list" in type_str.lower():
|
|
138
|
+
param_type = "array"
|
|
139
|
+
elif "dict" in type_str.lower():
|
|
140
|
+
param_type = "object"
|
|
141
|
+
elif param_name in type_hints:
|
|
142
|
+
hint = type_hints[param_name]
|
|
143
|
+
if isinstance(hint, type):
|
|
144
|
+
param_type = _python_type_to_json_schema_type(hint.__name__)
|
|
145
|
+
|
|
146
|
+
# Extract description from docstring
|
|
147
|
+
description = f"Parameter {param_name}"
|
|
148
|
+
if func.__doc__:
|
|
149
|
+
lines = func.__doc__.split("\n")
|
|
150
|
+
for line in lines:
|
|
151
|
+
if param_name in line and ":" in line:
|
|
152
|
+
parts = line.split(":")
|
|
153
|
+
if len(parts) > 1:
|
|
154
|
+
description = parts[1].strip()
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
properties[param_name] = {"type": param_type, "description": description}
|
|
158
|
+
|
|
159
|
+
# Check if parameter is required (no default value)
|
|
160
|
+
if param.default == inspect.Parameter.empty:
|
|
161
|
+
required.append(param_name)
|
|
162
|
+
|
|
163
|
+
if not properties:
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
schema = {"type": "object", "properties": properties}
|
|
167
|
+
if required:
|
|
168
|
+
schema["required"] = required
|
|
169
|
+
|
|
170
|
+
return json.dumps(schema)
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.debug(f"Could not build JSON schema from function: {e}")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _build_response_schema_from_function(func: Callable) -> str | None:
|
|
178
|
+
"""Build a response JSON schema from function return type."""
|
|
179
|
+
try:
|
|
180
|
+
sig = inspect.signature(func)
|
|
181
|
+
return_type = None
|
|
182
|
+
|
|
183
|
+
if sig.return_annotation != inspect.Signature.empty:
|
|
184
|
+
if isinstance(sig.return_annotation, type):
|
|
185
|
+
return_type = sig.return_annotation.__name__
|
|
186
|
+
else:
|
|
187
|
+
return_type = str(sig.return_annotation)
|
|
188
|
+
|
|
189
|
+
if return_type is None:
|
|
190
|
+
try:
|
|
191
|
+
type_hints = get_type_hints(func)
|
|
192
|
+
if "return" in type_hints:
|
|
193
|
+
hint = type_hints["return"]
|
|
194
|
+
return_type = hint.__name__ if isinstance(hint, type) else str(hint)
|
|
195
|
+
except Exception:
|
|
196
|
+
pass
|
|
197
|
+
|
|
198
|
+
if return_type and return_type not in ["Any", "None", "NoneType"]:
|
|
199
|
+
json_type = _python_type_to_json_schema_type(return_type)
|
|
200
|
+
return json.dumps(
|
|
201
|
+
{
|
|
202
|
+
"type": json_type,
|
|
203
|
+
"description": f"Return value of type {return_type}",
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
except Exception as e:
|
|
208
|
+
logger.debug(f"Could not build response schema from function: {e}")
|
|
209
|
+
|
|
210
|
+
return None
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _get_function_source(func: Callable) -> tuple[str, str]:
|
|
214
|
+
"""Extract source code and language from a function."""
|
|
215
|
+
try:
|
|
216
|
+
source = inspect.getsource(func)
|
|
217
|
+
return "python", source
|
|
218
|
+
except (OSError, TypeError):
|
|
219
|
+
# Source not available (e.g., built-in function)
|
|
220
|
+
return "python", f"# Source code not available for {func.__name__}"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _analyze_function_parameters(func: Callable) -> list[Parameter]:
|
|
224
|
+
"""Analyze function parameters and return Sondera format Parameters."""
|
|
225
|
+
parameters = []
|
|
226
|
+
sig = inspect.signature(func)
|
|
227
|
+
|
|
228
|
+
# Try to get type hints for better type information
|
|
229
|
+
try:
|
|
230
|
+
type_hints = get_type_hints(func)
|
|
231
|
+
except Exception:
|
|
232
|
+
type_hints = {}
|
|
233
|
+
|
|
234
|
+
for param_name, param in sig.parameters.items():
|
|
235
|
+
# Skip special parameters that LangChain injects
|
|
236
|
+
if param_name in ["self", "cls", "callbacks", "run_manager"]:
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
# Get parameter type
|
|
240
|
+
param_type = "Any"
|
|
241
|
+
if param.annotation != inspect.Parameter.empty:
|
|
242
|
+
if isinstance(param.annotation, type):
|
|
243
|
+
param_type = param.annotation.__name__
|
|
244
|
+
else:
|
|
245
|
+
param_type = str(param.annotation)
|
|
246
|
+
elif param_name in type_hints:
|
|
247
|
+
hint = type_hints[param_name]
|
|
248
|
+
param_type = hint.__name__ if isinstance(hint, type) else str(hint)
|
|
249
|
+
|
|
250
|
+
# Extract parameter description from docstring if available
|
|
251
|
+
description = f"Parameter {param_name}"
|
|
252
|
+
if func.__doc__:
|
|
253
|
+
# Simple extraction - could be enhanced with proper docstring parsing
|
|
254
|
+
lines = func.__doc__.split("\n")
|
|
255
|
+
for line in lines:
|
|
256
|
+
if param_name in line and ":" in line:
|
|
257
|
+
# Try to extract description after parameter name
|
|
258
|
+
parts = line.split(":")
|
|
259
|
+
if len(parts) > 1:
|
|
260
|
+
description = parts[1].strip()
|
|
261
|
+
break
|
|
262
|
+
|
|
263
|
+
parameters.append(
|
|
264
|
+
Parameter(name=param_name, description=description, type=param_type)
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
return parameters
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _get_function_return_type(func: Callable) -> str:
|
|
271
|
+
"""Extract the return type from a function."""
|
|
272
|
+
sig = inspect.signature(func)
|
|
273
|
+
if sig.return_annotation != inspect.Signature.empty:
|
|
274
|
+
if isinstance(sig.return_annotation, type):
|
|
275
|
+
return sig.return_annotation.__name__
|
|
276
|
+
else:
|
|
277
|
+
return str(sig.return_annotation)
|
|
278
|
+
|
|
279
|
+
# Try type hints as fallback
|
|
280
|
+
try:
|
|
281
|
+
type_hints = get_type_hints(func)
|
|
282
|
+
if "return" in type_hints:
|
|
283
|
+
hint = type_hints["return"]
|
|
284
|
+
if isinstance(hint, type):
|
|
285
|
+
return hint.__name__
|
|
286
|
+
else:
|
|
287
|
+
return str(hint)
|
|
288
|
+
except Exception:
|
|
289
|
+
pass
|
|
290
|
+
|
|
291
|
+
return "Any"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _analyze_langchain_tool(tool: Any) -> Tool:
|
|
295
|
+
"""Analyze a LangChain tool and convert it to Sondera Tool format."""
|
|
296
|
+
# Extract JSON schemas for the tool (works for all tool types)
|
|
297
|
+
parameters_json_schema, response_json_schema = _extract_tool_json_schemas(tool)
|
|
298
|
+
|
|
299
|
+
if inspect.isfunction(tool):
|
|
300
|
+
# It's a raw function decorated with @tool
|
|
301
|
+
func = tool
|
|
302
|
+
tool_name = func.__name__
|
|
303
|
+
tool_description = func.__doc__ or f"Function {tool_name}"
|
|
304
|
+
|
|
305
|
+
# Analyze function signature for parameters
|
|
306
|
+
parameters = _analyze_function_parameters(func)
|
|
307
|
+
|
|
308
|
+
# Get return type
|
|
309
|
+
response_type = _get_function_return_type(func)
|
|
310
|
+
|
|
311
|
+
# Extract source code
|
|
312
|
+
language, source_code = _get_function_source(func)
|
|
313
|
+
|
|
314
|
+
return Tool(
|
|
315
|
+
name=tool_name,
|
|
316
|
+
description=tool_description.strip(),
|
|
317
|
+
parameters=parameters,
|
|
318
|
+
parameters_json_schema=parameters_json_schema,
|
|
319
|
+
response=response_type,
|
|
320
|
+
response_json_schema=response_json_schema,
|
|
321
|
+
source=SourceCode(language=language, code=source_code),
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
elif isinstance(tool, BaseTool) or hasattr(tool, "func"):
|
|
325
|
+
# It's a BaseTool instance (including StructuredTool from @tool decorator)
|
|
326
|
+
tool_name = tool.name
|
|
327
|
+
tool_description = tool.description or f"Tool {tool_name}"
|
|
328
|
+
|
|
329
|
+
# If it has a func attribute (from @tool decorator), analyze the underlying function
|
|
330
|
+
# Note: StructuredTool has func attr, but BaseTool doesn't - use getattr for type safety
|
|
331
|
+
if (func := getattr(tool, "func", None)) and inspect.isfunction(func):
|
|
332
|
+
parameters = _analyze_function_parameters(func)
|
|
333
|
+
response_type = _get_function_return_type(func)
|
|
334
|
+
language, source_code = _get_function_source(func)
|
|
335
|
+
else:
|
|
336
|
+
# For other BaseTool instances, try to extract parameters from the schema
|
|
337
|
+
parameters = []
|
|
338
|
+
if hasattr(tool, "args_schema") and tool.args_schema:
|
|
339
|
+
schema = tool.args_schema
|
|
340
|
+
# Pydantic v1 style - has __fields__ dict with ModelField objects
|
|
341
|
+
if v1_fields := getattr(schema, "__fields__", None):
|
|
342
|
+
for field_name, field_info in v1_fields.items():
|
|
343
|
+
param_type = "Any"
|
|
344
|
+
# Pydantic v1 ModelField uses type_ attribute
|
|
345
|
+
if field_type := getattr(field_info, "type_", None):
|
|
346
|
+
if isinstance(field_type, type):
|
|
347
|
+
param_type = field_type.__name__
|
|
348
|
+
else:
|
|
349
|
+
param_type = str(field_type)
|
|
350
|
+
|
|
351
|
+
description = getattr(
|
|
352
|
+
field_info, "description", f"Parameter {field_name}"
|
|
353
|
+
)
|
|
354
|
+
if description is None:
|
|
355
|
+
description = f"Parameter {field_name}"
|
|
356
|
+
parameters.append(
|
|
357
|
+
Parameter(
|
|
358
|
+
name=field_name,
|
|
359
|
+
description=description,
|
|
360
|
+
type=param_type,
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
# Pydantic v2 style - has model_fields dict with FieldInfo objects
|
|
364
|
+
elif v2_fields := getattr(schema, "model_fields", None):
|
|
365
|
+
for field_name, field_info in v2_fields.items():
|
|
366
|
+
param_type = "Any"
|
|
367
|
+
if annotation := getattr(field_info, "annotation", None):
|
|
368
|
+
if isinstance(annotation, type):
|
|
369
|
+
param_type = annotation.__name__
|
|
370
|
+
else:
|
|
371
|
+
param_type = str(annotation)
|
|
372
|
+
|
|
373
|
+
description = getattr(
|
|
374
|
+
field_info, "description", f"Parameter {field_name}"
|
|
375
|
+
)
|
|
376
|
+
if description is None:
|
|
377
|
+
description = f"Parameter {field_name}"
|
|
378
|
+
parameters.append(
|
|
379
|
+
Parameter(
|
|
380
|
+
name=field_name,
|
|
381
|
+
description=description,
|
|
382
|
+
type=param_type,
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
response_type = "Any"
|
|
387
|
+
|
|
388
|
+
# Try to get source code from various methods
|
|
389
|
+
language = "python"
|
|
390
|
+
source_code = f"# BaseTool instance: {tool_name}"
|
|
391
|
+
for method_name in ["_run", "_arun", "run", "__call__"]:
|
|
392
|
+
if hasattr(tool, method_name):
|
|
393
|
+
try:
|
|
394
|
+
method = getattr(tool, method_name)
|
|
395
|
+
source_code = inspect.getsource(method)
|
|
396
|
+
break
|
|
397
|
+
except Exception:
|
|
398
|
+
pass
|
|
399
|
+
|
|
400
|
+
return Tool(
|
|
401
|
+
name=tool_name,
|
|
402
|
+
description=tool_description,
|
|
403
|
+
parameters=parameters,
|
|
404
|
+
parameters_json_schema=parameters_json_schema,
|
|
405
|
+
response=response_type,
|
|
406
|
+
response_json_schema=response_json_schema,
|
|
407
|
+
source=SourceCode(language=language, code=source_code),
|
|
408
|
+
)
|
|
409
|
+
|
|
410
|
+
else:
|
|
411
|
+
# Unknown tool type, do our best
|
|
412
|
+
tool_name = getattr(tool, "name", tool.__class__.__name__)
|
|
413
|
+
tool_description = getattr(tool, "description", f"Tool {tool_name}")
|
|
414
|
+
|
|
415
|
+
return Tool(
|
|
416
|
+
name=tool_name,
|
|
417
|
+
description=tool_description,
|
|
418
|
+
parameters=[],
|
|
419
|
+
parameters_json_schema=parameters_json_schema,
|
|
420
|
+
response="Any",
|
|
421
|
+
response_json_schema=response_json_schema,
|
|
422
|
+
source=SourceCode(
|
|
423
|
+
language="python", code=f"# Unknown tool type: {type(tool)}"
|
|
424
|
+
),
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def analyze_langchain_tools(
|
|
429
|
+
tools: list[Any],
|
|
430
|
+
agent_id: str,
|
|
431
|
+
agent_name: str | None = None,
|
|
432
|
+
agent_description: str | None = None,
|
|
433
|
+
agent_instruction: str | None = None,
|
|
434
|
+
provider_id: str = "langchain",
|
|
435
|
+
) -> Agent:
|
|
436
|
+
"""Analyze LangChain tools and generate a Sondera Agent object.
|
|
437
|
+
|
|
438
|
+
Args:
|
|
439
|
+
tools: List of LangChain tools (functions decorated with @tool or BaseTool instances)
|
|
440
|
+
agent_id: Unique identifier for the agent
|
|
441
|
+
agent_name: Name of the agent (defaults to agent_id)
|
|
442
|
+
agent_description: Description of the agent
|
|
443
|
+
agent_instruction: Instruction or goal of the agent
|
|
444
|
+
provider_id: Provider identifier (defaults to "langchain")
|
|
445
|
+
|
|
446
|
+
Returns:
|
|
447
|
+
Agent object with automatically analyzed tools
|
|
448
|
+
"""
|
|
449
|
+
agent_name = agent_name or agent_id
|
|
450
|
+
agent_description = agent_description or f"Agent {agent_name}"
|
|
451
|
+
agent_instruction = agent_instruction or "Execute tasks using available tools"
|
|
452
|
+
|
|
453
|
+
sondera_tools = []
|
|
454
|
+
for tool in tools:
|
|
455
|
+
try:
|
|
456
|
+
sondera_tool = _analyze_langchain_tool(tool)
|
|
457
|
+
sondera_tools.append(sondera_tool)
|
|
458
|
+
except Exception as e:
|
|
459
|
+
# Log the error but continue with other tools
|
|
460
|
+
import logging
|
|
461
|
+
|
|
462
|
+
logging.warning(f"Failed to analyze tool {tool}: {e}")
|
|
463
|
+
# Create a minimal tool entry
|
|
464
|
+
tool_name = getattr(tool, "name", str(tool))
|
|
465
|
+
sondera_tools.append(
|
|
466
|
+
Tool(
|
|
467
|
+
name=tool_name,
|
|
468
|
+
description=f"Tool {tool_name} (analysis failed)",
|
|
469
|
+
parameters=[],
|
|
470
|
+
response="Any",
|
|
471
|
+
source=SourceCode(
|
|
472
|
+
language="python", code=f"# Analysis failed for {tool_name}"
|
|
473
|
+
),
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
return Agent(
|
|
478
|
+
id=agent_id,
|
|
479
|
+
provider_id=provider_id,
|
|
480
|
+
name=agent_name,
|
|
481
|
+
description=agent_description,
|
|
482
|
+
instruction=agent_instruction,
|
|
483
|
+
tools=sondera_tools,
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
|
|
487
|
+
def create_agent_from_langchain_tools(
|
|
488
|
+
tools: list[Any],
|
|
489
|
+
agent_id: str,
|
|
490
|
+
agent_name: str | None = None,
|
|
491
|
+
agent_description: str | None = None,
|
|
492
|
+
agent_instruction: str | None = None,
|
|
493
|
+
provider_id: str = "langchain",
|
|
494
|
+
system_prompt_func: Callable[[], str] | None = None,
|
|
495
|
+
) -> Agent:
|
|
496
|
+
"""Convenience function to create a Sondera Agent from LangChain tools.
|
|
497
|
+
|
|
498
|
+
This function automatically analyzes LangChain tools and creates a Sondera Agent.
|
|
499
|
+
It can also extract system instructions from a provided system prompt function.
|
|
500
|
+
|
|
501
|
+
Args:
|
|
502
|
+
tools: List of LangChain tools (functions decorated with @tool or BaseTool instances)
|
|
503
|
+
agent_id: Unique identifier for the agent
|
|
504
|
+
agent_name: Human-readable name for the agent
|
|
505
|
+
agent_description: Description of what the agent does
|
|
506
|
+
agent_instruction: Instructions for the agent behavior (optional if system_prompt_func provided)
|
|
507
|
+
provider_id: Provider identifier (default: "langchain")
|
|
508
|
+
system_prompt_func: Optional function that returns system prompt/instructions
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
Agent: Configured Sondera Agent with automatically analyzed tools
|
|
512
|
+
"""
|
|
513
|
+
|
|
514
|
+
# Extract system instruction from system_prompt_func if provided and agent_instruction is None
|
|
515
|
+
final_instruction = agent_instruction
|
|
516
|
+
if final_instruction is None and system_prompt_func is not None:
|
|
517
|
+
try:
|
|
518
|
+
system_prompt = system_prompt_func()
|
|
519
|
+
if isinstance(system_prompt, str) and system_prompt.strip():
|
|
520
|
+
final_instruction = system_prompt.strip()
|
|
521
|
+
logger.info(
|
|
522
|
+
f"Extracted system instruction from system_prompt_func: {len(final_instruction)} characters"
|
|
523
|
+
)
|
|
524
|
+
except Exception as e:
|
|
525
|
+
logger.warning(
|
|
526
|
+
f"Failed to extract system instruction from system_prompt_func: {e}"
|
|
527
|
+
)
|
|
528
|
+
|
|
529
|
+
# Fallback to a default instruction if none provided
|
|
530
|
+
if final_instruction is None:
|
|
531
|
+
final_instruction = (
|
|
532
|
+
"Use the available tools to assist users effectively and safely."
|
|
533
|
+
)
|
|
534
|
+
logger.info("Using default agent instruction")
|
|
535
|
+
|
|
536
|
+
return analyze_langchain_tools(
|
|
537
|
+
tools=tools,
|
|
538
|
+
agent_id=agent_id,
|
|
539
|
+
agent_name=agent_name,
|
|
540
|
+
agent_description=agent_description,
|
|
541
|
+
agent_instruction=final_instruction,
|
|
542
|
+
provider_id=provider_id,
|
|
543
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""LangChain agent integration exceptions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
from sondera.types import Stage
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass(slots=True)
|
|
11
|
+
class GuardrailViolationError(RuntimeError):
|
|
12
|
+
"""Raised when guardrail enforcement blocks agent execution."""
|
|
13
|
+
|
|
14
|
+
stage: Stage
|
|
15
|
+
node: str
|
|
16
|
+
reason: str
|
|
17
|
+
|
|
18
|
+
def __str__(self) -> str: # pragma: no cover - string formatting
|
|
19
|
+
return f"Guardrail violation at {self.node} during {self.stage.value}: {self.reason}"
|