agentspec-ai 0.1.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.
- agentspec/__init__.py +13 -0
- agentspec/__main__.py +6 -0
- agentspec/adapters/__init__.py +45 -0
- agentspec/adapters/autogen.py +116 -0
- agentspec/adapters/crewai.py +102 -0
- agentspec/adapters/generic.py +165 -0
- agentspec/adapters/langchain.py +131 -0
- agentspec/adapters/openai_agents.py +111 -0
- agentspec/badge/__init__.py +1 -0
- agentspec/badge/generator.py +58 -0
- agentspec/badge/scorer.py +258 -0
- agentspec/badge/templates/.gitkeep +0 -0
- agentspec/ci/__init__.py +11 -0
- agentspec/ci/github.py +52 -0
- agentspec/ci/gitlab.py +33 -0
- agentspec/ci/pre_commit.py +58 -0
- agentspec/cli/__init__.py +1 -0
- agentspec/cli/app.py +645 -0
- agentspec/cli/diff_cmd.py +144 -0
- agentspec/cli/init_cmd.py +103 -0
- agentspec/cli/report_cmd.py +409 -0
- agentspec/cli/serve.py +58 -0
- agentspec/core/__init__.py +1 -0
- agentspec/core/linter.py +846 -0
- agentspec/core/loader.py +171 -0
- agentspec/core/models.py +560 -0
- agentspec/core/resolver.py +97 -0
- agentspec/core/validator.py +244 -0
- agentspec/generator/__init__.py +13 -0
- agentspec/generator/analyzer.py +168 -0
- agentspec/generator/llm_generator.py +132 -0
- agentspec/generator/scanner.py +268 -0
- agentspec/generator/templates.py +103 -0
- agentspec/runtime/__init__.py +46 -0
- agentspec/runtime/circuit_breaker.py +145 -0
- agentspec/runtime/decorators.py +90 -0
- agentspec/runtime/frameworks/__init__.py +16 -0
- agentspec/runtime/frameworks/generic_wrapper.py +151 -0
- agentspec/runtime/frameworks/langchain_middleware.py +243 -0
- agentspec/runtime/guards.py +236 -0
- agentspec/runtime/middleware.py +427 -0
- agentspec/runtime/tracker.py +168 -0
- agentspec/testing/__init__.py +1 -0
- agentspec/testing/assertions.py +274 -0
- agentspec/testing/generator.py +221 -0
- agentspec/testing/reporters/__init__.py +1 -0
- agentspec/testing/reporters/console.py +43 -0
- agentspec/testing/reporters/html.py +120 -0
- agentspec/testing/reporters/json.py +25 -0
- agentspec/testing/reporters/junit.py +54 -0
- agentspec/testing/runner.py +300 -0
- agentspec/utils/__init__.py +1 -0
- agentspec/utils/hash.py +47 -0
- agentspec/utils/yaml_utils.py +87 -0
- agentspec_ai-0.1.0.dist-info/METADATA +824 -0
- agentspec_ai-0.1.0.dist-info/RECORD +59 -0
- agentspec_ai-0.1.0.dist-info/WHEEL +4 -0
- agentspec_ai-0.1.0.dist-info/entry_points.txt +4 -0
- agentspec_ai-0.1.0.dist-info/licenses/LICENSE +94 -0
agentspec/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""AgentSpec — The OpenAPI for Agentic AI.
|
|
2
|
+
|
|
3
|
+
A universal specification standard for AI agent behavioral contracts.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
__spec_version__ = "0.1.0"
|
|
8
|
+
|
|
9
|
+
from agentspec.core.models import AgentSpecDocument
|
|
10
|
+
from agentspec.core.loader import SpecLoader
|
|
11
|
+
from agentspec.core.validator import SpecValidator
|
|
12
|
+
|
|
13
|
+
__all__ = ["AgentSpecDocument", "SpecLoader", "SpecValidator", "__version__", "__spec_version__"]
|
agentspec/__main__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Framework adapters for connecting AgentSpec to agent frameworks.
|
|
2
|
+
|
|
3
|
+
Provides abstract base class and registry for adapter plugins.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AgentAdapter(ABC):
|
|
13
|
+
"""Abstract base class for all framework adapters."""
|
|
14
|
+
|
|
15
|
+
name: str = "base"
|
|
16
|
+
|
|
17
|
+
@abstractmethod
|
|
18
|
+
def connect(self, source: str, **kwargs: Any) -> None:
|
|
19
|
+
"""Connect to an agent instance."""
|
|
20
|
+
|
|
21
|
+
@abstractmethod
|
|
22
|
+
def extract_spec(self) -> dict[str, Any]:
|
|
23
|
+
"""Extract a partial AgentSpec from the connected agent."""
|
|
24
|
+
|
|
25
|
+
@abstractmethod
|
|
26
|
+
def disconnect(self) -> None:
|
|
27
|
+
"""Disconnect from the agent."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class AdapterRegistry:
|
|
31
|
+
"""Registry for discovering and loading adapters."""
|
|
32
|
+
|
|
33
|
+
_adapters: dict[str, type[AgentAdapter]] = {}
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def register(cls, name: str, adapter_cls: type[AgentAdapter]) -> None:
|
|
37
|
+
cls._adapters[name] = adapter_cls
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def get(cls, name: str) -> type[AgentAdapter] | None:
|
|
41
|
+
return cls._adapters.get(name)
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def list_adapters(cls) -> list[str]:
|
|
45
|
+
return list(cls._adapters.keys())
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""AutoGen framework adapter.
|
|
2
|
+
|
|
3
|
+
Connects to AutoGen ConversableAgent or AssistantAgent and extracts
|
|
4
|
+
a partial AgentSpec.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from agentspec.adapters import AgentAdapter, AdapterRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AutoGenAdapter(AgentAdapter):
|
|
15
|
+
"""Adapter for Microsoft AutoGen agents.
|
|
16
|
+
|
|
17
|
+
Supports ConversableAgent, AssistantAgent, and UserProxyAgent.
|
|
18
|
+
Extracts system messages, registered functions, and model config.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "autogen"
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._agent: Any = None
|
|
25
|
+
self._system_message: str | None = None
|
|
26
|
+
self._functions: list[dict[str, Any]] = []
|
|
27
|
+
self._model: str | None = None
|
|
28
|
+
|
|
29
|
+
def connect(self, source: str, **kwargs: Any) -> None:
|
|
30
|
+
"""Connect to an AutoGen agent by importing its module.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
source: Python module path to the agent.
|
|
34
|
+
The module must expose an ``agent`` attribute.
|
|
35
|
+
"""
|
|
36
|
+
import importlib
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
module = importlib.import_module(source)
|
|
40
|
+
self._agent = getattr(module, "agent", None) or getattr(module, "assistant", None)
|
|
41
|
+
except ImportError as exc:
|
|
42
|
+
raise ConnectionError(f"Cannot import AutoGen agent from '{source}': {exc}") from exc
|
|
43
|
+
|
|
44
|
+
if self._agent is not None:
|
|
45
|
+
self._system_message = getattr(self._agent, "system_message", None)
|
|
46
|
+
self._extract_model()
|
|
47
|
+
self._extract_functions()
|
|
48
|
+
|
|
49
|
+
def disconnect(self) -> None:
|
|
50
|
+
self._agent = None
|
|
51
|
+
|
|
52
|
+
def extract_spec(self) -> dict[str, Any]:
|
|
53
|
+
"""Extract a partial AgentSpec from the AutoGen agent."""
|
|
54
|
+
capabilities = [
|
|
55
|
+
{
|
|
56
|
+
"id": _to_kebab(fn.get("name", f"fn-{i}")),
|
|
57
|
+
"name": fn.get("name", f"Function {i}"),
|
|
58
|
+
"description": fn.get("description", ""),
|
|
59
|
+
"category": "tool-use",
|
|
60
|
+
}
|
|
61
|
+
for i, fn in enumerate(self._functions)
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
# Add a default reasoning capability based on system message
|
|
65
|
+
if self._system_message and not capabilities:
|
|
66
|
+
capabilities.append({
|
|
67
|
+
"id": "agent-reasoning",
|
|
68
|
+
"name": "Agent Reasoning",
|
|
69
|
+
"description": self._system_message[:200],
|
|
70
|
+
"category": "reasoning",
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
spec: dict[str, Any] = {
|
|
74
|
+
"agentspec": "0.1.0",
|
|
75
|
+
"info": {"name": "autogen-agent", "version": "0.1.0"},
|
|
76
|
+
"agent": {
|
|
77
|
+
"type": "semi-autonomous",
|
|
78
|
+
"runtime": {
|
|
79
|
+
"framework": "autogen",
|
|
80
|
+
"model": self._model or "gpt-4o",
|
|
81
|
+
"model_provider": "openai",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
"capabilities": capabilities or [{"id": "default-task", "name": "Default Task", "category": "reasoning"}],
|
|
85
|
+
"boundaries": [],
|
|
86
|
+
"guardrails": {"behavioral_guardrails": {}},
|
|
87
|
+
"failure_modes": [],
|
|
88
|
+
}
|
|
89
|
+
return spec
|
|
90
|
+
|
|
91
|
+
def _extract_model(self) -> None:
|
|
92
|
+
if self._agent is None:
|
|
93
|
+
return
|
|
94
|
+
llm_config = getattr(self._agent, "llm_config", None) or {}
|
|
95
|
+
if isinstance(llm_config, dict):
|
|
96
|
+
configs = llm_config.get("config_list", [])
|
|
97
|
+
if configs and isinstance(configs[0], dict):
|
|
98
|
+
self._model = configs[0].get("model")
|
|
99
|
+
|
|
100
|
+
def _extract_functions(self) -> None:
|
|
101
|
+
if self._agent is None:
|
|
102
|
+
return
|
|
103
|
+
# AutoGen stores function_map as a dict
|
|
104
|
+
fn_map = getattr(self._agent, "function_map", {}) or {}
|
|
105
|
+
for name, fn in fn_map.items():
|
|
106
|
+
self._functions.append({
|
|
107
|
+
"name": name,
|
|
108
|
+
"description": getattr(fn, "__doc__", "") or "",
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _to_kebab(name: str) -> str:
|
|
113
|
+
return name.replace("_", "-").replace(" ", "-").lower()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
AdapterRegistry.register("autogen", AutoGenAdapter)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""CrewAI framework adapter.
|
|
2
|
+
|
|
3
|
+
Maps CrewAI Agent/Crew/Task concepts to AgentSpec sections.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agentspec.adapters import AgentAdapter, AdapterRegistry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CrewAIAdapter(AgentAdapter):
|
|
14
|
+
"""Adapter for CrewAI Crew objects.
|
|
15
|
+
|
|
16
|
+
Maps Crew → multi-agent-orchestrator, Agent → agent,
|
|
17
|
+
Task → capability, Tool → tool.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
name = "crewai"
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
self._crew: Any = None
|
|
24
|
+
self._agents: list[Any] = []
|
|
25
|
+
self._tasks: list[Any] = []
|
|
26
|
+
self._tools: list[Any] = []
|
|
27
|
+
|
|
28
|
+
def connect(self, source: str, **kwargs: Any) -> None:
|
|
29
|
+
"""Connect to a CrewAI Crew by importing its module.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
source: Python module path to the crew (e.g., "my_crew.crew").
|
|
33
|
+
The module must expose a ``crew`` attribute.
|
|
34
|
+
"""
|
|
35
|
+
import importlib
|
|
36
|
+
|
|
37
|
+
try:
|
|
38
|
+
module = importlib.import_module(source)
|
|
39
|
+
self._crew = getattr(module, "crew", None)
|
|
40
|
+
except ImportError as exc:
|
|
41
|
+
raise ConnectionError(f"Cannot import CrewAI crew from '{source}': {exc}") from exc
|
|
42
|
+
|
|
43
|
+
if self._crew is not None:
|
|
44
|
+
self._agents = list(getattr(self._crew, "agents", []) or [])
|
|
45
|
+
self._tasks = list(getattr(self._crew, "tasks", []) or [])
|
|
46
|
+
# Collect tools from all agents
|
|
47
|
+
for agent in self._agents:
|
|
48
|
+
self._tools.extend(getattr(agent, "tools", []) or [])
|
|
49
|
+
|
|
50
|
+
def disconnect(self) -> None:
|
|
51
|
+
"""Release references."""
|
|
52
|
+
self._crew = None
|
|
53
|
+
self._agents = []
|
|
54
|
+
self._tasks = []
|
|
55
|
+
self._tools = []
|
|
56
|
+
|
|
57
|
+
def extract_spec(self) -> dict[str, Any]:
|
|
58
|
+
"""Extract a partial spec from the CrewAI crew."""
|
|
59
|
+
capabilities = [
|
|
60
|
+
{
|
|
61
|
+
"id": _to_kebab(getattr(task, "description", f"task-{i}")[:40]),
|
|
62
|
+
"name": getattr(task, "name", f"Task {i}") or f"Task {i}",
|
|
63
|
+
"description": getattr(task, "description", ""),
|
|
64
|
+
"category": "reasoning",
|
|
65
|
+
}
|
|
66
|
+
for i, task in enumerate(self._tasks)
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
tools_spec = [
|
|
70
|
+
{
|
|
71
|
+
"id": _to_kebab(getattr(t, "name", f"tool-{i}")),
|
|
72
|
+
"name": getattr(t, "name", f"Tool {i}"),
|
|
73
|
+
"protocol": "custom",
|
|
74
|
+
"permissions": ["read"],
|
|
75
|
+
}
|
|
76
|
+
for i, t in enumerate(self._tools)
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
agent_type = "multi-agent-orchestrator" if len(self._agents) > 1 else "autonomous"
|
|
80
|
+
|
|
81
|
+
spec: dict[str, Any] = {
|
|
82
|
+
"agentspec": "0.1.0",
|
|
83
|
+
"info": {"name": "crewai-crew", "version": "0.1.0"},
|
|
84
|
+
"agent": {
|
|
85
|
+
"type": agent_type,
|
|
86
|
+
"runtime": {"framework": "crewai"},
|
|
87
|
+
},
|
|
88
|
+
"capabilities": capabilities,
|
|
89
|
+
"boundaries": [],
|
|
90
|
+
"guardrails": {"behavioral_guardrails": {}},
|
|
91
|
+
"failure_modes": [],
|
|
92
|
+
}
|
|
93
|
+
if tools_spec:
|
|
94
|
+
spec["tools"] = tools_spec
|
|
95
|
+
return spec
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _to_kebab(name: str) -> str:
|
|
99
|
+
return name.replace("_", "-").replace(" ", "-").lower().strip("-")[:40]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
AdapterRegistry.register("crewai", CrewAIAdapter)
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
"""Generic HTTP/callable adapter.
|
|
2
|
+
|
|
3
|
+
Provides a flexible adapter for any agent exposed as:
|
|
4
|
+
- A Python callable (function or class with .invoke())
|
|
5
|
+
- An HTTP endpoint
|
|
6
|
+
- An MCP-compatible server (stdio or HTTP/SSE)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any, Callable
|
|
14
|
+
|
|
15
|
+
from agentspec.adapters import AgentAdapter, AdapterRegistry
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class GenericAdapter(AgentAdapter):
|
|
19
|
+
"""Generic adapter connecting to any callable, HTTP endpoint, or MCP server.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
auth_header: Optional ``Authorization`` header value (e.g., "Bearer <token>").
|
|
23
|
+
response_path: JSONPath-style dot-notation to extract the response text
|
|
24
|
+
(e.g., "choices.0.message.content"). Defaults to the raw body.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
name = "generic"
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
auth_header: str | None = None,
|
|
32
|
+
response_path: str | None = None,
|
|
33
|
+
) -> None:
|
|
34
|
+
self._source: str | None = None
|
|
35
|
+
self._callable: Callable[[str], str] | None = None
|
|
36
|
+
self._endpoint: str | None = None
|
|
37
|
+
self._auth_header = auth_header
|
|
38
|
+
self._response_path = response_path
|
|
39
|
+
self._discovered_tools: list[dict[str, Any]] = []
|
|
40
|
+
|
|
41
|
+
def connect(self, source: str, **kwargs: Any) -> None:
|
|
42
|
+
"""Connect to a generic source.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
source: Either a Python module path (e.g., ``mymodule.agent``),
|
|
46
|
+
an HTTP URL (e.g., ``http://localhost:8080/invoke``), or
|
|
47
|
+
an MCP server path (e.g., ``mcp://localhost:8081``).
|
|
48
|
+
"""
|
|
49
|
+
self._source = source
|
|
50
|
+
|
|
51
|
+
if source.startswith(("http://", "https://")):
|
|
52
|
+
self._endpoint = source
|
|
53
|
+
self._probe_http()
|
|
54
|
+
elif source.startswith("mcp://"):
|
|
55
|
+
self._probe_mcp(source)
|
|
56
|
+
else:
|
|
57
|
+
# Assume Python module path
|
|
58
|
+
import importlib
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
module = importlib.import_module(source)
|
|
62
|
+
agent = (
|
|
63
|
+
getattr(module, "agent", None)
|
|
64
|
+
or getattr(module, "app", None)
|
|
65
|
+
or getattr(module, "invoke", None)
|
|
66
|
+
)
|
|
67
|
+
if callable(agent):
|
|
68
|
+
self._callable = agent
|
|
69
|
+
except ImportError as exc:
|
|
70
|
+
raise ConnectionError(f"Cannot connect to '{source}': {exc}") from exc
|
|
71
|
+
|
|
72
|
+
def disconnect(self) -> None:
|
|
73
|
+
self._callable = None
|
|
74
|
+
self._endpoint = None
|
|
75
|
+
self._source = None
|
|
76
|
+
|
|
77
|
+
def extract_spec(self) -> dict[str, Any]:
|
|
78
|
+
"""Generate a partial spec from discovered information."""
|
|
79
|
+
capabilities = [
|
|
80
|
+
{
|
|
81
|
+
"id": t["id"],
|
|
82
|
+
"name": t["name"],
|
|
83
|
+
"description": t.get("description", ""),
|
|
84
|
+
"category": "tool-use",
|
|
85
|
+
}
|
|
86
|
+
for t in self._discovered_tools
|
|
87
|
+
] or [{"id": "default-capability", "name": "Default Capability", "category": "reasoning"}]
|
|
88
|
+
|
|
89
|
+
agent_type = "tool-agent" if self._discovered_tools else "reactive"
|
|
90
|
+
|
|
91
|
+
spec: dict[str, Any] = {
|
|
92
|
+
"agentspec": "0.1.0",
|
|
93
|
+
"info": {"name": _source_to_name(self._source or "generic-agent"), "version": "0.1.0"},
|
|
94
|
+
"agent": {
|
|
95
|
+
"type": agent_type,
|
|
96
|
+
"runtime": {"framework": "custom"},
|
|
97
|
+
},
|
|
98
|
+
"capabilities": capabilities,
|
|
99
|
+
"boundaries": [],
|
|
100
|
+
"guardrails": {"behavioral_guardrails": {}},
|
|
101
|
+
"failure_modes": [],
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if self._endpoint:
|
|
105
|
+
spec["agent"]["protocol"] = "http"
|
|
106
|
+
|
|
107
|
+
return spec
|
|
108
|
+
|
|
109
|
+
def _probe_http(self) -> None:
|
|
110
|
+
"""Attempt to probe the HTTP endpoint for OpenAPI or tool definitions."""
|
|
111
|
+
if not self._endpoint:
|
|
112
|
+
return
|
|
113
|
+
try:
|
|
114
|
+
import httpx
|
|
115
|
+
|
|
116
|
+
headers: dict[str, str] = {}
|
|
117
|
+
if self._auth_header:
|
|
118
|
+
headers["Authorization"] = self._auth_header
|
|
119
|
+
|
|
120
|
+
# Try /.well-known/agent.json (A2A-style)
|
|
121
|
+
for probe_path in ["/tools", "/capabilities", "/.well-known/agent.json"]:
|
|
122
|
+
try:
|
|
123
|
+
url = self._endpoint.rstrip("/") + probe_path
|
|
124
|
+
resp = httpx.get(url, headers=headers, timeout=5.0)
|
|
125
|
+
if resp.status_code == 200:
|
|
126
|
+
data = resp.json()
|
|
127
|
+
self._parse_tool_list(data)
|
|
128
|
+
break
|
|
129
|
+
except Exception:
|
|
130
|
+
continue
|
|
131
|
+
except ImportError:
|
|
132
|
+
pass # httpx not available
|
|
133
|
+
|
|
134
|
+
def _probe_mcp(self, source: str) -> None:
|
|
135
|
+
"""Attempt to probe an MCP server for tool definitions."""
|
|
136
|
+
# MCP probing would require an MCP client library; stub with no-op
|
|
137
|
+
self._discovered_tools = []
|
|
138
|
+
|
|
139
|
+
def _parse_tool_list(self, data: Any) -> None:
|
|
140
|
+
"""Parse a JSON tool list into discovered_tools."""
|
|
141
|
+
if isinstance(data, list):
|
|
142
|
+
for item in data:
|
|
143
|
+
if isinstance(item, dict):
|
|
144
|
+
self._discovered_tools.append({
|
|
145
|
+
"id": _to_kebab(item.get("name", "tool")),
|
|
146
|
+
"name": item.get("name", "Tool"),
|
|
147
|
+
"description": item.get("description", ""),
|
|
148
|
+
})
|
|
149
|
+
elif isinstance(data, dict):
|
|
150
|
+
tools = data.get("tools") or data.get("capabilities") or []
|
|
151
|
+
self._parse_tool_list(tools)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _to_kebab(name: str) -> str:
|
|
155
|
+
return name.replace("_", "-").replace(" ", "-").lower()
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _source_to_name(source: str) -> str:
|
|
159
|
+
"""Derive a short agent name from the source string."""
|
|
160
|
+
if "/" in source or "\\" in source:
|
|
161
|
+
return Path(source).stem.replace("_", "-")
|
|
162
|
+
return source.split(".")[-1].replace("_", "-")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
AdapterRegistry.register("generic", GenericAdapter)
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""LangChain framework adapter.
|
|
2
|
+
|
|
3
|
+
Connects to a LangChain agent (AgentExecutor or LangGraph) and extracts
|
|
4
|
+
a partial AgentSpec, including detected tools and system prompts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from agentspec.adapters import AgentAdapter, AdapterRegistry
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class LangChainAdapter(AgentAdapter):
|
|
15
|
+
"""Adapter for LangChain agents (AgentExecutor, LangGraph chains).
|
|
16
|
+
|
|
17
|
+
Supports direct Python import connection. Extracts tools from the
|
|
18
|
+
agent's tool list and system prompt from the model's messages.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name = "langchain"
|
|
22
|
+
|
|
23
|
+
def __init__(self) -> None:
|
|
24
|
+
self._agent: Any = None
|
|
25
|
+
self._tools: list[dict[str, Any]] = []
|
|
26
|
+
self._system_prompt: str | None = None
|
|
27
|
+
self._model: str | None = None
|
|
28
|
+
|
|
29
|
+
def connect(self, source: str, **kwargs: Any) -> None:
|
|
30
|
+
"""Connect to a LangChain agent by importing its module.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
source: Python module path to the agent (e.g., "my_agent.agent").
|
|
34
|
+
The module must expose an ``agent`` attribute.
|
|
35
|
+
"""
|
|
36
|
+
import importlib
|
|
37
|
+
|
|
38
|
+
try:
|
|
39
|
+
module = importlib.import_module(source)
|
|
40
|
+
self._agent = getattr(module, "agent", None) or getattr(module, "executor", None)
|
|
41
|
+
except ImportError as exc:
|
|
42
|
+
raise ConnectionError(f"Cannot import LangChain agent from '{source}': {exc}") from exc
|
|
43
|
+
|
|
44
|
+
if self._agent is not None:
|
|
45
|
+
self._extract_tools()
|
|
46
|
+
self._extract_model()
|
|
47
|
+
|
|
48
|
+
def disconnect(self) -> None:
|
|
49
|
+
"""Release the reference to the connected agent."""
|
|
50
|
+
self._agent = None
|
|
51
|
+
|
|
52
|
+
def extract_spec(self) -> dict[str, Any]:
|
|
53
|
+
"""Extract a partial AgentSpec from the connected agent.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
Partial spec dict with detected tools, capabilities, and agent type.
|
|
57
|
+
"""
|
|
58
|
+
capabilities = [
|
|
59
|
+
{
|
|
60
|
+
"id": t["id"],
|
|
61
|
+
"name": t["name"],
|
|
62
|
+
"description": t.get("description", ""),
|
|
63
|
+
"category": "tool-use",
|
|
64
|
+
}
|
|
65
|
+
for t in self._tools
|
|
66
|
+
]
|
|
67
|
+
tools = [
|
|
68
|
+
{
|
|
69
|
+
"id": t["id"],
|
|
70
|
+
"name": t["name"],
|
|
71
|
+
"protocol": "custom",
|
|
72
|
+
"permissions": ["read"],
|
|
73
|
+
}
|
|
74
|
+
for t in self._tools
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
spec: dict[str, Any] = {
|
|
78
|
+
"agentspec": "0.1.0",
|
|
79
|
+
"info": {"name": "langchain-agent", "version": "0.1.0"},
|
|
80
|
+
"agent": {
|
|
81
|
+
"type": "autonomous",
|
|
82
|
+
"runtime": {
|
|
83
|
+
"framework": "langchain",
|
|
84
|
+
"model": self._model or "unknown",
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"capabilities": capabilities,
|
|
88
|
+
"boundaries": [],
|
|
89
|
+
"guardrails": {"behavioral_guardrails": {}},
|
|
90
|
+
"failure_modes": [],
|
|
91
|
+
}
|
|
92
|
+
if tools:
|
|
93
|
+
spec["tools"] = tools
|
|
94
|
+
return spec
|
|
95
|
+
|
|
96
|
+
def _extract_tools(self) -> None:
|
|
97
|
+
"""Pull tool definitions from the agent."""
|
|
98
|
+
if self._agent is None:
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
raw_tools = getattr(self._agent, "tools", None)
|
|
102
|
+
if not raw_tools:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
for tool in raw_tools:
|
|
106
|
+
self._tools.append({
|
|
107
|
+
"id": _to_kebab(getattr(tool, "name", "unknown-tool")),
|
|
108
|
+
"name": getattr(tool, "name", "Unknown"),
|
|
109
|
+
"description": getattr(tool, "description", ""),
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
def _extract_model(self) -> None:
|
|
113
|
+
"""Try to detect the underlying model name."""
|
|
114
|
+
if self._agent is None:
|
|
115
|
+
return
|
|
116
|
+
llm = getattr(self._agent, "llm", None) or getattr(self._agent, "llm_chain", None)
|
|
117
|
+
if llm:
|
|
118
|
+
model_name = (
|
|
119
|
+
getattr(llm, "model_name", None)
|
|
120
|
+
or getattr(llm, "model", None)
|
|
121
|
+
)
|
|
122
|
+
if model_name:
|
|
123
|
+
self._model = str(model_name)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _to_kebab(name: str) -> str:
|
|
127
|
+
"""Convert a snake_case or space name to kebab-case."""
|
|
128
|
+
return name.replace("_", "-").replace(" ", "-").lower()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
AdapterRegistry.register("langchain", LangChainAdapter)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""OpenAI Agents (Responses API) adapter.
|
|
2
|
+
|
|
3
|
+
Maps OpenAI function definitions and instructions to AgentSpec sections.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agentspec.adapters import AgentAdapter, AdapterRegistry
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class OpenAIAgentsAdapter(AgentAdapter):
|
|
14
|
+
"""Adapter for OpenAI Agents SDK / Responses API.
|
|
15
|
+
|
|
16
|
+
Extracts function tools and system instructions to generate a partial spec.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
name = "openai"
|
|
20
|
+
|
|
21
|
+
def __init__(self) -> None:
|
|
22
|
+
self._agent: Any = None
|
|
23
|
+
self._functions: list[dict[str, Any]] = []
|
|
24
|
+
self._instructions: str | None = None
|
|
25
|
+
self._model: str | None = None
|
|
26
|
+
|
|
27
|
+
def connect(self, source: str, **kwargs: Any) -> None:
|
|
28
|
+
"""Connect to an OpenAI agent by importing its module.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
source: Python module path to the agent.
|
|
32
|
+
The module must expose an ``agent`` attribute.
|
|
33
|
+
"""
|
|
34
|
+
import importlib
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
module = importlib.import_module(source)
|
|
38
|
+
self._agent = getattr(module, "agent", None) or getattr(module, "assistant", None)
|
|
39
|
+
except ImportError as exc:
|
|
40
|
+
raise ConnectionError(f"Cannot import OpenAI agent from '{source}': {exc}") from exc
|
|
41
|
+
|
|
42
|
+
if self._agent is not None:
|
|
43
|
+
self._extract_functions()
|
|
44
|
+
self._instructions = getattr(self._agent, "instructions", None)
|
|
45
|
+
self._model = getattr(self._agent, "model", None)
|
|
46
|
+
|
|
47
|
+
def disconnect(self) -> None:
|
|
48
|
+
self._agent = None
|
|
49
|
+
|
|
50
|
+
def extract_spec(self) -> dict[str, Any]:
|
|
51
|
+
"""Extract a partial AgentSpec from the OpenAI agent."""
|
|
52
|
+
capabilities = [
|
|
53
|
+
{
|
|
54
|
+
"id": _to_kebab(fn.get("name", f"fn-{i}")),
|
|
55
|
+
"name": fn.get("name", f"Function {i}"),
|
|
56
|
+
"description": fn.get("description", ""),
|
|
57
|
+
"category": "tool-use",
|
|
58
|
+
}
|
|
59
|
+
for i, fn in enumerate(self._functions)
|
|
60
|
+
]
|
|
61
|
+
tools = [
|
|
62
|
+
{
|
|
63
|
+
"id": _to_kebab(fn.get("name", f"fn-{i}")),
|
|
64
|
+
"name": fn.get("name", f"Function {i}"),
|
|
65
|
+
"protocol": "custom",
|
|
66
|
+
"permissions": ["execute"],
|
|
67
|
+
}
|
|
68
|
+
for i, fn in enumerate(self._functions)
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
spec: dict[str, Any] = {
|
|
72
|
+
"agentspec": "0.1.0",
|
|
73
|
+
"info": {"name": "openai-agent", "version": "0.1.0"},
|
|
74
|
+
"agent": {
|
|
75
|
+
"type": "tool-agent",
|
|
76
|
+
"runtime": {
|
|
77
|
+
"framework": "openai",
|
|
78
|
+
"model": self._model or "gpt-4o",
|
|
79
|
+
"model_provider": "openai",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
"capabilities": capabilities,
|
|
83
|
+
"boundaries": [],
|
|
84
|
+
"guardrails": {"behavioral_guardrails": {}},
|
|
85
|
+
"failure_modes": [],
|
|
86
|
+
}
|
|
87
|
+
if tools:
|
|
88
|
+
spec["tools"] = tools
|
|
89
|
+
return spec
|
|
90
|
+
|
|
91
|
+
def _extract_functions(self) -> None:
|
|
92
|
+
if self._agent is None:
|
|
93
|
+
return
|
|
94
|
+
# OpenAI SDK stores tools as list of dicts with "type"/"function" or direct callables
|
|
95
|
+
raw = getattr(self._agent, "tools", None) or getattr(self._agent, "functions", None) or []
|
|
96
|
+
for item in raw:
|
|
97
|
+
if isinstance(item, dict):
|
|
98
|
+
fn = item.get("function") or item
|
|
99
|
+
self._functions.append(fn)
|
|
100
|
+
elif callable(item):
|
|
101
|
+
self._functions.append({
|
|
102
|
+
"name": getattr(item, "__name__", "unknown"),
|
|
103
|
+
"description": getattr(item, "__doc__", "") or "",
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _to_kebab(name: str) -> str:
|
|
108
|
+
return name.replace("_", "-").replace(" ", "-").lower()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
AdapterRegistry.register("openai", OpenAIAgentsAdapter)
|