basedagents 0.2.0__tar.gz → 0.3.0__tar.gz
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.
- {basedagents-0.2.0 → basedagents-0.3.0}/PKG-INFO +1 -1
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/__init__.py +6 -1
- basedagents-0.3.0/basedagents/integrations/__init__.py +8 -0
- basedagents-0.3.0/basedagents/integrations/autogen.py +224 -0
- basedagents-0.3.0/basedagents/integrations/crewai.py +251 -0
- basedagents-0.3.0/basedagents/middleware.py +316 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/PKG-INFO +1 -1
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/SOURCES.txt +3 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/pyproject.toml +1 -1
- basedagents-0.2.0/basedagents/integrations/__init__.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/README.md +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/auth.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/cli.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/client.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/easy.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/integrations/langchain.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/keypair.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents/pow.py +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/dependency_links.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/entry_points.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/requires.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/basedagents.egg-info/top_level.txt +0 -0
- {basedagents-0.2.0 → basedagents-0.3.0}/setup.cfg +0 -0
|
@@ -33,10 +33,15 @@ from .keypair import AgentKeypair, generate as generate_keypair, from_private_ke
|
|
|
33
33
|
from .client import RegistryClient, BasedAgentsError
|
|
34
34
|
from .auth import build_headers as build_auth_headers
|
|
35
35
|
from .easy import register_or_load
|
|
36
|
+
from .middleware import require_agent, verify_request, fetch_attestation, VerifiedAgent
|
|
36
37
|
|
|
37
|
-
__version__ = "0.
|
|
38
|
+
__version__ = "0.3.0"
|
|
38
39
|
__all__ = [
|
|
39
40
|
"register_or_load",
|
|
41
|
+
"require_agent",
|
|
42
|
+
"verify_request",
|
|
43
|
+
"fetch_attestation",
|
|
44
|
+
"VerifiedAgent",
|
|
40
45
|
"AgentKeypair",
|
|
41
46
|
"RegistryClient",
|
|
42
47
|
"BasedAgentsError",
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AutoGen integration for basedagents.
|
|
3
|
+
|
|
4
|
+
Introspects an AutoGen ConversableAgent, AssistantAgent, or GroupChat and
|
|
5
|
+
auto-populates capabilities and skills for registration.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from autogen import AssistantAgent, GroupChat
|
|
9
|
+
from basedagents.integrations.autogen import register_autogen_agent
|
|
10
|
+
|
|
11
|
+
# Single agent
|
|
12
|
+
agent_id = register_autogen_agent(
|
|
13
|
+
assistant,
|
|
14
|
+
name="my-autogen-assistant",
|
|
15
|
+
description="Writes and executes Python code.",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# GroupChat (multi-agent)
|
|
19
|
+
agent_id = register_autogen_agent(
|
|
20
|
+
groupchat,
|
|
21
|
+
name="my-autogen-group",
|
|
22
|
+
description="Research + coding multi-agent system.",
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Known AutoGen agent class names → capabilities ────────────────────────────
|
|
31
|
+
_AGENT_CLASS_CAPABILITIES: dict[str, list[str]] = {
|
|
32
|
+
"AssistantAgent": ["reasoning", "code"],
|
|
33
|
+
"UserProxyAgent": ["code"], # executes code by default
|
|
34
|
+
"GPTAssistantAgent": ["reasoning", "code"],
|
|
35
|
+
"RetrieveAssistantAgent": ["reasoning", "knowledge"],
|
|
36
|
+
"RetrieveUserProxyAgent": ["knowledge"],
|
|
37
|
+
"MathUserProxyAgent": ["reasoning"],
|
|
38
|
+
"TeachableAgent": ["reasoning", "knowledge"],
|
|
39
|
+
"CompressibleAgent": ["reasoning"],
|
|
40
|
+
"TransformMessages": ["reasoning"],
|
|
41
|
+
"WebSurferAgent": ["web-search", "web-scraping"],
|
|
42
|
+
"MultimodalConversableAgent": ["vision", "reasoning"],
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# ── Code execution detection ──────────────────────────────────────────────────
|
|
46
|
+
_CODE_EXEC_CLASSES = {"UserProxyAgent", "GPTAssistantAgent"}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _collect_agents(agent_or_group: Any) -> list[Any]:
|
|
50
|
+
"""Extract agents from GroupChat, GroupChatManager, or return single agent."""
|
|
51
|
+
# GroupChat has .agents
|
|
52
|
+
agents = getattr(agent_or_group, "agents", None)
|
|
53
|
+
if agents:
|
|
54
|
+
return list(agents)
|
|
55
|
+
# GroupChatManager has .groupchat.agents
|
|
56
|
+
gc = getattr(agent_or_group, "groupchat", None)
|
|
57
|
+
if gc:
|
|
58
|
+
return list(getattr(gc, "agents", []) or [])
|
|
59
|
+
# Single agent
|
|
60
|
+
return [agent_or_group]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _is_multi_agent(agent_or_group: Any) -> bool:
|
|
64
|
+
return len(_collect_agents(agent_or_group)) > 1
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _agent_executes_code(agent: Any) -> bool:
|
|
68
|
+
"""Check if an agent is configured to execute code."""
|
|
69
|
+
cls = type(agent).__name__
|
|
70
|
+
if cls in _CODE_EXEC_CLASSES:
|
|
71
|
+
return True
|
|
72
|
+
# human_input_mode="NEVER" + code_execution_config set = code executor
|
|
73
|
+
code_cfg = getattr(agent, "code_execution_config", None)
|
|
74
|
+
if code_cfg and code_cfg is not False:
|
|
75
|
+
return True
|
|
76
|
+
return False
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _detect_llm_skill(agent: Any) -> str | None:
|
|
80
|
+
"""Try to detect LLM provider package from llm_config."""
|
|
81
|
+
llm_config = getattr(agent, "llm_config", None) or {}
|
|
82
|
+
if not isinstance(llm_config, dict):
|
|
83
|
+
return None
|
|
84
|
+
model = llm_config.get("model", "") or ""
|
|
85
|
+
config_list = llm_config.get("config_list", [{}])
|
|
86
|
+
if config_list:
|
|
87
|
+
model = model or config_list[0].get("model", "")
|
|
88
|
+
model = model.lower()
|
|
89
|
+
if "gpt" in model or "o1" in model or "o3" in model:
|
|
90
|
+
return "pyautogen"
|
|
91
|
+
if "claude" in model:
|
|
92
|
+
return "pyautogen"
|
|
93
|
+
if "gemini" in model:
|
|
94
|
+
return "pyautogen"
|
|
95
|
+
return "pyautogen" # always include base pyautogen
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def extract_profile(
|
|
99
|
+
agent_or_group: Any,
|
|
100
|
+
extra_capabilities: list[str] | None = None,
|
|
101
|
+
extra_skills: list[dict[str, str]] | None = None,
|
|
102
|
+
) -> dict[str, Any]:
|
|
103
|
+
"""
|
|
104
|
+
Introspect an AutoGen agent or GroupChat and return a partial profile dict
|
|
105
|
+
with auto-detected capabilities and skills.
|
|
106
|
+
"""
|
|
107
|
+
agents = _collect_agents(agent_or_group)
|
|
108
|
+
capabilities: set[str] = set(extra_capabilities or [])
|
|
109
|
+
skills_seen: set[str] = set()
|
|
110
|
+
skills: list[dict[str, str]] = list(extra_skills or [])
|
|
111
|
+
|
|
112
|
+
# Base skill
|
|
113
|
+
skills.append({"name": "pyautogen", "registry": "pypi"})
|
|
114
|
+
skills_seen.add("pyautogen")
|
|
115
|
+
|
|
116
|
+
if _is_multi_agent(agent_or_group):
|
|
117
|
+
capabilities.add("multi-agent")
|
|
118
|
+
|
|
119
|
+
for agent in agents:
|
|
120
|
+
cls_name = type(agent).__name__
|
|
121
|
+
|
|
122
|
+
for cap in _AGENT_CLASS_CAPABILITIES.get(cls_name, []):
|
|
123
|
+
capabilities.add(cap)
|
|
124
|
+
|
|
125
|
+
if _agent_executes_code(agent):
|
|
126
|
+
capabilities.add("code")
|
|
127
|
+
|
|
128
|
+
# Detect function/tool calling
|
|
129
|
+
fn_map = getattr(agent, "function_map", None) or {}
|
|
130
|
+
if fn_map:
|
|
131
|
+
capabilities.add("tool-use")
|
|
132
|
+
|
|
133
|
+
# Detect retrieval augmentation
|
|
134
|
+
retrieve_config = getattr(agent, "retrieve_config", None)
|
|
135
|
+
if retrieve_config:
|
|
136
|
+
capabilities.add("knowledge")
|
|
137
|
+
|
|
138
|
+
# Extra skill from LLM config
|
|
139
|
+
pkg = _detect_llm_skill(agent)
|
|
140
|
+
if pkg and pkg not in skills_seen:
|
|
141
|
+
skills.append({"name": pkg, "registry": "pypi"})
|
|
142
|
+
skills_seen.add(pkg)
|
|
143
|
+
|
|
144
|
+
# Fallback capability
|
|
145
|
+
if not capabilities:
|
|
146
|
+
capabilities.add("reasoning")
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
"capabilities": sorted(capabilities),
|
|
150
|
+
"protocols": ["https"],
|
|
151
|
+
"skills": skills,
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def register_autogen_agent(
|
|
156
|
+
agent_or_group: Any,
|
|
157
|
+
name: str,
|
|
158
|
+
description: str = "",
|
|
159
|
+
contact_endpoint: str | None = None,
|
|
160
|
+
organization: str | None = None,
|
|
161
|
+
version: str | None = None,
|
|
162
|
+
tags: list[str] | None = None,
|
|
163
|
+
extra_capabilities: list[str] | None = None,
|
|
164
|
+
extra_skills: list[dict[str, str]] | None = None,
|
|
165
|
+
keypair_path: str | None = None,
|
|
166
|
+
api_url: str | None = None,
|
|
167
|
+
verbose: bool = True,
|
|
168
|
+
) -> str:
|
|
169
|
+
"""
|
|
170
|
+
Register an AutoGen agent or GroupChat with basedagents.ai.
|
|
171
|
+
|
|
172
|
+
Introspects the agent to auto-detect capabilities and skills.
|
|
173
|
+
Idempotent — safe to call on every startup.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
agent_or_group: AutoGen ConversableAgent, AssistantAgent, UserProxyAgent,
|
|
177
|
+
GroupChat, or GroupChatManager
|
|
178
|
+
name: Unique agent name (globally unique on registry)
|
|
179
|
+
description: What this agent/group does
|
|
180
|
+
contact_endpoint: URL where the agent can be reached for verification
|
|
181
|
+
organization: Optional org name
|
|
182
|
+
version: Optional version string
|
|
183
|
+
tags: Optional extra tags
|
|
184
|
+
extra_capabilities: Additional capabilities beyond auto-detected ones
|
|
185
|
+
extra_skills: Additional skills beyond auto-detected ones
|
|
186
|
+
keypair_path: Override keypair file location
|
|
187
|
+
api_url: Override API URL (defaults to BASEDAGENTS_API env or prod)
|
|
188
|
+
verbose: Print progress (default True)
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
agent_id string
|
|
192
|
+
|
|
193
|
+
Example:
|
|
194
|
+
import autogen
|
|
195
|
+
from basedagents.integrations.autogen import register_autogen_agent
|
|
196
|
+
|
|
197
|
+
assistant = autogen.AssistantAgent("assistant", llm_config={"model": "gpt-4o"})
|
|
198
|
+
user_proxy = autogen.UserProxyAgent("user_proxy", code_execution_config={"work_dir": "."})
|
|
199
|
+
|
|
200
|
+
agent_id = register_autogen_agent(
|
|
201
|
+
assistant,
|
|
202
|
+
name="my-autogen-assistant",
|
|
203
|
+
description="Writes and debugs Python code via GPT-4o.",
|
|
204
|
+
)
|
|
205
|
+
"""
|
|
206
|
+
from ..easy import register_or_load
|
|
207
|
+
|
|
208
|
+
profile = extract_profile(agent_or_group, extra_capabilities, extra_skills)
|
|
209
|
+
merged_tags = list(set(["autogen"] + (tags or [])))
|
|
210
|
+
|
|
211
|
+
return register_or_load(
|
|
212
|
+
name=name,
|
|
213
|
+
description=description,
|
|
214
|
+
capabilities=profile["capabilities"],
|
|
215
|
+
protocols=profile["protocols"],
|
|
216
|
+
skills=profile["skills"],
|
|
217
|
+
contact_endpoint=contact_endpoint,
|
|
218
|
+
organization=organization,
|
|
219
|
+
version=version,
|
|
220
|
+
tags=merged_tags,
|
|
221
|
+
keypair_path=keypair_path,
|
|
222
|
+
api_url=api_url,
|
|
223
|
+
verbose=verbose,
|
|
224
|
+
)
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrewAI integration for basedagents.
|
|
3
|
+
|
|
4
|
+
Introspects a CrewAI Crew or Agent and auto-populates capabilities and skills
|
|
5
|
+
for registration.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from crewai import Crew, Agent
|
|
9
|
+
from basedagents.integrations.crewai import register_crewai_agent
|
|
10
|
+
|
|
11
|
+
# Register from a Crew (uses all agents' tools)
|
|
12
|
+
agent_id = register_crewai_agent(
|
|
13
|
+
crew,
|
|
14
|
+
name="my-research-crew",
|
|
15
|
+
description="Multi-agent crew for research and analysis.",
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
# Or register a single CrewAI Agent
|
|
19
|
+
agent_id = register_crewai_agent(
|
|
20
|
+
agent,
|
|
21
|
+
name="my-researcher",
|
|
22
|
+
description="Searches and summarises the web.",
|
|
23
|
+
)
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from typing import Any
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── Tool name → capabilities mapping ─────────────────────────────────────────
|
|
31
|
+
_TOOL_TO_CAPABILITIES: dict[str, list[str]] = {
|
|
32
|
+
# Search
|
|
33
|
+
"SerperDevTool": ["web-search"],
|
|
34
|
+
"TavilySearchTool": ["web-search"],
|
|
35
|
+
"EXASearchTool": ["web-search"],
|
|
36
|
+
"BraveSearchTool": ["web-search"],
|
|
37
|
+
"DuckDuckGoSearchTool": ["web-search"],
|
|
38
|
+
"GoogleSearchTool": ["web-search"],
|
|
39
|
+
"ScrapeWebsiteTool": ["web-scraping"],
|
|
40
|
+
"SeleniumScrapingTool": ["web-scraping"],
|
|
41
|
+
"ScrapeElementFromWebsiteTool": ["web-scraping"],
|
|
42
|
+
"WebsiteSearchTool": ["web-search", "web-scraping"],
|
|
43
|
+
# Code
|
|
44
|
+
"CodeDocsSearchTool": ["code", "knowledge"],
|
|
45
|
+
"CodeInterpreterTool": ["code"],
|
|
46
|
+
"GithubSearchTool": ["code", "web-search"],
|
|
47
|
+
# Files
|
|
48
|
+
"FileReadTool": ["file-access"],
|
|
49
|
+
"FileWriterTool": ["file-access"],
|
|
50
|
+
"DirectoryReadTool": ["file-access"],
|
|
51
|
+
"DirectorySearchTool": ["file-access"],
|
|
52
|
+
"PDFSearchTool": ["file-access", "knowledge"],
|
|
53
|
+
"DOCXSearchTool": ["file-access", "knowledge"],
|
|
54
|
+
"CSVSearchTool": ["file-access", "data-analysis"],
|
|
55
|
+
"JSONSearchTool": ["file-access"],
|
|
56
|
+
"TXTSearchTool": ["file-access"],
|
|
57
|
+
"XMLSearchTool": ["file-access"],
|
|
58
|
+
"SpreadsheetSearchTool": ["file-access", "data-analysis"],
|
|
59
|
+
# Data
|
|
60
|
+
"PGSearchTool": ["data-analysis", "sql"],
|
|
61
|
+
"MySQLSearchTool": ["data-analysis", "sql"],
|
|
62
|
+
"NL2SQLTool": ["data-analysis", "sql"],
|
|
63
|
+
# Knowledge / RAG
|
|
64
|
+
"RagTool": ["knowledge"],
|
|
65
|
+
"YoutubeVideoSearchTool": ["knowledge"],
|
|
66
|
+
"YoutubeChannelSearchTool": ["knowledge"],
|
|
67
|
+
"MDXSearchTool": ["knowledge"],
|
|
68
|
+
# Comms
|
|
69
|
+
"BrowserbaseTool": ["web-scraping"],
|
|
70
|
+
"MultiOnTool": ["web-scraping"],
|
|
71
|
+
# Vision
|
|
72
|
+
"VisionTool": ["vision"],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# ── Tool name → PyPI package ──────────────────────────────────────────────────
|
|
76
|
+
_TOOL_TO_PYPI: dict[str, str] = {
|
|
77
|
+
"SerperDevTool": "crewai-tools",
|
|
78
|
+
"TavilySearchTool": "crewai-tools",
|
|
79
|
+
"EXASearchTool": "crewai-tools",
|
|
80
|
+
"BraveSearchTool": "crewai-tools",
|
|
81
|
+
"DuckDuckGoSearchTool": "crewai-tools",
|
|
82
|
+
"ScrapeWebsiteTool": "crewai-tools",
|
|
83
|
+
"SeleniumScrapingTool": "crewai-tools",
|
|
84
|
+
"WebsiteSearchTool": "crewai-tools",
|
|
85
|
+
"CodeDocsSearchTool": "crewai-tools",
|
|
86
|
+
"CodeInterpreterTool": "crewai-tools",
|
|
87
|
+
"GithubSearchTool": "crewai-tools",
|
|
88
|
+
"FileReadTool": "crewai-tools",
|
|
89
|
+
"FileWriterTool": "crewai-tools",
|
|
90
|
+
"DirectoryReadTool": "crewai-tools",
|
|
91
|
+
"PDFSearchTool": "crewai-tools",
|
|
92
|
+
"DOCXSearchTool": "crewai-tools",
|
|
93
|
+
"CSVSearchTool": "crewai-tools",
|
|
94
|
+
"PGSearchTool": "crewai-tools",
|
|
95
|
+
"NL2SQLTool": "crewai-tools",
|
|
96
|
+
"RagTool": "crewai-tools",
|
|
97
|
+
"YoutubeVideoSearchTool": "crewai-tools",
|
|
98
|
+
"VisionTool": "crewai-tools",
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _collect_tools(crew_or_agent: Any) -> list[Any]:
|
|
103
|
+
"""Extract all tools from a Crew or single Agent."""
|
|
104
|
+
tools: list[Any] = []
|
|
105
|
+
|
|
106
|
+
# Single Agent: has .tools
|
|
107
|
+
if hasattr(crew_or_agent, "tools") and not hasattr(crew_or_agent, "agents"):
|
|
108
|
+
return list(getattr(crew_or_agent, "tools", []) or [])
|
|
109
|
+
|
|
110
|
+
# Crew: has .agents, each with .tools
|
|
111
|
+
agents = getattr(crew_or_agent, "agents", []) or []
|
|
112
|
+
for agent in agents:
|
|
113
|
+
tools.extend(list(getattr(agent, "tools", []) or []))
|
|
114
|
+
|
|
115
|
+
return tools
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _is_multi_agent(crew_or_agent: Any) -> bool:
|
|
119
|
+
agents = getattr(crew_or_agent, "agents", None)
|
|
120
|
+
return bool(agents and len(agents) > 1)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def extract_profile(
|
|
124
|
+
crew_or_agent: Any,
|
|
125
|
+
extra_capabilities: list[str] | None = None,
|
|
126
|
+
extra_skills: list[dict[str, str]] | None = None,
|
|
127
|
+
) -> dict[str, Any]:
|
|
128
|
+
"""
|
|
129
|
+
Introspect a CrewAI Crew or Agent and return a partial profile dict
|
|
130
|
+
with auto-detected capabilities and skills.
|
|
131
|
+
"""
|
|
132
|
+
tools = _collect_tools(crew_or_agent)
|
|
133
|
+
capabilities: set[str] = set(extra_capabilities or [])
|
|
134
|
+
skills_seen: set[str] = set()
|
|
135
|
+
skills: list[dict[str, str]] = list(extra_skills or [])
|
|
136
|
+
|
|
137
|
+
# Always add crewai base skill
|
|
138
|
+
skills.append({"name": "crewai", "registry": "pypi"})
|
|
139
|
+
skills_seen.add("crewai")
|
|
140
|
+
|
|
141
|
+
# Multi-agent crews get the orchestration capability
|
|
142
|
+
if _is_multi_agent(crew_or_agent):
|
|
143
|
+
capabilities.add("multi-agent")
|
|
144
|
+
|
|
145
|
+
for tool in tools:
|
|
146
|
+
cls_name = type(tool).__name__
|
|
147
|
+
tool_name = getattr(tool, "name", cls_name)
|
|
148
|
+
|
|
149
|
+
for key in (cls_name, tool_name):
|
|
150
|
+
for cap in _TOOL_TO_CAPABILITIES.get(key, []):
|
|
151
|
+
capabilities.add(cap)
|
|
152
|
+
|
|
153
|
+
pkg = _TOOL_TO_PYPI.get(cls_name) or _TOOL_TO_PYPI.get(tool_name)
|
|
154
|
+
if pkg and pkg not in skills_seen:
|
|
155
|
+
skills.append({"name": pkg, "registry": "pypi"})
|
|
156
|
+
skills_seen.add(pkg)
|
|
157
|
+
|
|
158
|
+
# Detect LLM provider
|
|
159
|
+
llm = getattr(crew_or_agent, "llm", None)
|
|
160
|
+
if llm is None:
|
|
161
|
+
# Try first agent's LLM
|
|
162
|
+
agents = getattr(crew_or_agent, "agents", []) or []
|
|
163
|
+
if agents:
|
|
164
|
+
llm = getattr(agents[0], "llm", None)
|
|
165
|
+
if llm:
|
|
166
|
+
cls_name = type(llm).__name__
|
|
167
|
+
_LLM_TO_PYPI = {
|
|
168
|
+
"ChatOpenAI": "langchain-openai",
|
|
169
|
+
"ChatAnthropic": "langchain-anthropic",
|
|
170
|
+
"ChatGoogleGenerativeAI": "langchain-google-genai",
|
|
171
|
+
"ChatGroq": "langchain-groq",
|
|
172
|
+
}
|
|
173
|
+
pkg = _LLM_TO_PYPI.get(cls_name)
|
|
174
|
+
if pkg and pkg not in skills_seen:
|
|
175
|
+
skills.append({"name": pkg, "registry": "pypi"})
|
|
176
|
+
skills_seen.add(pkg)
|
|
177
|
+
|
|
178
|
+
return {
|
|
179
|
+
"capabilities": sorted(capabilities) if capabilities else ["reasoning"],
|
|
180
|
+
"protocols": ["https"],
|
|
181
|
+
"skills": skills,
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def register_crewai_agent(
|
|
186
|
+
crew_or_agent: Any,
|
|
187
|
+
name: str,
|
|
188
|
+
description: str = "",
|
|
189
|
+
contact_endpoint: str | None = None,
|
|
190
|
+
organization: str | None = None,
|
|
191
|
+
version: str | None = None,
|
|
192
|
+
tags: list[str] | None = None,
|
|
193
|
+
extra_capabilities: list[str] | None = None,
|
|
194
|
+
extra_skills: list[dict[str, str]] | None = None,
|
|
195
|
+
keypair_path: str | None = None,
|
|
196
|
+
api_url: str | None = None,
|
|
197
|
+
verbose: bool = True,
|
|
198
|
+
) -> str:
|
|
199
|
+
"""
|
|
200
|
+
Register a CrewAI Crew or Agent with basedagents.ai.
|
|
201
|
+
|
|
202
|
+
Introspects tools to auto-detect capabilities and skills.
|
|
203
|
+
Idempotent — safe to call on every startup.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
crew_or_agent: CrewAI Crew or Agent instance
|
|
207
|
+
name: Unique agent name (globally unique on registry)
|
|
208
|
+
description: What this crew/agent does
|
|
209
|
+
contact_endpoint: URL where the agent can be reached for verification
|
|
210
|
+
organization: Optional org name
|
|
211
|
+
version: Optional version string
|
|
212
|
+
tags: Optional extra tags
|
|
213
|
+
extra_capabilities: Additional capabilities beyond auto-detected ones
|
|
214
|
+
extra_skills: Additional skills beyond auto-detected ones
|
|
215
|
+
keypair_path: Override keypair file location
|
|
216
|
+
api_url: Override API URL (defaults to BASEDAGENTS_API env or prod)
|
|
217
|
+
verbose: Print progress (default True)
|
|
218
|
+
|
|
219
|
+
Returns:
|
|
220
|
+
agent_id string
|
|
221
|
+
|
|
222
|
+
Example:
|
|
223
|
+
from crewai import Crew
|
|
224
|
+
from basedagents.integrations.crewai import register_crewai_agent
|
|
225
|
+
|
|
226
|
+
crew = Crew(agents=[researcher, writer], tasks=[...])
|
|
227
|
+
agent_id = register_crewai_agent(
|
|
228
|
+
crew,
|
|
229
|
+
name="my-research-crew",
|
|
230
|
+
description="Research and write blog posts.",
|
|
231
|
+
)
|
|
232
|
+
"""
|
|
233
|
+
from ..easy import register_or_load
|
|
234
|
+
|
|
235
|
+
profile = extract_profile(crew_or_agent, extra_capabilities, extra_skills)
|
|
236
|
+
merged_tags = list(set(["crewai"] + (tags or [])))
|
|
237
|
+
|
|
238
|
+
return register_or_load(
|
|
239
|
+
name=name,
|
|
240
|
+
description=description,
|
|
241
|
+
capabilities=profile["capabilities"],
|
|
242
|
+
protocols=profile["protocols"],
|
|
243
|
+
skills=profile["skills"],
|
|
244
|
+
contact_endpoint=contact_endpoint,
|
|
245
|
+
organization=organization,
|
|
246
|
+
version=version,
|
|
247
|
+
tags=merged_tags,
|
|
248
|
+
keypair_path=keypair_path,
|
|
249
|
+
api_url=api_url,
|
|
250
|
+
verbose=verbose,
|
|
251
|
+
)
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""
|
|
2
|
+
basedagents auth middleware.
|
|
3
|
+
|
|
4
|
+
Drop-in reputation-gated authentication for FastAPI and WSGI (Flask/Starlette) apps.
|
|
5
|
+
Agents prove identity via AgentSig header; the middleware verifies the attestation
|
|
6
|
+
offline using the basedagents registry public key.
|
|
7
|
+
|
|
8
|
+
Usage (FastAPI):
|
|
9
|
+
from basedagents.middleware import require_agent, VerifiedAgent
|
|
10
|
+
|
|
11
|
+
@app.post("/execute")
|
|
12
|
+
async def execute(request: Request, agent: VerifiedAgent = Depends(require_agent(
|
|
13
|
+
min_reputation=0.5,
|
|
14
|
+
capabilities=["code"],
|
|
15
|
+
))):
|
|
16
|
+
print(f"Request from {agent.name} (rep={agent.reputation})")
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
Usage (manual):
|
|
20
|
+
from basedagents.middleware import verify_request
|
|
21
|
+
|
|
22
|
+
agent = await verify_request(request_headers, method, path, body)
|
|
23
|
+
if agent is None:
|
|
24
|
+
raise HTTPException(status_code=403)
|
|
25
|
+
"""
|
|
26
|
+
from __future__ import annotations
|
|
27
|
+
|
|
28
|
+
import base64
|
|
29
|
+
import hashlib
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import time
|
|
33
|
+
from dataclasses import dataclass
|
|
34
|
+
from typing import Any, Callable
|
|
35
|
+
|
|
36
|
+
import httpx
|
|
37
|
+
|
|
38
|
+
from .keypair import _base58_decode as b58decode_key
|
|
39
|
+
|
|
40
|
+
# ── Registry public key (Ed25519, hex) ──────────────────────────────────────
|
|
41
|
+
# Published at https://api.basedagents.ai/v1/attestation/public-key
|
|
42
|
+
# Hardcoded here for offline verification — no API call needed at auth time.
|
|
43
|
+
REGISTRY_PUBLIC_KEY_HEX = "9827a77ffa3bbddff01444277707271838098f3e8f2d29a200054cc0bca308d0"
|
|
44
|
+
|
|
45
|
+
_DEFAULT_BASE = "https://api.basedagents.ai"
|
|
46
|
+
API_BASE = os.environ.get("BASEDAGENTS_API", _DEFAULT_BASE)
|
|
47
|
+
|
|
48
|
+
# Attestation TTL tolerance (seconds beyond stated expiry)
|
|
49
|
+
CLOCK_SKEW_TOLERANCE = 30
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@dataclass
|
|
53
|
+
class VerifiedAgent:
|
|
54
|
+
"""A fully verified, reputation-checked agent identity."""
|
|
55
|
+
agent_id: str
|
|
56
|
+
name: str
|
|
57
|
+
public_key_b58: str
|
|
58
|
+
capabilities: list[str]
|
|
59
|
+
protocols: list[str]
|
|
60
|
+
reputation: float
|
|
61
|
+
reputation_tier: str
|
|
62
|
+
verification_count: int
|
|
63
|
+
issued_at: int
|
|
64
|
+
expires_at: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class AttestationError(Exception):
|
|
68
|
+
"""Raised when attestation verification fails."""
|
|
69
|
+
pass
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# ── In-process attestation cache ────────────────────────────────────────────
|
|
73
|
+
_cache: dict[str, tuple[dict[str, Any], float]] = {} # agent_id → (attestation, fetched_at)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _cache_get(agent_id: str) -> dict[str, Any] | None:
|
|
77
|
+
entry = _cache.get(agent_id)
|
|
78
|
+
if entry is None:
|
|
79
|
+
return None
|
|
80
|
+
attestation, _ = entry
|
|
81
|
+
# Use until expires_at minus skew tolerance
|
|
82
|
+
if time.time() > attestation["expires_at"] - CLOCK_SKEW_TOLERANCE:
|
|
83
|
+
del _cache[agent_id]
|
|
84
|
+
return None
|
|
85
|
+
return attestation
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _cache_set(agent_id: str, attestation: dict[str, Any]) -> None:
|
|
89
|
+
_cache[agent_id] = (attestation, time.time())
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# ── Attestation fetch & verify ───────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def _verify_attestation_signature(attestation: dict[str, Any]) -> bool:
|
|
95
|
+
"""
|
|
96
|
+
Verify the registry's Ed25519 signature on the attestation document.
|
|
97
|
+
Uses the hardcoded REGISTRY_PUBLIC_KEY_HEX for offline verification.
|
|
98
|
+
"""
|
|
99
|
+
try:
|
|
100
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
101
|
+
from cryptography.exceptions import InvalidSignature
|
|
102
|
+
|
|
103
|
+
sig_b64 = attestation.get("signature", "")
|
|
104
|
+
sig_bytes = base64.b64decode(sig_b64)
|
|
105
|
+
|
|
106
|
+
# Reconstruct the payload that was signed (all fields except signature, sorted keys)
|
|
107
|
+
payload = {k: v for k, v in attestation.items() if k not in ("signature", "_verify")}
|
|
108
|
+
sorted_keys = sorted(payload.keys())
|
|
109
|
+
canonical = json.dumps(payload, sort_keys=True, separators=(',', ':'))
|
|
110
|
+
# Match server: JSON.stringify(payload, sortedKeys) — compact, no spaces
|
|
111
|
+
msg_bytes = canonical.encode("utf-8")
|
|
112
|
+
|
|
113
|
+
pub_key_bytes = bytes.fromhex(REGISTRY_PUBLIC_KEY_HEX)
|
|
114
|
+
pub_key = Ed25519PublicKey.from_public_bytes(pub_key_bytes)
|
|
115
|
+
pub_key.verify(sig_bytes, msg_bytes)
|
|
116
|
+
return True
|
|
117
|
+
except (InvalidSignature, Exception):
|
|
118
|
+
return False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def fetch_attestation(agent_id: str, base_url: str = API_BASE) -> dict[str, Any]:
|
|
122
|
+
"""
|
|
123
|
+
Fetch a fresh attestation from the registry and verify its signature.
|
|
124
|
+
Raises AttestationError on failure.
|
|
125
|
+
"""
|
|
126
|
+
cached = _cache_get(agent_id)
|
|
127
|
+
if cached is not None:
|
|
128
|
+
return cached
|
|
129
|
+
|
|
130
|
+
url = f"{base_url}/v1/agents/{agent_id}/attestation"
|
|
131
|
+
with httpx.Client(timeout=10.0) as client:
|
|
132
|
+
res = client.get(url)
|
|
133
|
+
|
|
134
|
+
if res.status_code == 404:
|
|
135
|
+
raise AttestationError(f"Agent {agent_id} not found in registry")
|
|
136
|
+
if res.status_code == 403:
|
|
137
|
+
raise AttestationError(f"Agent {agent_id} is suspended or revoked")
|
|
138
|
+
if not res.is_success:
|
|
139
|
+
raise AttestationError(f"Registry returned {res.status_code} for {agent_id}")
|
|
140
|
+
|
|
141
|
+
attestation = res.json()
|
|
142
|
+
|
|
143
|
+
# Verify registry signature
|
|
144
|
+
if not _verify_attestation_signature(attestation):
|
|
145
|
+
raise AttestationError("Attestation signature verification failed — possible tampering")
|
|
146
|
+
|
|
147
|
+
# Check expiry
|
|
148
|
+
now = int(time.time())
|
|
149
|
+
if now > attestation["expires_at"] + CLOCK_SKEW_TOLERANCE:
|
|
150
|
+
raise AttestationError("Attestation has expired")
|
|
151
|
+
|
|
152
|
+
_cache_set(agent_id, attestation)
|
|
153
|
+
return attestation
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def verify_request(
|
|
157
|
+
headers: dict[str, str],
|
|
158
|
+
method: str,
|
|
159
|
+
path: str,
|
|
160
|
+
body: str | bytes = "",
|
|
161
|
+
base_url: str = API_BASE,
|
|
162
|
+
) -> VerifiedAgent | None:
|
|
163
|
+
"""
|
|
164
|
+
Verify an inbound agent request:
|
|
165
|
+
1. Parse X-Agent-ID and Authorization: AgentSig headers
|
|
166
|
+
2. Verify the request signature (agent signed this request)
|
|
167
|
+
3. Fetch + verify the registry attestation (registry signed the agent's identity)
|
|
168
|
+
4. Confirm the signing key matches the attested public key
|
|
169
|
+
Returns VerifiedAgent or None if verification fails.
|
|
170
|
+
"""
|
|
171
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
|
172
|
+
from cryptography.exceptions import InvalidSignature
|
|
173
|
+
|
|
174
|
+
# Parse headers (case-insensitive)
|
|
175
|
+
headers_lower = {k.lower(): v for k, v in headers.items()}
|
|
176
|
+
|
|
177
|
+
agent_id = headers_lower.get("x-agent-id", "").strip()
|
|
178
|
+
auth_header = headers_lower.get("authorization", "")
|
|
179
|
+
timestamp_str = headers_lower.get("x-timestamp", "")
|
|
180
|
+
|
|
181
|
+
if not agent_id or not auth_header or not timestamp_str:
|
|
182
|
+
return None
|
|
183
|
+
|
|
184
|
+
if not auth_header.startswith("AgentSig "):
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
sig_part = auth_header[len("AgentSig "):]
|
|
189
|
+
pubkey_b58, sig_b64 = sig_part.rsplit(":", 1)
|
|
190
|
+
timestamp = int(timestamp_str)
|
|
191
|
+
except (ValueError, IndexError):
|
|
192
|
+
return None
|
|
193
|
+
|
|
194
|
+
# Check clock skew (±60 seconds)
|
|
195
|
+
now = int(time.time())
|
|
196
|
+
if abs(now - timestamp) > 60:
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
# Verify request signature
|
|
200
|
+
# Signed message: "<METHOD>:<path>:<timestamp>:<sha256_hex_of_body>"
|
|
201
|
+
body_bytes = body.encode() if isinstance(body, str) else body
|
|
202
|
+
body_hash = hashlib.sha256(body_bytes).hexdigest()
|
|
203
|
+
message = f"{method.upper()}:{path}:{timestamp}:{body_hash}"
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
pub_key_bytes = b58decode_key(pubkey_b58)
|
|
207
|
+
sig_bytes = base64.b64decode(sig_b64)
|
|
208
|
+
pub_key = Ed25519PublicKey.from_public_bytes(pub_key_bytes)
|
|
209
|
+
pub_key.verify(sig_bytes, message.encode("utf-8"))
|
|
210
|
+
except (InvalidSignature, Exception):
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
# Fetch attestation and verify registry signature
|
|
214
|
+
try:
|
|
215
|
+
attestation = fetch_attestation(agent_id, base_url)
|
|
216
|
+
except AttestationError:
|
|
217
|
+
return None
|
|
218
|
+
|
|
219
|
+
# Confirm agent_id matches
|
|
220
|
+
if attestation["agent_id"] != agent_id:
|
|
221
|
+
return None
|
|
222
|
+
|
|
223
|
+
# Confirm signing key matches attested public key
|
|
224
|
+
if attestation["public_key_b58"] != pubkey_b58:
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
return VerifiedAgent(
|
|
228
|
+
agent_id=attestation["agent_id"],
|
|
229
|
+
name=attestation["agent_name"],
|
|
230
|
+
public_key_b58=attestation["public_key_b58"],
|
|
231
|
+
capabilities=attestation["capabilities"],
|
|
232
|
+
protocols=attestation["protocols"],
|
|
233
|
+
reputation=attestation["reputation"],
|
|
234
|
+
reputation_tier=attestation["reputation_tier"],
|
|
235
|
+
verification_count=attestation["verification_count"],
|
|
236
|
+
issued_at=attestation["issued_at"],
|
|
237
|
+
expires_at=attestation["expires_at"],
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
# ── FastAPI dependency ────────────────────────────────────────────────────────
|
|
242
|
+
|
|
243
|
+
def require_agent(
|
|
244
|
+
min_reputation: float = 0.0,
|
|
245
|
+
capabilities: list[str] | None = None,
|
|
246
|
+
base_url: str = API_BASE,
|
|
247
|
+
) -> Callable:
|
|
248
|
+
"""
|
|
249
|
+
FastAPI dependency factory for reputation-gated agent authentication.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
min_reputation: Minimum reputation score (0–1). Default 0 (any registered agent).
|
|
253
|
+
capabilities: Required capabilities. Agent must have ALL of them verified.
|
|
254
|
+
base_url: Override API base URL (useful for testing with staging).
|
|
255
|
+
|
|
256
|
+
Returns:
|
|
257
|
+
FastAPI Depends-compatible callable that returns VerifiedAgent.
|
|
258
|
+
|
|
259
|
+
Raises:
|
|
260
|
+
HTTPException(401) if agent identity cannot be verified.
|
|
261
|
+
HTTPException(403) if agent doesn't meet reputation/capability requirements.
|
|
262
|
+
|
|
263
|
+
Example:
|
|
264
|
+
@app.post("/run")
|
|
265
|
+
async def run(request: Request, agent: VerifiedAgent = Depends(require_agent(
|
|
266
|
+
min_reputation=0.5,
|
|
267
|
+
capabilities=["code"],
|
|
268
|
+
))):
|
|
269
|
+
...
|
|
270
|
+
"""
|
|
271
|
+
async def _dependency(request: Any) -> VerifiedAgent:
|
|
272
|
+
# Import here to avoid hard dep on fastapi
|
|
273
|
+
try:
|
|
274
|
+
from fastapi import HTTPException
|
|
275
|
+
from fastapi import Request as FastAPIRequest
|
|
276
|
+
except ImportError:
|
|
277
|
+
raise ImportError("fastapi is required for require_agent(). pip install fastapi")
|
|
278
|
+
|
|
279
|
+
# Read body
|
|
280
|
+
body = b""
|
|
281
|
+
try:
|
|
282
|
+
body = await request.body()
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
headers = dict(request.headers)
|
|
287
|
+
path = request.url.path
|
|
288
|
+
method = request.method
|
|
289
|
+
|
|
290
|
+
agent = verify_request(headers, method, path, body, base_url)
|
|
291
|
+
|
|
292
|
+
if agent is None:
|
|
293
|
+
raise HTTPException(
|
|
294
|
+
status_code=401,
|
|
295
|
+
detail="Agent identity verification failed. Include X-Agent-ID and Authorization: AgentSig headers.",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
if agent.reputation < min_reputation:
|
|
299
|
+
raise HTTPException(
|
|
300
|
+
status_code=403,
|
|
301
|
+
detail=f"Agent reputation {agent.reputation:.3f} below required {min_reputation}. "
|
|
302
|
+
f"Current tier: {agent.reputation_tier}.",
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
if capabilities:
|
|
306
|
+
missing = [c for c in capabilities if c not in agent.capabilities]
|
|
307
|
+
if missing:
|
|
308
|
+
raise HTTPException(
|
|
309
|
+
status_code=403,
|
|
310
|
+
detail=f"Agent missing required capabilities: {missing}. "
|
|
311
|
+
f"Attested capabilities: {agent.capabilities}.",
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
return agent
|
|
315
|
+
|
|
316
|
+
return _dependency
|
|
@@ -6,6 +6,7 @@ basedagents/cli.py
|
|
|
6
6
|
basedagents/client.py
|
|
7
7
|
basedagents/easy.py
|
|
8
8
|
basedagents/keypair.py
|
|
9
|
+
basedagents/middleware.py
|
|
9
10
|
basedagents/pow.py
|
|
10
11
|
basedagents.egg-info/PKG-INFO
|
|
11
12
|
basedagents.egg-info/SOURCES.txt
|
|
@@ -14,4 +15,6 @@ basedagents.egg-info/entry_points.txt
|
|
|
14
15
|
basedagents.egg-info/requires.txt
|
|
15
16
|
basedagents.egg-info/top_level.txt
|
|
16
17
|
basedagents/integrations/__init__.py
|
|
18
|
+
basedagents/integrations/autogen.py
|
|
19
|
+
basedagents/integrations/crewai.py
|
|
17
20
|
basedagents/integrations/langchain.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "basedagents"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "Python SDK for basedagents.ai — cryptographic identity and reputation registry for AI agents"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "MIT" }
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|