Skip to content

Commit bbcc007

Browse files
authored
Merge branch 'main' into fix/handle-circular-refs
2 parents ce68d5b + b53bc55 commit bbcc007

File tree

16 files changed

+438
-326
lines changed

16 files changed

+438
-326
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from . import agent
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""LiteLLM sample agent for SSE text streaming."""
16+
17+
from __future__ import annotations
18+
19+
from google.adk import Agent
20+
from google.adk.models.lite_llm import LiteLlm
21+
22+
root_agent = Agent(
23+
name='litellm_streaming_agent',
24+
model=LiteLlm(model='gemini/gemini-2.5-flash'),
25+
description='A LiteLLM agent used for streaming text responses.',
26+
instruction='You are a verbose assistant',
27+
)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Runs the LiteLLM streaming sample with SSE enabled."""
16+
17+
from __future__ import annotations
18+
19+
import asyncio
20+
21+
from dotenv import load_dotenv
22+
from google.adk.agents.run_config import RunConfig
23+
from google.adk.agents.run_config import StreamingMode
24+
from google.adk.cli.utils import logs
25+
from google.adk.runners import InMemoryRunner
26+
from google.genai import types
27+
28+
from . import agent
29+
30+
load_dotenv(override=True)
31+
logs.log_to_tmp_folder()
32+
33+
34+
async def _run_prompt(
35+
*,
36+
runner: InMemoryRunner,
37+
user_id: str,
38+
session_id: str,
39+
prompt: str,
40+
) -> None:
41+
"""Runs one prompt and prints partial chunks in real time."""
42+
content = types.Content(
43+
role='user',
44+
parts=[types.Part.from_text(text=prompt)],
45+
)
46+
47+
print(f'User: {prompt}')
48+
print('Agent: ', end='', flush=True)
49+
saw_text = False
50+
saw_partial_text = False
51+
52+
# For `adk web`, enable the `Streaming` toggle in the UI to get
53+
# partial SSE responses similar to this script.
54+
async for event in runner.run_async(
55+
user_id=user_id,
56+
session_id=session_id,
57+
new_message=content,
58+
run_config=RunConfig(streaming_mode=StreamingMode.SSE),
59+
):
60+
if not event.content:
61+
continue
62+
text = ''.join(part.text for part in event.content.parts if part.text)
63+
if not text:
64+
continue
65+
66+
if event.partial:
67+
print(text, end='', flush=True)
68+
saw_text = True
69+
saw_partial_text = True
70+
continue
71+
72+
# With SSE mode, ADK emits a final aggregated event after partial chunks.
73+
if not saw_partial_text:
74+
print(text, end='', flush=True)
75+
saw_text = True
76+
77+
if saw_text:
78+
print()
79+
else:
80+
print('(no text response)')
81+
print('------------------------------------')
82+
83+
84+
async def main() -> None:
85+
app_name = 'litellm_streaming_demo'
86+
user_id = 'user_1'
87+
runner = InMemoryRunner(agent=agent.root_agent, app_name=app_name)
88+
session = await runner.session_service.create_session(
89+
app_name=app_name,
90+
user_id=user_id,
91+
)
92+
prompts = [
93+
'Write an essay about the roman empire',
94+
'Now summarize the essay into one sentence.',
95+
]
96+
97+
for prompt in prompts:
98+
await _run_prompt(
99+
runner=runner,
100+
user_id=user_id,
101+
session_id=session.id,
102+
prompt=prompt,
103+
)
104+
105+
106+
if __name__ == '__main__':
107+
asyncio.run(main())

src/google/adk/agents/callback_context.py

Lines changed: 4 additions & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -14,240 +14,9 @@
1414

1515
from __future__ import annotations
1616

17-
from collections.abc import Mapping
18-
from collections.abc import Sequence
19-
from typing import Any
20-
from typing import Optional
21-
from typing import TYPE_CHECKING
22-
23-
from typing_extensions import override
24-
17+
from .context import Context
18+
# Keep ReadonlyContext for backward compatibility
2519
from .readonly_context import ReadonlyContext
2620

27-
if TYPE_CHECKING:
28-
from google.genai import types
29-
30-
from ..artifacts.base_artifact_service import ArtifactVersion
31-
from ..auth.auth_credential import AuthCredential
32-
from ..auth.auth_tool import AuthConfig
33-
from ..events.event import Event
34-
from ..events.event_actions import EventActions
35-
from ..sessions.state import State
36-
from .invocation_context import InvocationContext
37-
38-
39-
class CallbackContext(ReadonlyContext):
40-
"""The context of various callbacks within an agent run."""
41-
42-
def __init__(
43-
self,
44-
invocation_context: InvocationContext,
45-
*,
46-
event_actions: Optional[EventActions] = None,
47-
) -> None:
48-
super().__init__(invocation_context)
49-
50-
from ..events.event_actions import EventActions
51-
from ..sessions.state import State
52-
53-
self._event_actions = event_actions or EventActions()
54-
self._state = State(
55-
value=invocation_context.session.state,
56-
delta=self._event_actions.state_delta,
57-
)
58-
59-
@property
60-
@override
61-
def state(self) -> State:
62-
"""The delta-aware state of the current session.
63-
64-
For any state change, you can mutate this object directly,
65-
e.g. `ctx.state['foo'] = 'bar'`
66-
"""
67-
return self._state
68-
69-
async def load_artifact(
70-
self, filename: str, version: Optional[int] = None
71-
) -> Optional[types.Part]:
72-
"""Loads an artifact attached to the current session.
73-
74-
Args:
75-
filename: The filename of the artifact.
76-
version: The version of the artifact. If None, the latest version will be
77-
returned.
78-
79-
Returns:
80-
The artifact.
81-
"""
82-
if self._invocation_context.artifact_service is None:
83-
raise ValueError("Artifact service is not initialized.")
84-
return await self._invocation_context.artifact_service.load_artifact(
85-
app_name=self._invocation_context.app_name,
86-
user_id=self._invocation_context.user_id,
87-
session_id=self._invocation_context.session.id,
88-
filename=filename,
89-
version=version,
90-
)
91-
92-
async def save_artifact(
93-
self,
94-
filename: str,
95-
artifact: types.Part,
96-
custom_metadata: Optional[dict[str, Any]] = None,
97-
) -> int:
98-
"""Saves an artifact and records it as delta for the current session.
99-
100-
Args:
101-
filename: The filename of the artifact.
102-
artifact: The artifact to save.
103-
custom_metadata: Custom metadata to associate with the artifact.
104-
105-
Returns:
106-
The version of the artifact.
107-
"""
108-
if self._invocation_context.artifact_service is None:
109-
raise ValueError("Artifact service is not initialized.")
110-
version = await self._invocation_context.artifact_service.save_artifact(
111-
app_name=self._invocation_context.app_name,
112-
user_id=self._invocation_context.user_id,
113-
session_id=self._invocation_context.session.id,
114-
filename=filename,
115-
artifact=artifact,
116-
custom_metadata=custom_metadata,
117-
)
118-
self._event_actions.artifact_delta[filename] = version
119-
return version
120-
121-
async def get_artifact_version(
122-
self, filename: str, version: Optional[int] = None
123-
) -> Optional[ArtifactVersion]:
124-
"""Gets artifact version info.
125-
126-
Args:
127-
filename: The filename of the artifact.
128-
version: The version of the artifact. If None, the latest version will be
129-
returned.
130-
131-
Returns:
132-
The artifact version info.
133-
"""
134-
if self._invocation_context.artifact_service is None:
135-
raise ValueError("Artifact service is not initialized.")
136-
return await self._invocation_context.artifact_service.get_artifact_version(
137-
app_name=self._invocation_context.app_name,
138-
user_id=self._invocation_context.user_id,
139-
session_id=self._invocation_context.session.id,
140-
filename=filename,
141-
version=version,
142-
)
143-
144-
async def list_artifacts(self) -> list[str]:
145-
"""Lists the filenames of the artifacts attached to the current session."""
146-
if self._invocation_context.artifact_service is None:
147-
raise ValueError("Artifact service is not initialized.")
148-
return await self._invocation_context.artifact_service.list_artifact_keys(
149-
app_name=self._invocation_context.app_name,
150-
user_id=self._invocation_context.user_id,
151-
session_id=self._invocation_context.session.id,
152-
)
153-
154-
async def save_credential(self, auth_config: AuthConfig) -> None:
155-
"""Saves a credential to the credential service.
156-
157-
Args:
158-
auth_config: The authentication configuration containing the credential.
159-
"""
160-
if self._invocation_context.credential_service is None:
161-
raise ValueError("Credential service is not initialized.")
162-
await self._invocation_context.credential_service.save_credential(
163-
auth_config, self
164-
)
165-
166-
async def load_credential(
167-
self, auth_config: AuthConfig
168-
) -> Optional[AuthCredential]:
169-
"""Loads a credential from the credential service.
170-
171-
Args:
172-
auth_config: The authentication configuration for the credential.
173-
174-
Returns:
175-
The loaded credential, or None if not found.
176-
"""
177-
if self._invocation_context.credential_service is None:
178-
raise ValueError("Credential service is not initialized.")
179-
return await self._invocation_context.credential_service.load_credential(
180-
auth_config, self
181-
)
182-
183-
def get_auth_response(
184-
self, auth_config: AuthConfig
185-
) -> Optional[AuthCredential]:
186-
"""Gets the auth response credential from session state.
187-
188-
This method retrieves an authentication credential that was previously
189-
stored in session state after a user completed an OAuth flow or other
190-
authentication process.
191-
192-
Args:
193-
auth_config: The authentication configuration for the credential.
194-
195-
Returns:
196-
The auth credential from the auth response, or None if not found.
197-
"""
198-
from ..auth.auth_handler import AuthHandler
199-
200-
return AuthHandler(auth_config).get_auth_response(self.state)
201-
202-
async def add_session_to_memory(self) -> None:
203-
"""Triggers memory generation for the current session.
204-
205-
This method saves the current session's events to the memory service,
206-
enabling the agent to recall information from past interactions.
207-
208-
Raises:
209-
ValueError: If memory service is not available.
210-
211-
Example:
212-
```python
213-
async def my_after_agent_callback(callback_context: CallbackContext):
214-
# Save conversation to memory at the end of each interaction
215-
await callback_context.add_session_to_memory()
216-
```
217-
"""
218-
if self._invocation_context.memory_service is None:
219-
raise ValueError(
220-
"Cannot add session to memory: memory service is not available."
221-
)
222-
await self._invocation_context.memory_service.add_session_to_memory(
223-
self._invocation_context.session
224-
)
225-
226-
async def add_events_to_memory(
227-
self,
228-
*,
229-
events: Sequence[Event],
230-
custom_metadata: Mapping[str, object] | None = None,
231-
) -> None:
232-
"""Adds an explicit list of events to the memory service.
233-
234-
Uses this callback's current session identifiers as memory scope.
235-
236-
Args:
237-
events: Explicit events to add to memory.
238-
custom_metadata: Optional standard metadata for memory generation.
239-
240-
Raises:
241-
ValueError: If memory service is not available.
242-
"""
243-
if self._invocation_context.memory_service is None:
244-
raise ValueError(
245-
"Cannot add events to memory: memory service is not available."
246-
)
247-
await self._invocation_context.memory_service.add_events_to_memory(
248-
app_name=self._invocation_context.session.app_name,
249-
user_id=self._invocation_context.session.user_id,
250-
session_id=self._invocation_context.session.id,
251-
events=events,
252-
custom_metadata=custom_metadata,
253-
)
21+
# CallbackContext is unified into Context
22+
CallbackContext = Context

0 commit comments

Comments
 (0)