google-adk-extras 0.2.5__py3-none-any.whl → 0.2.7__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.
- google_adk_extras/__init__.py +3 -3
- google_adk_extras/adk_builder.py +15 -292
- google_adk_extras/artifacts/local_folder_artifact_service.py +0 -2
- google_adk_extras/artifacts/mongo_artifact_service.py +0 -1
- google_adk_extras/artifacts/s3_artifact_service.py +0 -1
- google_adk_extras/artifacts/sql_artifact_service.py +0 -1
- google_adk_extras/custom_agent_loader.py +1 -1
- google_adk_extras/enhanced_adk_web_server.py +0 -2
- google_adk_extras/enhanced_fastapi.py +97 -1
- google_adk_extras/memory/mongo_memory_service.py +0 -1
- google_adk_extras/memory/sql_memory_service.py +1 -1
- google_adk_extras/memory/yaml_file_memory_service.py +1 -3
- google_adk_extras/sessions/mongo_session_service.py +0 -1
- google_adk_extras/sessions/redis_session_service.py +1 -1
- google_adk_extras/sessions/yaml_file_session_service.py +0 -2
- google_adk_extras/streaming/__init__.py +12 -0
- google_adk_extras/streaming/streaming_controller.py +262 -0
- {google_adk_extras-0.2.5.dist-info → google_adk_extras-0.2.7.dist-info}/METADATA +12 -34
- google_adk_extras-0.2.7.dist-info/RECORD +32 -0
- google_adk_extras/credentials/__init__.py +0 -34
- google_adk_extras/credentials/github_oauth2_credential_service.py +0 -213
- google_adk_extras/credentials/google_oauth2_credential_service.py +0 -216
- google_adk_extras/credentials/http_basic_auth_credential_service.py +0 -388
- google_adk_extras/credentials/jwt_credential_service.py +0 -345
- google_adk_extras/credentials/microsoft_oauth2_credential_service.py +0 -250
- google_adk_extras/credentials/x_oauth2_credential_service.py +0 -240
- google_adk_extras-0.2.5.dist-info/RECORD +0 -37
- {google_adk_extras-0.2.5.dist-info → google_adk_extras-0.2.7.dist-info}/WHEEL +0 -0
- {google_adk_extras-0.2.5.dist-info → google_adk_extras-0.2.7.dist-info}/licenses/LICENSE +0 -0
- {google_adk_extras-0.2.5.dist-info → google_adk_extras-0.2.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
"""Streaming support (SSE/WebSocket) for google-adk-extras.
|
2
|
+
|
3
|
+
This package provides an optional, persistent bi-directional streaming layer
|
4
|
+
with strict ADK type parity by default. It complements ADK's built-in
|
5
|
+
`/run`, `/run_sse`, and `/run_live` endpoints by offering per-channel
|
6
|
+
subscription and send semantics for chat-style UIs.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from .streaming_controller import StreamingConfig, StreamingController
|
10
|
+
|
11
|
+
__all__ = ["StreamingConfig", "StreamingController"]
|
12
|
+
|
@@ -0,0 +1,262 @@
|
|
1
|
+
import asyncio
|
2
|
+
import time
|
3
|
+
from dataclasses import dataclass, field
|
4
|
+
from typing import Any, Dict, Optional, Callable, Awaitable
|
5
|
+
|
6
|
+
from fastapi import HTTPException
|
7
|
+
from pydantic import BaseModel
|
8
|
+
|
9
|
+
from google.adk.events.event import Event
|
10
|
+
from google.adk.runners import Runner
|
11
|
+
|
12
|
+
|
13
|
+
class StreamingConfig(BaseModel):
|
14
|
+
enable_streaming: bool = False
|
15
|
+
streaming_path_base: str = "/stream"
|
16
|
+
strict_types: bool = True
|
17
|
+
create_session_on_open: bool = True
|
18
|
+
ttl_seconds: int = 900
|
19
|
+
max_queue_size: int = 128
|
20
|
+
max_channels_per_user: int = 20
|
21
|
+
heartbeat_interval: Optional[float] = 20.0
|
22
|
+
reuse_session_policy: str = "per_channel" # "per_channel" or "external"
|
23
|
+
|
24
|
+
|
25
|
+
@dataclass
|
26
|
+
class _Subscriber:
|
27
|
+
queue: "asyncio.Queue[str]"
|
28
|
+
kind: str # "sse" | "ws"
|
29
|
+
|
30
|
+
|
31
|
+
@dataclass
|
32
|
+
class _Channel:
|
33
|
+
channel_id: str
|
34
|
+
app_name: str
|
35
|
+
user_id: str
|
36
|
+
session_id: str
|
37
|
+
in_q: "asyncio.Queue[Any]" = field(default_factory=asyncio.Queue)
|
38
|
+
subscribers: list[_Subscriber] = field(default_factory=list)
|
39
|
+
worker_task: Optional[asyncio.Task] = None
|
40
|
+
created_at: float = field(default_factory=lambda: time.time())
|
41
|
+
last_activity: float = field(default_factory=lambda: time.time())
|
42
|
+
lock: asyncio.Lock = field(default_factory=asyncio.Lock)
|
43
|
+
|
44
|
+
|
45
|
+
class StreamingController:
|
46
|
+
"""Manages streaming channels and workers.
|
47
|
+
|
48
|
+
This controller binds a channel to (app_name, user_id, session_id) and
|
49
|
+
runs a background worker per channel to execute streamed runs and push
|
50
|
+
ADK Event JSON to all subscribers.
|
51
|
+
"""
|
52
|
+
|
53
|
+
def __init__(
|
54
|
+
self,
|
55
|
+
*,
|
56
|
+
config: StreamingConfig,
|
57
|
+
get_runner_async: Callable[[str], Awaitable[Runner]],
|
58
|
+
session_service,
|
59
|
+
) -> None:
|
60
|
+
self._config = config
|
61
|
+
self._get_runner_async = get_runner_async
|
62
|
+
self._session_service = session_service
|
63
|
+
self._channels: Dict[str, _Channel] = {}
|
64
|
+
self._gc_task: Optional[asyncio.Task] = None
|
65
|
+
|
66
|
+
def start(self) -> None:
|
67
|
+
if self._gc_task is None:
|
68
|
+
self._gc_task = asyncio.create_task(self._gc_loop())
|
69
|
+
|
70
|
+
async def stop(self) -> None:
|
71
|
+
if self._gc_task:
|
72
|
+
self._gc_task.cancel()
|
73
|
+
with asyncio.CancelledError:
|
74
|
+
pass
|
75
|
+
self._gc_task = None
|
76
|
+
# Cancel workers
|
77
|
+
for ch in list(self._channels.values()):
|
78
|
+
if ch.worker_task and not ch.worker_task.done():
|
79
|
+
ch.worker_task.cancel()
|
80
|
+
self._channels.clear()
|
81
|
+
|
82
|
+
def _ensure_user_limit(self, user_id: str) -> None:
|
83
|
+
if self._config.max_channels_per_user <= 0:
|
84
|
+
return
|
85
|
+
count = sum(1 for c in self._channels.values() if c.user_id == user_id)
|
86
|
+
if count >= self._config.max_channels_per_user:
|
87
|
+
raise HTTPException(status_code=429, detail="Too many channels for this user")
|
88
|
+
|
89
|
+
async def open_or_bind_channel(
|
90
|
+
self,
|
91
|
+
*,
|
92
|
+
channel_id: str,
|
93
|
+
app_name: str,
|
94
|
+
user_id: str,
|
95
|
+
session_id: Optional[str],
|
96
|
+
) -> _Channel:
|
97
|
+
# Existing channel validation/match
|
98
|
+
if channel_id in self._channels:
|
99
|
+
ch = self._channels[channel_id]
|
100
|
+
if ch.app_name != app_name or ch.user_id != user_id:
|
101
|
+
raise HTTPException(status_code=409, detail="Channel binding conflict")
|
102
|
+
if session_id and session_id != ch.session_id:
|
103
|
+
raise HTTPException(status_code=409, detail="Channel already bound to different session")
|
104
|
+
ch.last_activity = time.time()
|
105
|
+
return ch
|
106
|
+
|
107
|
+
# New channel
|
108
|
+
self._ensure_user_limit(user_id)
|
109
|
+
if not session_id:
|
110
|
+
if not self._config.create_session_on_open:
|
111
|
+
raise HTTPException(status_code=400, detail="sessionId required for this channel")
|
112
|
+
# Create a fresh ADK session
|
113
|
+
create = getattr(self._session_service, "create_session", None)
|
114
|
+
if create is None:
|
115
|
+
# Older ADK interfaces may expose sync variant
|
116
|
+
create = getattr(self._session_service, "create_session_sync", None)
|
117
|
+
if create is None:
|
118
|
+
raise HTTPException(status_code=500, detail="Session service does not support create_session")
|
119
|
+
if asyncio.iscoroutinefunction(create):
|
120
|
+
session = await create(app_name=app_name, user_id=user_id)
|
121
|
+
else:
|
122
|
+
# Call sync and wrap
|
123
|
+
session = create(app_name=app_name, user_id=user_id)
|
124
|
+
session_id = session.id
|
125
|
+
else:
|
126
|
+
# Validate existing session
|
127
|
+
session = await self._session_service.get_session(app_name=app_name, user_id=user_id, session_id=session_id)
|
128
|
+
if not session:
|
129
|
+
raise HTTPException(status_code=404, detail="Session not found")
|
130
|
+
|
131
|
+
ch = _Channel(
|
132
|
+
channel_id=channel_id,
|
133
|
+
app_name=app_name,
|
134
|
+
user_id=user_id,
|
135
|
+
session_id=session_id,
|
136
|
+
in_q=asyncio.Queue(),
|
137
|
+
)
|
138
|
+
self._channels[channel_id] = ch
|
139
|
+
ch.worker_task = asyncio.create_task(self._worker(ch))
|
140
|
+
return ch
|
141
|
+
|
142
|
+
def subscribe(self, channel_id: str, kind: str) -> asyncio.Queue[str]:
|
143
|
+
if channel_id not in self._channels:
|
144
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
145
|
+
q: asyncio.Queue[str] = asyncio.Queue(maxsize=self._config.max_queue_size)
|
146
|
+
self._channels[channel_id].subscribers.append(_Subscriber(queue=q, kind=kind))
|
147
|
+
self._channels[channel_id].last_activity = time.time()
|
148
|
+
return q
|
149
|
+
|
150
|
+
def unsubscribe(self, channel_id: str, q: asyncio.Queue[str]) -> None:
|
151
|
+
ch = self._channels.get(channel_id)
|
152
|
+
if not ch:
|
153
|
+
return
|
154
|
+
ch.subscribers = [s for s in ch.subscribers if s.queue is not q]
|
155
|
+
ch.last_activity = time.time()
|
156
|
+
|
157
|
+
async def enqueue(self, channel_id: str, req: Any) -> None:
|
158
|
+
ch = self._channels.get(channel_id)
|
159
|
+
if not ch:
|
160
|
+
raise HTTPException(status_code=404, detail="Channel not found")
|
161
|
+
# Validate binding
|
162
|
+
if getattr(req, "app_name", None) != ch.app_name or getattr(req, "user_id", None) != ch.user_id or getattr(req, "session_id", None) != ch.session_id:
|
163
|
+
raise HTTPException(status_code=409, detail="Request does not match channel binding")
|
164
|
+
await ch.in_q.put(req)
|
165
|
+
ch.last_activity = time.time()
|
166
|
+
|
167
|
+
async def _worker(self, ch: _Channel) -> None:
|
168
|
+
try:
|
169
|
+
while True:
|
170
|
+
req = await ch.in_q.get()
|
171
|
+
ch.last_activity = time.time()
|
172
|
+
try:
|
173
|
+
runner = await self._get_runner_async(ch.app_name)
|
174
|
+
# Stream events for this request
|
175
|
+
async with _aclosing(
|
176
|
+
runner.run_async(
|
177
|
+
user_id=ch.user_id,
|
178
|
+
session_id=ch.session_id,
|
179
|
+
new_message=req.new_message,
|
180
|
+
state_delta=getattr(req, "state_delta", None),
|
181
|
+
run_config=_maybe_run_config_streaming(True),
|
182
|
+
)
|
183
|
+
) as agen:
|
184
|
+
async for event in agen:
|
185
|
+
await self._broadcast_event(ch, event)
|
186
|
+
except Exception as e: # pragma: no cover - safety
|
187
|
+
await self._broadcast_error(ch, str(e))
|
188
|
+
except asyncio.CancelledError: # worker shutdown
|
189
|
+
return
|
190
|
+
|
191
|
+
async def _broadcast_event(self, ch: _Channel, event: Event) -> None:
|
192
|
+
payload = event.model_dump_json(exclude_none=True, by_alias=True)
|
193
|
+
for sub in list(ch.subscribers):
|
194
|
+
try:
|
195
|
+
sub.queue.put_nowait(payload)
|
196
|
+
except asyncio.QueueFull:
|
197
|
+
# Drop subscriber on backpressure
|
198
|
+
ch.subscribers = [s for s in ch.subscribers if s is not sub]
|
199
|
+
ch.last_activity = time.time()
|
200
|
+
|
201
|
+
async def _broadcast_heartbeat(self, ch: _Channel) -> None:
|
202
|
+
if self._config.heartbeat_interval is None:
|
203
|
+
return
|
204
|
+
payload = '{"event":"heartbeat"}'
|
205
|
+
for sub in list(ch.subscribers):
|
206
|
+
try:
|
207
|
+
sub.queue.put_nowait(payload)
|
208
|
+
except asyncio.QueueFull:
|
209
|
+
ch.subscribers = [s for s in ch.subscribers if s is not sub]
|
210
|
+
|
211
|
+
async def _broadcast_error(self, ch: _Channel, message: str) -> None:
|
212
|
+
payload = '{"error": %s}' % _json_escape(message)
|
213
|
+
for sub in list(ch.subscribers):
|
214
|
+
try:
|
215
|
+
sub.queue.put_nowait(payload)
|
216
|
+
except asyncio.QueueFull:
|
217
|
+
ch.subscribers = [s for s in ch.subscribers if s is not sub]
|
218
|
+
|
219
|
+
async def _gc_loop(self) -> None:
|
220
|
+
try:
|
221
|
+
while True:
|
222
|
+
await asyncio.sleep(min(10, max(1, int(self._config.ttl_seconds / 3))))
|
223
|
+
now = time.time()
|
224
|
+
for cid, ch in list(self._channels.items()):
|
225
|
+
idle = now - ch.last_activity
|
226
|
+
if idle >= self._config.ttl_seconds and not ch.subscribers and ch.in_q.empty():
|
227
|
+
if ch.worker_task and not ch.worker_task.done():
|
228
|
+
ch.worker_task.cancel()
|
229
|
+
self._channels.pop(cid, None)
|
230
|
+
except asyncio.CancelledError:
|
231
|
+
return
|
232
|
+
|
233
|
+
|
234
|
+
# Utilities (avoid importing optional internals at module import time)
|
235
|
+
def _maybe_run_config_streaming(enabled: bool):
|
236
|
+
# Support multiple ADK versions by resolving RunConfig/StreamingMode from
|
237
|
+
# either google.adk.runners or google.adk.agents.run_config
|
238
|
+
try:
|
239
|
+
from google.adk.runners import RunConfig # type: ignore
|
240
|
+
except Exception: # pragma: no cover - version fallback
|
241
|
+
from google.adk.agents.run_config import RunConfig # type: ignore
|
242
|
+
try:
|
243
|
+
from google.adk.agents.run_config import StreamingMode # type: ignore
|
244
|
+
except Exception: # pragma: no cover - defensive
|
245
|
+
StreamingMode = type("StreamingMode", (), {"SSE": "sse", "NONE": None}) # minimal stub
|
246
|
+
return RunConfig(streaming_mode=StreamingMode.SSE if enabled else StreamingMode.NONE)
|
247
|
+
|
248
|
+
|
249
|
+
class _aclosing:
|
250
|
+
def __init__(self, agen):
|
251
|
+
self._agen = agen
|
252
|
+
async def __aenter__(self):
|
253
|
+
return self._agen
|
254
|
+
async def __aexit__(self, exc_type, exc, tb):
|
255
|
+
try:
|
256
|
+
await self._agen.aclose()
|
257
|
+
except Exception:
|
258
|
+
pass
|
259
|
+
|
260
|
+
|
261
|
+
def _json_escape(s: str) -> str:
|
262
|
+
return '"' + s.replace('\\', '\\\\').replace('"', '\\"') + '"'
|
@@ -1,7 +1,7 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: google-adk-extras
|
3
|
-
Version: 0.2.
|
4
|
-
Summary: Production-ready services
|
3
|
+
Version: 0.2.7
|
4
|
+
Summary: Production-ready services and FastAPI wiring for Google ADK
|
5
5
|
Home-page: https://github.com/DeadMeme5441/google-adk-extras
|
6
6
|
Author: DeadMeme5441
|
7
7
|
Author-email: DeadMeme5441 <deadunderscorememe@gmail.com>
|
@@ -49,7 +49,7 @@ Dynamic: requires-python
|
|
49
49
|
[](https://deadmeme5441.github.io/google-adk-extras/)
|
50
50
|
[](https://github.com/DeadMeme5441/google-adk-extras/actions/workflows/docs.yml)
|
51
51
|
|
52
|
-
Production-ready extensions for Google ADK (Agent Development Kit). This library adds durable service backends (sessions, artifacts, memory)
|
52
|
+
Production-ready extensions for Google ADK (Agent Development Kit). This library adds durable service backends (sessions, artifacts, memory) and clean FastAPI wiring (with optional streaming) so you can run ADK agents with real storage.
|
53
53
|
|
54
54
|
What this is not: a fork of ADK. It builds on ADK’s core runtime, agents, tools and callbacks, and drops in where ADK expects services and a web server.
|
55
55
|
|
@@ -58,10 +58,9 @@ What this is not: a fork of ADK. It builds on ADK’s core runtime, agents, tool
|
|
58
58
|
|
59
59
|
ADK provides the core primitives (Runner, Session/State, MemoryService, ArtifactService, CredentialService, Agents/Tools, callbacks, Dev UI, and deployment paths). See the official ADK docs for concepts and APIs.
|
60
60
|
|
61
|
-
This package focuses on
|
61
|
+
This package focuses on a few gaps common in real apps:
|
62
62
|
- Durable storage backends beyond in‑memory defaults
|
63
|
-
-
|
64
|
-
- FastAPI integration that accepts your credential service without hacks
|
63
|
+
- FastAPI integration with optional streaming (SSE/WS)
|
65
64
|
|
66
65
|
|
67
66
|
## Features
|
@@ -69,8 +68,7 @@ This package focuses on three gaps common in real apps:
|
|
69
68
|
- Session services: SQL (SQLite/Postgres/MySQL), MongoDB, Redis, YAML files
|
70
69
|
- Artifact services: Local folder (versioned), S3‑compatible, SQL, MongoDB
|
71
70
|
- Memory services: SQL, MongoDB, Redis, YAML files (term search over text parts)
|
72
|
-
-
|
73
|
-
- Enhanced FastAPI wiring that respects a provided credential service
|
71
|
+
- Enhanced FastAPI wiring for ADK apps (with optional streaming)
|
74
72
|
- Fluent builder (`AdkBuilder`) to assemble a FastAPI app or a Runner
|
75
73
|
- A2A helpers for exposing/consuming agents (see below)
|
76
74
|
|
@@ -95,17 +93,17 @@ If you plan to use specific backends, also install their clients (examples):
|
|
95
93
|
- MongoDB: `uv pip install pymongo`
|
96
94
|
- Redis: `uv pip install redis`
|
97
95
|
- S3: `uv pip install boto3`
|
98
|
-
|
96
|
+
|
97
|
+
Note on credentials (0.2.7): This release removes custom credential services and URI helpers from this package. For outbound credentials used by tools, rely on ADK’s experimental BaseCredentialService (e.g., InMemory/SessionState) or your own ADK-compatible implementation. Inbound API authentication (protecting /run and streaming routes) will be provided as an optional FastAPI layer separately.
|
99
98
|
|
100
99
|
|
101
100
|
## Quickstart (FastAPI)
|
102
101
|
|
103
|
-
Use the fluent builder to wire services
|
102
|
+
Use the fluent builder to wire services. Then run with uvicorn.
|
104
103
|
|
105
104
|
```python
|
106
105
|
# app.py
|
107
106
|
from google_adk_extras import AdkBuilder
|
108
|
-
from google_adk_extras.credentials import GoogleOAuth2CredentialService
|
109
107
|
|
110
108
|
app = (
|
111
109
|
AdkBuilder()
|
@@ -113,11 +111,7 @@ app = (
|
|
113
111
|
.with_session_service("sqlite:///./sessions.db") # or: mongodb://, redis://, yaml://
|
114
112
|
.with_artifact_service("local://./artifacts") # or: s3://bucket, mongodb://, sql://
|
115
113
|
.with_memory_service("yaml://./memory") # or: redis://, mongodb://, sql://
|
116
|
-
|
117
|
-
client_id="…apps.googleusercontent.com",
|
118
|
-
client_secret="…",
|
119
|
-
scopes=["openid", "email", "profile"],
|
120
|
-
))
|
114
|
+
# credentials: rely on ADK defaults or pass an ADK BaseCredentialService if needed
|
121
115
|
.with_web_ui(True) # serve ADK’s dev UI if assets available
|
122
116
|
.with_agent_reload(True)
|
123
117
|
.build_fastapi_app()
|
@@ -251,24 +245,8 @@ app = (
|
|
251
245
|
```
|
252
246
|
|
253
247
|
|
254
|
-
|
255
|
-
|
256
|
-
If you prefer URIs instead of constructing services:
|
257
|
-
|
258
|
-
- Google OAuth2: `oauth2-google://client_id:secret@scopes=openid,email,profile`
|
259
|
-
- GitHub OAuth2: `oauth2-github://client_id:secret@scopes=user,repo`
|
260
|
-
- Microsoft OAuth2: `oauth2-microsoft://<tenant>/<client_id>:<secret>@scopes=User.Read`
|
261
|
-
- X OAuth2: `oauth2-x://client_id:secret@scopes=tweet.read,users.read`
|
262
|
-
- JWT: `jwt://<secret>@algorithm=HS256&issuer=my-app&audience=api.example.com&expiration_minutes=60`
|
263
|
-
- Basic: `basic-auth://username:password@realm=My%20API`
|
264
|
-
|
265
|
-
```python
|
266
|
-
cred = (
|
267
|
-
AdkBuilder()
|
268
|
-
.with_credential_service_uri("jwt://secret@issuer=my-app")
|
269
|
-
._create_credential_service()
|
270
|
-
)
|
271
|
-
```
|
248
|
+
<!-- Credential URI helpers removed. Use ADK’s BaseCredentialService directly if needed,
|
249
|
+
and handle inbound API authentication at FastAPI level. -->
|
272
250
|
|
273
251
|
|
274
252
|
## Notes & limitations
|
@@ -0,0 +1,32 @@
|
|
1
|
+
google_adk_extras/__init__.py,sha256=NnpXFV1q3Y50qmHhbvVp-7wxpd6esrk_gooU7LaHiFM,851
|
2
|
+
google_adk_extras/adk_builder.py,sha256=Ax5e_NegGZcdb_xm4t4e18gpJjcR5ICn0Zefy6VuLh4,30068
|
3
|
+
google_adk_extras/custom_agent_loader.py,sha256=e_sgA58RmDzCUHCySAi3Hruxumtozw3UScZV2vxlCbw,5991
|
4
|
+
google_adk_extras/enhanced_adk_web_server.py,sha256=4QxTADlQv6oXaAEVMQc7-bpf84jCrTkN6pJC6R2jmww,5348
|
5
|
+
google_adk_extras/enhanced_fastapi.py,sha256=U7g24_c5-uQidCSs4Z158FWqR7qflW8O5EY9kqfuLqE,28202
|
6
|
+
google_adk_extras/enhanced_runner.py,sha256=b7O1a9-4S49LduILOEDs6IxjCI4w_E39sc-Hs4y3Rys,1410
|
7
|
+
google_adk_extras/artifacts/__init__.py,sha256=_IsKDgf6wanWR0HXvSpK9SiLa3n5URKLtazkKyH1P-o,931
|
8
|
+
google_adk_extras/artifacts/base_custom_artifact_service.py,sha256=O9rkc250B3yDRYbyDI0EvTrCKvnih5_DQas5OF-hRMY,9721
|
9
|
+
google_adk_extras/artifacts/local_folder_artifact_service.py,sha256=7oepQIHYimXxSjGl3-i1HgZ595H9hZWa3OptsMeyqgU,12509
|
10
|
+
google_adk_extras/artifacts/mongo_artifact_service.py,sha256=K46Ycl7gkzCbCweVL0GrWsFxCcJ3T7JnE9gpIamfd6Y,7099
|
11
|
+
google_adk_extras/artifacts/s3_artifact_service.py,sha256=inIc2KL3OdIQGkCA_HYJE0ZfGFQ3YcX_SFZEFVUb0T8,15655
|
12
|
+
google_adk_extras/artifacts/sql_artifact_service.py,sha256=OovKSzM0nib2c-pzkv7NYyiHi8kFK-k3SFOMi92U4Mk,11980
|
13
|
+
google_adk_extras/credentials/base_custom_credential_service.py,sha256=iYHacJAsZmDfpxLOPYx4tQpbtWTbwC75tRp6hlZFoSg,4014
|
14
|
+
google_adk_extras/memory/__init__.py,sha256=2FFJXw9CZHctKXmCuc-lrdETeQ5xqdivy3oarHJz5gs,994
|
15
|
+
google_adk_extras/memory/base_custom_memory_service.py,sha256=TRQMaXiRg2LXFwYZnFHoL-yBVtecuX1ownyPBJf6Xww,3613
|
16
|
+
google_adk_extras/memory/mongo_memory_service.py,sha256=toXp7lg0el247zxY8HURY_7VxoaAPViZKnbvLCUxdWU,6747
|
17
|
+
google_adk_extras/memory/redis_memory_service.py,sha256=P6vYvP8gv6kCH1lRB0SQld3mzS_JVKMUDtKifXDfu38,7400
|
18
|
+
google_adk_extras/memory/sql_memory_service.py,sha256=cvAOsBZCFN3ruj0weuwvihgAnnvRJMO6HYMZX58rG9k,10760
|
19
|
+
google_adk_extras/memory/yaml_file_memory_service.py,sha256=yx-nPqxXWxg9RI4OiwMAbdXs-eX1nBb1LzSWvib28S0,9909
|
20
|
+
google_adk_extras/sessions/__init__.py,sha256=VgHyPULLzjJD7ShsyABz98rWVND0mOoM6qX74MrTJwA,915
|
21
|
+
google_adk_extras/sessions/base_custom_session_service.py,sha256=npwrSNAtgqN6K7C8e4idiWkFNr_3pcOAiFYpGXu3NnI,8912
|
22
|
+
google_adk_extras/sessions/mongo_session_service.py,sha256=Wo_dnsbnEex5rWeGOj5RABRhMICeaa4x--QBJPSur-4,8317
|
23
|
+
google_adk_extras/sessions/redis_session_service.py,sha256=0G4yHWCngZz5w3UnQEigHXhgAqwttE8BTTYiDA1Hl7c,10428
|
24
|
+
google_adk_extras/sessions/sql_session_service.py,sha256=TaOeEVWnwQ_8nvDZBW7e3qhzR_ecuGsjvZ_kh6Guq8g,14558
|
25
|
+
google_adk_extras/sessions/yaml_file_session_service.py,sha256=g65ptJWAMVN4XQmCxQ0UwnSC2GU1NJ6QRvrwfzSK_xo,11797
|
26
|
+
google_adk_extras/streaming/__init__.py,sha256=rcjmlCJHTlvUiCrx6qNGw5ObCnEtfENkGTvzfEiGL0M,461
|
27
|
+
google_adk_extras/streaming/streaming_controller.py,sha256=Z72k5QgvWBIU2YP8iXlc3D3oWxDYWJo9eygj_KzALYA,10489
|
28
|
+
google_adk_extras-0.2.7.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
29
|
+
google_adk_extras-0.2.7.dist-info/METADATA,sha256=ff9M_h1T_1bvq9RPCrd7z0eI_a9CWX_vnXmLH1qNTMA,10121
|
30
|
+
google_adk_extras-0.2.7.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
31
|
+
google_adk_extras-0.2.7.dist-info/top_level.txt,sha256=DDWgVkz8G8ihPzznxAWyKa2jgJW3F6Fwy__qMddoKTs,18
|
32
|
+
google_adk_extras-0.2.7.dist-info/RECORD,,
|
@@ -1,34 +0,0 @@
|
|
1
|
-
"""Custom credential service implementations for Google ADK.
|
2
|
-
|
3
|
-
Optional services are imported lazily to avoid import-time failures when
|
4
|
-
their third-party dependencies are not installed.
|
5
|
-
"""
|
6
|
-
|
7
|
-
from .base_custom_credential_service import BaseCustomCredentialService
|
8
|
-
from .google_oauth2_credential_service import GoogleOAuth2CredentialService
|
9
|
-
from .github_oauth2_credential_service import GitHubOAuth2CredentialService
|
10
|
-
from .microsoft_oauth2_credential_service import MicrosoftOAuth2CredentialService
|
11
|
-
from .x_oauth2_credential_service import XOAuth2CredentialService
|
12
|
-
from .http_basic_auth_credential_service import (
|
13
|
-
HTTPBasicAuthCredentialService,
|
14
|
-
HTTPBasicAuthWithCredentialsService,
|
15
|
-
)
|
16
|
-
|
17
|
-
# Optional: JWT (requires PyJWT)
|
18
|
-
try:
|
19
|
-
from .jwt_credential_service import JWTCredentialService # type: ignore
|
20
|
-
except Exception: # ImportError or transitive import errors
|
21
|
-
JWTCredentialService = None # type: ignore
|
22
|
-
|
23
|
-
__all__ = [
|
24
|
-
"BaseCustomCredentialService",
|
25
|
-
"GoogleOAuth2CredentialService",
|
26
|
-
"GitHubOAuth2CredentialService",
|
27
|
-
"MicrosoftOAuth2CredentialService",
|
28
|
-
"XOAuth2CredentialService",
|
29
|
-
"HTTPBasicAuthCredentialService",
|
30
|
-
"HTTPBasicAuthWithCredentialsService",
|
31
|
-
]
|
32
|
-
|
33
|
-
if JWTCredentialService is not None:
|
34
|
-
__all__.append("JWTCredentialService")
|
@@ -1,213 +0,0 @@
|
|
1
|
-
"""GitHub OAuth2 credential service implementation."""
|
2
|
-
|
3
|
-
from typing import Optional, List
|
4
|
-
import logging
|
5
|
-
|
6
|
-
from google.adk.auth.credential_service.session_state_credential_service import SessionStateCredentialService
|
7
|
-
from google.adk.auth.credential_service.base_credential_service import CallbackContext
|
8
|
-
from google.adk.auth import AuthConfig, AuthCredential, AuthCredentialTypes
|
9
|
-
from google.adk.auth.auth_credential import OAuth2Auth
|
10
|
-
from fastapi.openapi.models import OAuth2
|
11
|
-
from fastapi.openapi.models import OAuthFlowAuthorizationCode, OAuthFlows
|
12
|
-
|
13
|
-
from .base_custom_credential_service import BaseCustomCredentialService
|
14
|
-
|
15
|
-
logger = logging.getLogger(__name__)
|
16
|
-
|
17
|
-
|
18
|
-
class GitHubOAuth2CredentialService(BaseCustomCredentialService):
|
19
|
-
"""GitHub OAuth2 credential service for handling GitHub authentication flows.
|
20
|
-
|
21
|
-
This service provides pre-configured OAuth2 flows for GitHub APIs including
|
22
|
-
repository access, user information, and organization management.
|
23
|
-
|
24
|
-
Args:
|
25
|
-
client_id: The GitHub OAuth2 client ID from GitHub Developer Settings.
|
26
|
-
client_secret: The GitHub OAuth2 client secret from GitHub Developer Settings.
|
27
|
-
scopes: List of OAuth2 scopes to request. Common scopes include:
|
28
|
-
- "user" - Access to user profile information
|
29
|
-
- "user:email" - Access to user email addresses
|
30
|
-
- "repo" - Full access to repositories
|
31
|
-
- "public_repo" - Access to public repositories only
|
32
|
-
- "admin:org" - Full access to organization data
|
33
|
-
- "read:org" - Read access to organization data
|
34
|
-
- "notifications" - Access to notifications
|
35
|
-
use_session_state: If True, stores credentials in session state. If False,
|
36
|
-
uses in-memory storage. Default is True for persistence.
|
37
|
-
|
38
|
-
Example:
|
39
|
-
```python
|
40
|
-
credential_service = GitHubOAuth2CredentialService(
|
41
|
-
client_id="your-github-client-id",
|
42
|
-
client_secret="your-github-client-secret",
|
43
|
-
scopes=["user", "repo", "read:org"]
|
44
|
-
)
|
45
|
-
await credential_service.initialize()
|
46
|
-
|
47
|
-
# Use with Runner
|
48
|
-
runner = Runner(
|
49
|
-
agent=agent,
|
50
|
-
session_service=session_service,
|
51
|
-
credential_service=credential_service,
|
52
|
-
app_name="my_app"
|
53
|
-
)
|
54
|
-
```
|
55
|
-
"""
|
56
|
-
|
57
|
-
# GitHub OAuth2 endpoints
|
58
|
-
GITHUB_AUTH_URL = "https://github.com/login/oauth/authorize"
|
59
|
-
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token"
|
60
|
-
|
61
|
-
# Common GitHub OAuth2 scopes
|
62
|
-
COMMON_SCOPES = {
|
63
|
-
"user": "Access to user profile information",
|
64
|
-
"user:email": "Access to user email addresses",
|
65
|
-
"user:follow": "Access to follow/unfollow users",
|
66
|
-
"repo": "Full access to public and private repositories",
|
67
|
-
"public_repo": "Access to public repositories only",
|
68
|
-
"repo:status": "Access to commit status",
|
69
|
-
"repo_deployment": "Access to deployment status",
|
70
|
-
"admin:org": "Full control of orgs and teams, read/write org projects",
|
71
|
-
"write:org": "Read and write access to organization membership and projects",
|
72
|
-
"read:org": "Read-only access to organization membership and projects",
|
73
|
-
"admin:public_key": "Full control of user public keys",
|
74
|
-
"write:public_key": "Write access to user public keys",
|
75
|
-
"read:public_key": "Read access to user public keys",
|
76
|
-
"admin:repo_hook": "Full control of repository hooks",
|
77
|
-
"write:repo_hook": "Write access to repository hooks",
|
78
|
-
"read:repo_hook": "Read access to repository hooks",
|
79
|
-
"admin:org_hook": "Full control of organization hooks",
|
80
|
-
"gist": "Write access to gists",
|
81
|
-
"notifications": "Access to notifications",
|
82
|
-
"delete_repo": "Delete repositories",
|
83
|
-
"write:packages": "Upload packages to GitHub Package Registry",
|
84
|
-
"read:packages": "Download packages from GitHub Package Registry",
|
85
|
-
"workflow": "Update GitHub Action workflows"
|
86
|
-
}
|
87
|
-
|
88
|
-
def __init__(
|
89
|
-
self,
|
90
|
-
client_id: str,
|
91
|
-
client_secret: str,
|
92
|
-
scopes: Optional[List[str]] = None,
|
93
|
-
use_session_state: bool = True
|
94
|
-
):
|
95
|
-
"""Initialize the GitHub OAuth2 credential service.
|
96
|
-
|
97
|
-
Args:
|
98
|
-
client_id: GitHub OAuth2 client ID.
|
99
|
-
client_secret: GitHub OAuth2 client secret.
|
100
|
-
scopes: List of OAuth2 scopes to request.
|
101
|
-
use_session_state: Whether to use session state for credential storage.
|
102
|
-
"""
|
103
|
-
super().__init__()
|
104
|
-
self.client_id = client_id
|
105
|
-
self.client_secret = client_secret
|
106
|
-
self.scopes = scopes or ["user", "repo"]
|
107
|
-
self.use_session_state = use_session_state
|
108
|
-
|
109
|
-
# Underlying credential service for storage
|
110
|
-
if use_session_state:
|
111
|
-
self._storage_service = SessionStateCredentialService()
|
112
|
-
else:
|
113
|
-
from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService
|
114
|
-
self._storage_service = InMemoryCredentialService()
|
115
|
-
|
116
|
-
async def _initialize_impl(self) -> None:
|
117
|
-
"""Initialize the GitHub OAuth2 credential service.
|
118
|
-
|
119
|
-
Validates the client credentials and sets up the OAuth2 auth scheme.
|
120
|
-
|
121
|
-
Raises:
|
122
|
-
ValueError: If client_id or client_secret is missing.
|
123
|
-
"""
|
124
|
-
if not self.client_id:
|
125
|
-
raise ValueError("GitHub OAuth2 client_id is required")
|
126
|
-
if not self.client_secret:
|
127
|
-
raise ValueError("GitHub OAuth2 client_secret is required")
|
128
|
-
if not self.scopes:
|
129
|
-
raise ValueError("At least one OAuth2 scope is required")
|
130
|
-
|
131
|
-
# Validate scopes against known GitHub scopes
|
132
|
-
unknown_scopes = set(self.scopes) - set(self.COMMON_SCOPES.keys())
|
133
|
-
if unknown_scopes:
|
134
|
-
logger.warning(f"Unknown GitHub OAuth2 scopes: {unknown_scopes}")
|
135
|
-
|
136
|
-
logger.info(f"Initialized GitHub OAuth2 credential service with scopes: {self.scopes}")
|
137
|
-
|
138
|
-
def create_auth_config(self) -> AuthConfig:
|
139
|
-
"""Create an AuthConfig for GitHub OAuth2 authentication.
|
140
|
-
|
141
|
-
Returns:
|
142
|
-
AuthConfig: Configured auth config for GitHub OAuth2 flow.
|
143
|
-
"""
|
144
|
-
self._check_initialized()
|
145
|
-
|
146
|
-
# Create OAuth2 auth scheme
|
147
|
-
auth_scheme = OAuth2(
|
148
|
-
flows=OAuthFlows(
|
149
|
-
authorizationCode=OAuthFlowAuthorizationCode(
|
150
|
-
authorizationUrl=self.GITHUB_AUTH_URL,
|
151
|
-
tokenUrl=self.GITHUB_TOKEN_URL,
|
152
|
-
scopes={
|
153
|
-
scope: self.COMMON_SCOPES.get(scope, f"GitHub scope: {scope}")
|
154
|
-
for scope in self.scopes
|
155
|
-
}
|
156
|
-
)
|
157
|
-
)
|
158
|
-
)
|
159
|
-
|
160
|
-
# Create OAuth2 credential
|
161
|
-
auth_credential = AuthCredential(
|
162
|
-
auth_type=AuthCredentialTypes.OAUTH2,
|
163
|
-
oauth2=OAuth2Auth(
|
164
|
-
client_id=self.client_id,
|
165
|
-
client_secret=self.client_secret
|
166
|
-
)
|
167
|
-
)
|
168
|
-
|
169
|
-
return AuthConfig(
|
170
|
-
auth_scheme=auth_scheme,
|
171
|
-
raw_auth_credential=auth_credential
|
172
|
-
)
|
173
|
-
|
174
|
-
async def load_credential(
|
175
|
-
self,
|
176
|
-
auth_config: AuthConfig,
|
177
|
-
callback_context: CallbackContext,
|
178
|
-
) -> Optional[AuthCredential]:
|
179
|
-
"""Load GitHub OAuth2 credential from storage.
|
180
|
-
|
181
|
-
Args:
|
182
|
-
auth_config: The auth config containing credential key information.
|
183
|
-
callback_context: The current callback context.
|
184
|
-
|
185
|
-
Returns:
|
186
|
-
Optional[AuthCredential]: The stored credential or None if not found.
|
187
|
-
"""
|
188
|
-
self._check_initialized()
|
189
|
-
return await self._storage_service.load_credential(auth_config, callback_context)
|
190
|
-
|
191
|
-
async def save_credential(
|
192
|
-
self,
|
193
|
-
auth_config: AuthConfig,
|
194
|
-
callback_context: CallbackContext,
|
195
|
-
) -> None:
|
196
|
-
"""Save GitHub OAuth2 credential to storage.
|
197
|
-
|
198
|
-
Args:
|
199
|
-
auth_config: The auth config containing the credential to save.
|
200
|
-
callback_context: The current callback context.
|
201
|
-
"""
|
202
|
-
self._check_initialized()
|
203
|
-
await self._storage_service.save_credential(auth_config, callback_context)
|
204
|
-
|
205
|
-
logger.info(f"Saved GitHub OAuth2 credential for user {callback_context._invocation_context.user_id}")
|
206
|
-
|
207
|
-
def get_supported_scopes(self) -> dict:
|
208
|
-
"""Get dictionary of supported GitHub OAuth2 scopes and their descriptions.
|
209
|
-
|
210
|
-
Returns:
|
211
|
-
dict: Mapping of scope names to descriptions.
|
212
|
-
"""
|
213
|
-
return self.COMMON_SCOPES.copy()
|