Skip to content

Commit b0f1467

Browse files
committed
feat(adapters): crewai_adapter.py
1 parent afb649a commit b0f1467

1 file changed

Lines changed: 323 additions & 0 deletions

File tree

Lines changed: 323 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,323 @@
1+
"""
2+
agentcard_adapters.crewai_adapter
3+
==================================
4+
5+
Bidirectional adapter between CrewAI agents/crews and AgentCard v1.0.
6+
7+
Usage
8+
-----
9+
**Export a CrewAI Agent as AgentCard:**
10+
11+
from crewai import Agent
12+
from agentcard_adapters.crewai_adapter import agent_to_agentcard
13+
14+
researcher = Agent(
15+
role="Senior Research Analyst",
16+
goal="Uncover cutting-edge developments in AI",
17+
backstory="You are an expert AI researcher with 10 years of experience.",
18+
tools=[search_tool, scrape_tool],
19+
verbose=True,
20+
)
21+
22+
card = agent_to_agentcard(
23+
agent=researcher,
24+
agent_id="01HZQK3P8EMXR9V7T5N2W4J6C0",
25+
endpoint_url="https://my-crew.example.com/api/researcher",
26+
)
27+
card.validate()
28+
print(card.to_json(indent=2))
29+
30+
**Export an entire Crew as an AgentCard registry:**
31+
32+
from crewai import Crew
33+
from agentcard_adapters.crewai_adapter import crew_to_agentcards
34+
35+
crew = Crew(agents=[researcher, writer], tasks=[...])
36+
cards = crew_to_agentcards(crew, base_url="https://my-crew.example.com/api")
37+
for card in cards:
38+
print(card.to_json(indent=2))
39+
40+
License
41+
-------
42+
Apache 2.0. See https://github.com/kwailapt/AgentCard/blob/main/LICENSE
43+
"""
44+
45+
from __future__ import annotations
46+
47+
import re
48+
from typing import TYPE_CHECKING, Any, Optional
49+
50+
from .core import (
51+
AgentCard,
52+
Capability,
53+
Endpoint,
54+
PricingModel,
55+
LANDAUER_FLOOR_JOULES,
56+
)
57+
58+
if TYPE_CHECKING:
59+
from crewai import Agent as CrewAgent, Crew
60+
61+
__all__ = [
62+
"agent_to_agentcard",
63+
"crew_to_agentcards",
64+
"agentcard_to_agent",
65+
]
66+
67+
68+
# ── Helpers ───────────────────────────────────────────────────────────────────
69+
70+
def _role_to_cap_id(role: str) -> str:
71+
"""
72+
Convert a CrewAI agent role string to a valid capability id.
73+
74+
Examples:
75+
"Senior Research Analyst" → "senior_research_analyst"
76+
"Blog Post Writer" → "blog_post_writer"
77+
"Code Review Expert" → "code_review_expert"
78+
"""
79+
s = role.lower().strip()
80+
s = re.sub(r"[\s]+", "_", s)
81+
s = re.sub(r"[^a-z0-9._-]", "", s)
82+
s = re.sub(r"^[^a-z0-9]+", "", s)
83+
return s or "agent"
84+
85+
86+
def _tool_to_capability(tool: Any) -> Capability:
87+
"""Convert a CrewAI/LangChain BaseTool to a Capability."""
88+
name = getattr(tool, "name", str(tool))
89+
desc = getattr(tool, "description", f"Tool: {name}")
90+
cap_id = re.sub(r"[^a-z0-9._-]", "", name.lower().replace(" ", "_"))
91+
if not cap_id or not cap_id[0].isalnum():
92+
cap_id = "tool_" + cap_id
93+
return Capability(id=cap_id or "tool", description=desc)
94+
95+
96+
# ── Single agent → AgentCard ──────────────────────────────────────────────────
97+
98+
def agent_to_agentcard(
99+
agent: "CrewAgent",
100+
agent_id: str,
101+
endpoint_url: str,
102+
*,
103+
version: str = "1.0.0",
104+
protocol: str = "http",
105+
health_url: Optional[str] = None,
106+
estimated_latency_ms: Optional[float] = None,
107+
include_tool_capabilities: bool = True,
108+
) -> AgentCard:
109+
"""
110+
Convert a CrewAI ``Agent`` to an ``AgentCard``.
111+
112+
The agent's *role* becomes the primary capability id.
113+
If ``include_tool_capabilities=True``, each tool is also listed as
114+
a separate capability under the namespace ``tool.<tool_name>``.
115+
116+
Parameters
117+
----------
118+
agent:
119+
A CrewAI ``Agent`` instance.
120+
agent_id:
121+
26-character Crockford Base32 ULID for this agent.
122+
endpoint_url:
123+
URL where this agent is reachable.
124+
version:
125+
Semantic version of the card. Defaults to ``"1.0.0"``.
126+
protocol:
127+
Transport protocol. Defaults to ``"http"``.
128+
health_url:
129+
Optional health-check endpoint.
130+
estimated_latency_ms:
131+
Estimated response latency in milliseconds.
132+
include_tool_capabilities:
133+
If ``True``, each tool is listed as a separate Capability.
134+
Set to ``False`` to emit only the role capability.
135+
136+
Returns
137+
-------
138+
AgentCard
139+
A validated AgentCard for this CrewAI agent.
140+
"""
141+
role: str = getattr(agent, "role", "Agent")
142+
goal: str = getattr(agent, "goal", "")
143+
backstory: str = getattr(agent, "backstory", "")
144+
145+
# Primary capability: the agent's role
146+
primary_desc = goal
147+
if backstory:
148+
primary_desc = f"{goal}{backstory[:200]}"
149+
150+
primary_cap = Capability(
151+
id=_role_to_cap_id(role),
152+
description=primary_desc or f"CrewAI agent: {role}",
153+
tags=["crewai", "agent"],
154+
)
155+
156+
capabilities = [primary_cap]
157+
158+
# Tool capabilities
159+
if include_tool_capabilities:
160+
tools: list[Any] = getattr(agent, "tools", []) or []
161+
for tool in tools:
162+
try:
163+
cap = _tool_to_capability(tool)
164+
# Namespace under "tool." to avoid id collision with role
165+
if not cap.id.startswith("tool."):
166+
cap.id = f"tool.{cap.id}"
167+
capabilities.append(cap)
168+
except Exception: # noqa: BLE001
169+
pass
170+
171+
pricing = None
172+
if estimated_latency_ms is not None:
173+
pricing = PricingModel(
174+
base_cost_joules=LANDAUER_FLOOR_JOULES,
175+
estimated_latency_ms=estimated_latency_ms,
176+
)
177+
178+
card = AgentCard(
179+
agent_id=agent_id,
180+
name=role,
181+
version=version,
182+
capabilities=capabilities,
183+
endpoint=Endpoint(
184+
protocol=protocol,
185+
url=endpoint_url,
186+
health_url=health_url,
187+
),
188+
pricing=pricing,
189+
)
190+
card.validate()
191+
return card
192+
193+
194+
# ── Crew → list[AgentCard] ────────────────────────────────────────────────────
195+
196+
def crew_to_agentcards(
197+
crew: "Crew",
198+
agent_ids: Optional[list[str]] = None,
199+
base_url: str = "https://localhost/api",
200+
*,
201+
version: str = "1.0.0",
202+
protocol: str = "http",
203+
include_tool_capabilities: bool = True,
204+
) -> list[AgentCard]:
205+
"""
206+
Convert all agents in a CrewAI ``Crew`` to a list of ``AgentCard`` objects.
207+
208+
Each agent gets a separate card. The endpoint URL is derived as
209+
``{base_url}/{slug}`` where *slug* is the role normalised to a URL path.
210+
211+
Parameters
212+
----------
213+
crew:
214+
A CrewAI ``Crew`` instance.
215+
agent_ids:
216+
Optional list of 26-char ULIDs, one per agent. If omitted, IDs
217+
are derived from the agent roles (NOT globally unique — suitable
218+
for local testing only).
219+
base_url:
220+
Base URL prefix. Each agent's card URL = ``{base_url}/{role_slug}``.
221+
version:
222+
Semantic version applied to all cards.
223+
protocol:
224+
Transport protocol for all cards.
225+
include_tool_capabilities:
226+
Whether to include tool capabilities in each card.
227+
228+
Returns
229+
-------
230+
list[AgentCard]
231+
One validated AgentCard per agent.
232+
"""
233+
agents: list[Any] = getattr(crew, "agents", []) or []
234+
if not agents:
235+
raise ValueError("Crew has no agents")
236+
237+
cards = []
238+
for i, agent in enumerate(agents):
239+
role = getattr(agent, "role", f"agent_{i}")
240+
slug = _role_to_cap_id(role)
241+
url = f"{base_url.rstrip('/')}/{slug}"
242+
243+
# Agent ID: use provided or generate a deterministic placeholder
244+
if agent_ids and i < len(agent_ids):
245+
aid = agent_ids[i]
246+
else:
247+
# Deterministic placeholder (NOT a real ULID — call out in docs)
248+
# Real deployment must supply proper ULIDs
249+
import hashlib
250+
h = hashlib.sha256(role.encode()).hexdigest().upper()[:26]
251+
# Ensure Crockford alphabet (replace I, L, O, U)
252+
h = h.translate(str.maketrans("ILOU", "JKMN"))
253+
aid = h
254+
255+
card = agent_to_agentcard(
256+
agent=agent,
257+
agent_id=aid,
258+
endpoint_url=url,
259+
version=version,
260+
protocol=protocol,
261+
include_tool_capabilities=include_tool_capabilities,
262+
)
263+
cards.append(card)
264+
265+
return cards
266+
267+
268+
# ── AgentCard → CrewAI Agent ──────────────────────────────────────────────────
269+
270+
def agentcard_to_agent(
271+
card: AgentCard,
272+
llm: Any = None,
273+
verbose: bool = False,
274+
) -> "CrewAgent":
275+
"""
276+
Reconstruct a CrewAI ``Agent`` from an ``AgentCard``.
277+
278+
The primary capability (index 0) is used as the agent's *role* and *goal*.
279+
Additional capabilities with ``tool.`` prefix are converted to
280+
``RemoteAgentCardTool`` instances that POST to the card's endpoint.
281+
282+
Parameters
283+
----------
284+
card:
285+
The AgentCard to convert.
286+
llm:
287+
LLM instance to pass to the CrewAI Agent. If ``None``, uses
288+
CrewAI's default.
289+
verbose:
290+
Enable verbose CrewAI logging.
291+
292+
Returns
293+
-------
294+
crewai.Agent
295+
A CrewAI Agent backed by the AgentCard's endpoint.
296+
297+
Raises
298+
------
299+
ImportError
300+
If ``crewai`` is not installed.
301+
"""
302+
try:
303+
from crewai import Agent as CrewAgent
304+
except ImportError as e:
305+
raise ImportError("crewai is required: pip install crewai") from e
306+
307+
primary = card.capabilities[0]
308+
role = card.name
309+
goal = primary.description
310+
311+
kwargs: dict[str, Any] = {
312+
"role": role,
313+
"goal": goal,
314+
"backstory": (
315+
f"An agent with AgentCard id {card.agent_id} reachable at "
316+
f"{card.endpoint.url} via {card.endpoint.protocol}."
317+
),
318+
"verbose": verbose,
319+
}
320+
if llm is not None:
321+
kwargs["llm"] = llm
322+
323+
return CrewAgent(**kwargs)

0 commit comments

Comments
 (0)