kagent-adk 0.0.1__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.
Potentially problematic release.
This version of kagent-adk might be problematic. Click here for more details.
- kagent_adk-0.0.1/.gitignore +173 -0
- kagent_adk-0.0.1/.python-version +1 -0
- kagent_adk-0.0.1/PKG-INFO +28 -0
- kagent_adk-0.0.1/README.md +0 -0
- kagent_adk-0.0.1/pyproject.toml +44 -0
- kagent_adk-0.0.1/src/kagent_adk/__init__.py +10 -0
- kagent_adk-0.0.1/src/kagent_adk/a2a.py +202 -0
- kagent_adk-0.0.1/src/kagent_adk/kagent_session_service.py +175 -0
- kagent_adk-0.0.1/src/kagent_adk/kagent_task_store.py +30 -0
- kagent_adk-0.0.1/src/kagent_adk/models.py +110 -0
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py,cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
#Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
#uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
#poetry.lock
|
|
109
|
+
|
|
110
|
+
# pdm
|
|
111
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
112
|
+
#pdm.lock
|
|
113
|
+
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
|
114
|
+
# in version control.
|
|
115
|
+
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
|
116
|
+
.pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
121
|
+
__pypackages__/
|
|
122
|
+
|
|
123
|
+
# Celery stuff
|
|
124
|
+
celerybeat-schedule
|
|
125
|
+
celerybeat.pid
|
|
126
|
+
|
|
127
|
+
# SageMath parsed files
|
|
128
|
+
*.sage.py
|
|
129
|
+
|
|
130
|
+
# Environments
|
|
131
|
+
.env
|
|
132
|
+
.venv
|
|
133
|
+
env/
|
|
134
|
+
venv/
|
|
135
|
+
ENV/
|
|
136
|
+
env.bak/
|
|
137
|
+
venv.bak/
|
|
138
|
+
|
|
139
|
+
# Spyder project settings
|
|
140
|
+
.spyderproject
|
|
141
|
+
.spyproject
|
|
142
|
+
|
|
143
|
+
# Rope project settings
|
|
144
|
+
.ropeproject
|
|
145
|
+
|
|
146
|
+
# mkdocs documentation
|
|
147
|
+
/site
|
|
148
|
+
|
|
149
|
+
# mypy
|
|
150
|
+
.mypy_cache/
|
|
151
|
+
.dmypy.json
|
|
152
|
+
dmypy.json
|
|
153
|
+
|
|
154
|
+
# Pyre type checker
|
|
155
|
+
.pyre/
|
|
156
|
+
|
|
157
|
+
# pytype static type analyzer
|
|
158
|
+
.pytype/
|
|
159
|
+
|
|
160
|
+
# Cython debug symbols
|
|
161
|
+
cython_debug/
|
|
162
|
+
|
|
163
|
+
# PyCharm
|
|
164
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
165
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
166
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
167
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
168
|
+
#.idea/
|
|
169
|
+
|
|
170
|
+
# PyPI configuration file
|
|
171
|
+
.pypirc
|
|
172
|
+
|
|
173
|
+
.DS_Store
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13.5
|
|
@@ -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'
|
|
File without changes
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kagent-adk"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "kagent-adk is an sdk for integrating adk agents with kagent"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.12.11"
|
|
11
|
+
dependencies = [
|
|
12
|
+
"anyio>=4.9.0",
|
|
13
|
+
"openai>=1.72.0",
|
|
14
|
+
"mcp>=1.12.0",
|
|
15
|
+
"protobuf>=6.31.1",
|
|
16
|
+
"opentelemetry-api>=1.32.0",
|
|
17
|
+
"opentelemetry-sdk>=1.32.0",
|
|
18
|
+
"opentelemetry-exporter-otlp-proto-grpc>=1.32.0",
|
|
19
|
+
"opentelemetry-instrumentation-openai>= 0.39.0",
|
|
20
|
+
"opentelemetry-instrumentation-httpx >= 0.52.0",
|
|
21
|
+
"anthropic[vertex]>=0.49.0",
|
|
22
|
+
"fastapi>=0.115.1",
|
|
23
|
+
"litellm>=1.74.3",
|
|
24
|
+
"google-adk>=1.8.0",
|
|
25
|
+
"google-genai>=1.21.1",
|
|
26
|
+
"google-auth>=2.40.2",
|
|
27
|
+
"httpx>=0.25.0",
|
|
28
|
+
"pydantic>=2.5.0",
|
|
29
|
+
"typing-extensions>=4.8.0",
|
|
30
|
+
"jsonref>=1.1.0",
|
|
31
|
+
"a2a-sdk>=0.2.16",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
test = [
|
|
36
|
+
"pytest>=8.3.5",
|
|
37
|
+
"pytest-asyncio>=0.25.3",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[tool.hatch.build.targets.wheel]
|
|
41
|
+
packages = ["src/kagent_adk"]
|
|
42
|
+
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
extend = "../../pyproject.toml"
|
|
@@ -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"]
|
|
@@ -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()
|
|
@@ -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
|
+
)
|