kagent-adk 0.0.1__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.

Potentially problematic release.


This version of kagent-adk might be problematic. Click here for more details.

kagent_adk/__init__.py ADDED
@@ -0,0 +1,10 @@
1
+ import importlib.metadata
2
+
3
+ from .a2a import KAgentApp
4
+ from .kagent_session_service import KAgentSessionService
5
+ from .kagent_task_store import KAgentTaskStore
6
+ from .models import AgentConfig
7
+
8
+ __version__ = importlib.metadata.version("kagent_adk")
9
+
10
+ __all__ = ["KAgentSessionService", "KAgentTaskStore", "KAgentApp", "AgentConfig"]
kagent_adk/a2a.py ADDED
@@ -0,0 +1,202 @@
1
+ #! /usr/bin/env python3
2
+ import faulthandler
3
+ import logging
4
+ import os
5
+ import sys
6
+ from contextlib import asynccontextmanager
7
+ from typing import Callable
8
+
9
+ import httpx
10
+ from a2a.auth.user import User
11
+ from a2a.server.agent_execution import RequestContext, SimpleRequestContextBuilder
12
+ from a2a.server.apps import A2AStarletteApplication
13
+ from a2a.server.context import ServerCallContext
14
+ from a2a.server.request_handlers import DefaultRequestHandler
15
+ from a2a.server.tasks import TaskStore
16
+ from a2a.types import AgentCard, MessageSendParams, Task
17
+ from fastapi import FastAPI, Request
18
+ from fastapi.responses import PlainTextResponse
19
+ from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
20
+ from google.adk.agents import BaseAgent
21
+ from google.adk.runners import Runner
22
+ from google.adk.sessions import InMemorySessionService
23
+ from google.genai import types
24
+
25
+ from .kagent_session_service import KAgentSessionService
26
+ from .kagent_task_store import KAgentTaskStore
27
+
28
+ # --- Constants ---
29
+ USER_ID = "admin@kagent.dev"
30
+
31
+ # --- Configure Logging ---
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ class KAgentUser(User):
36
+ def __init__(self, user_id: str):
37
+ self.user_id = user_id
38
+
39
+ @property
40
+ def is_authenticated(self) -> bool:
41
+ return False
42
+
43
+ @property
44
+ def user_name(self) -> str:
45
+ return self.user_id
46
+
47
+
48
+ class KAgentRequestContextBuilder(SimpleRequestContextBuilder):
49
+ """
50
+ A request context builder that will be used to hack in the user_id for now.
51
+ """
52
+
53
+ def __init__(self, user_id: str, task_store: TaskStore):
54
+ super().__init__(task_store=task_store)
55
+ self.user_id = user_id
56
+
57
+ async def build(
58
+ self,
59
+ params: MessageSendParams | None = None,
60
+ task_id: str | None = None,
61
+ context_id: str | None = None,
62
+ task: Task | None = None,
63
+ context: ServerCallContext | None = None,
64
+ ) -> RequestContext:
65
+ if not context:
66
+ context = ServerCallContext(user=KAgentUser(user_id=self.user_id))
67
+ else:
68
+ context.user = KAgentUser(user_id=self.user_id)
69
+ request_context = await super().build(params, task_id, context_id, task, context)
70
+ return request_context
71
+
72
+
73
+ def health_check(request: Request) -> PlainTextResponse:
74
+ return PlainTextResponse("OK")
75
+
76
+
77
+ def thread_dump(request: Request) -> PlainTextResponse:
78
+ import io
79
+
80
+ buf = io.StringIO()
81
+ faulthandler.dump_traceback(file=buf)
82
+ buf.seek(0)
83
+ return PlainTextResponse(buf.read())
84
+
85
+
86
+ kagent_url_override = os.getenv("KAGENT_URL")
87
+
88
+
89
+ class KAgentApp:
90
+ def __init__(
91
+ self,
92
+ root_agent: BaseAgent | Callable[[], BaseAgent],
93
+ agent_card: AgentCard,
94
+ kagent_url: str,
95
+ app_name: str,
96
+ ):
97
+ self.root_agent = root_agent
98
+ self.kagent_url = kagent_url
99
+ self.app_name = app_name
100
+ self.agent_card = agent_card
101
+
102
+ def build(self) -> FastAPI:
103
+ http_client = httpx.AsyncClient(base_url=kagent_url_override or self.kagent_url)
104
+ session_service = KAgentSessionService(http_client)
105
+
106
+ if isinstance(self.root_agent, Callable):
107
+ agent_factory = self.root_agent
108
+
109
+ def create_runner() -> Runner:
110
+ return Runner(
111
+ agent=agent_factory(),
112
+ app_name=self.app_name,
113
+ session_service=session_service,
114
+ )
115
+
116
+ runner = create_runner
117
+ elif isinstance(self.root_agent, BaseAgent):
118
+ agent_instance = self.root_agent
119
+
120
+ def create_runner() -> Runner:
121
+ return Runner(
122
+ agent=agent_instance,
123
+ app_name=self.app_name,
124
+ session_service=session_service,
125
+ )
126
+
127
+ runner = create_runner
128
+ else:
129
+ raise ValueError(f"Invalid root agent: {self.root_agent}")
130
+
131
+ agent_executor = A2aAgentExecutor(
132
+ runner=runner,
133
+ )
134
+
135
+ kagent_task_store = KAgentTaskStore(http_client)
136
+
137
+ request_context_builder = KAgentRequestContextBuilder(user_id=USER_ID, task_store=kagent_task_store)
138
+ request_handler = DefaultRequestHandler(
139
+ agent_executor=agent_executor,
140
+ task_store=kagent_task_store,
141
+ request_context_builder=request_context_builder,
142
+ )
143
+
144
+ a2a_app = A2AStarletteApplication(
145
+ agent_card=self.agent_card,
146
+ http_handler=request_handler,
147
+ )
148
+
149
+ # @asynccontextmanager
150
+ # async def agent_lifespan(app: FastAPI):
151
+ # yield
152
+ # if isinstance(runner, Runner):
153
+ # await runner.close()
154
+
155
+ faulthandler.enable()
156
+ app = FastAPI()
157
+
158
+ # Health check/readiness probe
159
+ app.add_route("/health", methods=["GET"], route=health_check)
160
+ app.add_route("/thread_dump", methods=["GET"], route=thread_dump)
161
+ a2a_app.add_routes_to_app(app)
162
+
163
+ return app
164
+
165
+ async def test(self, task: str):
166
+ session_service = InMemorySessionService()
167
+ SESSION_ID = "12345"
168
+ USER_ID = "admin"
169
+ await session_service.create_session(
170
+ app_name=self.app_name,
171
+ session_id=SESSION_ID,
172
+ user_id=USER_ID,
173
+ )
174
+ if isinstance(self.root_agent, Callable):
175
+ agent_factory = self.root_agent
176
+ root_agent = agent_factory()
177
+ else:
178
+ root_agent = self.root_agent
179
+
180
+ runner = Runner(
181
+ agent=root_agent,
182
+ app_name=self.app_name,
183
+ session_service=session_service,
184
+ )
185
+
186
+ logger.info(f"\n>>> User Query: {task}")
187
+
188
+ # Prepare the user's message in ADK format
189
+ content = types.Content(role="user", parts=[types.Part(text=task)])
190
+ # Key Concept: run_async executes the agent logic and yields Events.
191
+ # We iterate through events to find the final answer.
192
+ async for event in runner.run_async(
193
+ user_id=USER_ID,
194
+ session_id=SESSION_ID,
195
+ new_message=content,
196
+ ):
197
+ # You can uncomment the line below to see *all* events during execution
198
+ # print(f" [Event] Author: {event.author}, Type: {type(event).__name__}, Final: {event.is_final_response()}, Content: {event.content}")
199
+
200
+ # Key Concept: is_final_response() marks the concluding message for the turn.
201
+ jsn = event.model_dump_json()
202
+ logger.info(f" [Event] {jsn}")
@@ -0,0 +1,175 @@
1
+ import logging
2
+ from typing import Any, Optional
3
+
4
+ import httpx
5
+ from google.adk.events.event import Event
6
+ from google.adk.sessions import Session
7
+ from google.adk.sessions.base_session_service import (
8
+ BaseSessionService,
9
+ GetSessionConfig,
10
+ ListSessionsResponse,
11
+ )
12
+ from typing_extensions import override
13
+
14
+ logger = logging.getLogger("kagent." + __name__)
15
+
16
+
17
+ class KAgentSessionService(BaseSessionService):
18
+ """A session service implementation that uses the Kagent API.
19
+ This service integrates with the Kagent server to manage session state
20
+ and persistence through HTTP API calls.
21
+ """
22
+
23
+ def __init__(self, client: httpx.AsyncClient):
24
+ super().__init__()
25
+ self.client = client
26
+
27
+ async def _get_user_id(self) -> str:
28
+ """Get the default user ID. Override this method to implement custom user ID logic."""
29
+ return "admin@kagent.dev"
30
+
31
+ @override
32
+ async def create_session(
33
+ self,
34
+ *,
35
+ app_name: str,
36
+ user_id: str,
37
+ state: Optional[dict[str, Any]] = None,
38
+ session_id: Optional[str] = None,
39
+ ) -> Session:
40
+ # Prepare request data
41
+ request_data = {
42
+ "user_id": user_id,
43
+ "agent_ref": app_name, # Use app_name as agent reference
44
+ }
45
+ if session_id:
46
+ request_data["id"] = session_id
47
+
48
+ # Make API call to create session
49
+ response = await self.client.post(
50
+ "/api/sessions",
51
+ json=request_data,
52
+ headers={"X-User-ID": user_id},
53
+ )
54
+ response.raise_for_status()
55
+
56
+ data = response.json()
57
+ if not data.get("data"):
58
+ raise RuntimeError(f"Failed to create session: {data.get('message', 'Unknown error')}")
59
+
60
+ session_data = data["data"]
61
+
62
+ # Convert to ADK Session format
63
+ return Session(id=session_data["id"], user_id=session_data["user_id"], state=state or {}, app_name=app_name)
64
+
65
+ @override
66
+ async def get_session(
67
+ self,
68
+ *,
69
+ app_name: str,
70
+ user_id: str,
71
+ session_id: str,
72
+ config: Optional[GetSessionConfig] = None,
73
+ ) -> Optional[Session]:
74
+ try:
75
+ url = f"/api/sessions/{session_id}?user_id={user_id}"
76
+ if config:
77
+ if config.after_timestamp:
78
+ # TODO: implement
79
+ # url += f"&after={config.after_timestamp}"
80
+ pass
81
+ if config.num_recent_events:
82
+ url += f"&limit={config.num_recent_events}"
83
+ else:
84
+ url += "&limit=-1"
85
+ else:
86
+ # return all
87
+ url += "&limit=-1"
88
+
89
+ # Make API call to get session
90
+ response: httpx.Response = await self.client.get(
91
+ url,
92
+ headers={"X-User-ID": user_id},
93
+ )
94
+ if response.status_code == 404:
95
+ return None
96
+ response.raise_for_status()
97
+
98
+ data = response.json()
99
+ if not data.get("data"):
100
+ return None
101
+
102
+ if not data.get("data").get("session"):
103
+ return None
104
+ session_data = data["data"]["session"]
105
+
106
+ events_data = data["data"]["events"]
107
+
108
+ events: list[Event] = []
109
+ for event_data in events_data:
110
+ events.append(Event.model_validate_json(event_data["data"]))
111
+
112
+ # Convert to ADK Session format
113
+ return Session(
114
+ id=session_data["id"],
115
+ user_id=session_data["user_id"],
116
+ events=events,
117
+ app_name=app_name,
118
+ state={}, # TODO: restore State
119
+ )
120
+ except httpx.HTTPStatusError as e:
121
+ if e.response.status_code == 404:
122
+ return None
123
+ raise
124
+
125
+ @override
126
+ async def list_sessions(self, *, app_name: str, user_id: str) -> ListSessionsResponse:
127
+ # Make API call to list sessions
128
+ response = await self.client.get(f"/api/sessions?user_id={user_id}", headers={"X-User-ID": user_id})
129
+ response.raise_for_status()
130
+
131
+ data = response.json()
132
+ sessions_data = data.get("data", [])
133
+
134
+ # Convert to ADK Session format
135
+ sessions = []
136
+ for session_data in sessions_data:
137
+ session = Session(id=session_data["id"], user_id=session_data["user_id"], state={}, app_name=app_name)
138
+ sessions.append(session)
139
+
140
+ return ListSessionsResponse(sessions=sessions)
141
+
142
+ def list_sessions_sync(self, *, app_name: str, user_id: str) -> ListSessionsResponse:
143
+ raise NotImplementedError("not supported. use async")
144
+
145
+ @override
146
+ async def delete_session(self, *, app_name: str, user_id: str, session_id: str) -> None:
147
+ # Make API call to delete session
148
+ response = await self.client.delete(
149
+ f"/api/sessions/{session_id}?user_id={user_id}",
150
+ headers={"X-User-ID": user_id},
151
+ )
152
+ response.raise_for_status()
153
+
154
+ @override
155
+ async def append_event(self, session: Session, event: Event) -> Event:
156
+ # Convert ADK Event to JSON format
157
+ event_data = {
158
+ "id": event.id,
159
+ "data": event.model_dump_json(),
160
+ }
161
+
162
+ # Make API call to append event to session
163
+ response = await self.client.post(
164
+ f"/api/sessions/{session.id}/events?user_id={session.user_id}",
165
+ json=event_data,
166
+ headers={"X-User-ID": session.user_id},
167
+ )
168
+ response.raise_for_status()
169
+
170
+ # TODO: potentially pull and update the session from the server
171
+ # Update the in-memory session.
172
+ session.last_update_time = event.timestamp
173
+ await super().append_event(session=session, event=event)
174
+
175
+ return event
@@ -0,0 +1,30 @@
1
+ from typing import override
2
+
3
+ import httpx
4
+ from a2a.server.tasks import TaskStore
5
+ from a2a.types import Task
6
+
7
+
8
+ class KAgentTaskStore(TaskStore):
9
+ client: httpx.AsyncClient
10
+
11
+ def __init__(self, client: httpx.AsyncClient):
12
+ self.client = client
13
+
14
+ @override
15
+ async def save(self, task: Task) -> None:
16
+ response = await self.client.post("/api/tasks", json=task.model_dump())
17
+ response.raise_for_status()
18
+
19
+ @override
20
+ async def get(self, task_id: str) -> Task | None:
21
+ response = await self.client.get(f"/api/tasks/{task_id}")
22
+ if response.status_code == 404:
23
+ return None
24
+ response.raise_for_status()
25
+ return Task.model_validate(response.json())
26
+
27
+ @override
28
+ async def delete(self, task_id: str) -> None:
29
+ response = await self.client.delete(f"/api/tasks/{task_id}")
30
+ response.raise_for_status()
kagent_adk/models.py ADDED
@@ -0,0 +1,110 @@
1
+ import logging
2
+ from typing import Literal, Self, Union
3
+
4
+ from a2a.types import AgentCard
5
+ from google.adk.agents import Agent
6
+ from google.adk.agents.llm_agent import ToolUnion
7
+ from google.adk.agents.run_config import RunConfig, StreamingMode
8
+ from google.adk.models.anthropic_llm import Claude as ClaudeLLM
9
+ from google.adk.models.google_llm import Gemini as GeminiLLM
10
+ from google.adk.models.lite_llm import LiteLlm
11
+ from google.adk.tools.agent_tool import AgentTool
12
+ from google.adk.tools.mcp_tool import MCPToolset, SseConnectionParams, StreamableHTTPConnectionParams
13
+ from pydantic import BaseModel, Field
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ class HttpMcpServerConfig(BaseModel):
19
+ params: StreamableHTTPConnectionParams
20
+ tools: list[str] = Field(default_factory=list)
21
+
22
+
23
+ class SseMcpServerConfig(BaseModel):
24
+ params: SseConnectionParams
25
+ tools: list[str] = Field(default_factory=list)
26
+
27
+
28
+ class BaseLLM(BaseModel):
29
+ model: str
30
+
31
+
32
+ class OpenAI(BaseLLM):
33
+ base_url: str | None = None
34
+
35
+ type: Literal["openai"]
36
+
37
+
38
+ class AzureOpenAI(BaseLLM):
39
+ type: Literal["azure_openai"]
40
+
41
+
42
+ class Anthropic(BaseLLM):
43
+ base_url: str | None = None
44
+
45
+ type: Literal["anthropic"]
46
+
47
+
48
+ class GeminiVertexAI(BaseLLM):
49
+ type: Literal["gemini_vertex_ai"]
50
+
51
+
52
+ class GeminiAnthropic(BaseLLM):
53
+ type: Literal["gemini_anthropic"]
54
+
55
+
56
+ class Ollama(BaseLLM):
57
+ type: Literal["ollama"]
58
+
59
+
60
+ class Gemini(BaseLLM):
61
+ type: Literal["gemini"]
62
+
63
+
64
+ class AgentConfig(BaseModel):
65
+ kagent_url: str # The URL of the KAgent server
66
+ agent_card: AgentCard
67
+ name: str
68
+ model: Union[OpenAI, Anthropic, GeminiVertexAI, GeminiAnthropic, Ollama, AzureOpenAI, Gemini] = Field(
69
+ discriminator="type"
70
+ )
71
+ description: str
72
+ instruction: str
73
+ http_tools: list[HttpMcpServerConfig] | None = None # tools, always MCP
74
+ sse_tools: list[SseMcpServerConfig] | None = None # tools, always MCP
75
+ agents: list[Self] | None = None # agent names
76
+
77
+ def to_agent(self) -> Agent:
78
+ mcp_toolsets: list[ToolUnion] = []
79
+ if self.http_tools:
80
+ for http_tool in self.http_tools: # add http tools
81
+ mcp_toolsets.append(MCPToolset(connection_params=http_tool.params, tool_filter=http_tool.tools))
82
+ if self.sse_tools:
83
+ for sse_tool in self.sse_tools: # add stdio tools
84
+ mcp_toolsets.append(MCPToolset(connection_params=sse_tool.params, tool_filter=sse_tool.tools))
85
+ if self.agents:
86
+ for agent in self.agents: # Add sub agents as tools
87
+ mcp_toolsets.append(AgentTool(agent.to_agent()))
88
+ if self.model.type == "openai":
89
+ model = LiteLlm(model=f"openai/{self.model.model}", base_url=self.model.base_url)
90
+ elif self.model.type == "anthropic":
91
+ model = LiteLlm(model=f"anthropic/{self.model.model}", base_url=self.model.base_url)
92
+ elif self.model.type == "gemini_vertex_ai":
93
+ model = GeminiLLM(model=self.model.model)
94
+ elif self.model.type == "gemini_anthropic":
95
+ model = ClaudeLLM(model=self.model.model)
96
+ elif self.model.type == "ollama":
97
+ model = LiteLlm(model=f"ollama_chat/{self.model.model}")
98
+ elif self.model.type == "azure_openai":
99
+ model = LiteLlm(model=f"azure/{self.model.model}")
100
+ elif self.model.type == "gemini":
101
+ model = self.model.model
102
+ else:
103
+ raise ValueError(f"Invalid model type: {self.model.type}")
104
+ return Agent(
105
+ name=self.name,
106
+ model=model,
107
+ description=self.description,
108
+ instruction=self.instruction,
109
+ tools=mcp_toolsets,
110
+ )
@@ -0,0 +1,28 @@
1
+ Metadata-Version: 2.4
2
+ Name: kagent-adk
3
+ Version: 0.0.1
4
+ Summary: kagent-adk is an sdk for integrating adk agents with kagent
5
+ Requires-Python: >=3.12.11
6
+ Requires-Dist: a2a-sdk>=0.2.16
7
+ Requires-Dist: anthropic[vertex]>=0.49.0
8
+ Requires-Dist: anyio>=4.9.0
9
+ Requires-Dist: fastapi>=0.115.1
10
+ Requires-Dist: google-adk>=1.8.0
11
+ Requires-Dist: google-auth>=2.40.2
12
+ Requires-Dist: google-genai>=1.21.1
13
+ Requires-Dist: httpx>=0.25.0
14
+ Requires-Dist: jsonref>=1.1.0
15
+ Requires-Dist: litellm>=1.74.3
16
+ Requires-Dist: mcp>=1.12.0
17
+ Requires-Dist: openai>=1.72.0
18
+ Requires-Dist: opentelemetry-api>=1.32.0
19
+ Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.32.0
20
+ Requires-Dist: opentelemetry-instrumentation-httpx>=0.52.0
21
+ Requires-Dist: opentelemetry-instrumentation-openai>=0.39.0
22
+ Requires-Dist: opentelemetry-sdk>=1.32.0
23
+ Requires-Dist: protobuf>=6.31.1
24
+ Requires-Dist: pydantic>=2.5.0
25
+ Requires-Dist: typing-extensions>=4.8.0
26
+ Provides-Extra: test
27
+ Requires-Dist: pytest-asyncio>=0.25.3; extra == 'test'
28
+ Requires-Dist: pytest>=8.3.5; extra == 'test'
@@ -0,0 +1,8 @@
1
+ kagent_adk/__init__.py,sha256=S0SQAy9KBIX8Vm55TO5d4Jj94AvmTDXtG8YurUKrSj4,329
2
+ kagent_adk/a2a.py,sha256=d49IIoC2r__Zmjf5iuD_pFp8XKevrDn1Yh3sH0FRZKY,6460
3
+ kagent_adk/kagent_session_service.py,sha256=A47gsfDVp8jITzeW987AHTJLEhcU_mU3ik_SFptFGIc,5815
4
+ kagent_adk/kagent_task_store.py,sha256=3ApKbFfcDZmcEnwef6bCDhBhoGY9ZYwwyP671B1DHFo,889
5
+ kagent_adk/models.py,sha256=dtwUQny2r5K1JtCtl_mGk3dNMW-XnPPjWJT8JbyWE0E,3654
6
+ kagent_adk-0.0.1.dist-info/METADATA,sha256=FumdkqL9hAtChYuWbWkvMoZ31e5cJ1hdtZw5DbgaPjw,1021
7
+ kagent_adk-0.0.1.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
8
+ kagent_adk-0.0.1.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any