agent-starter-pack 0.2.3__py3-none-any.whl → 0.3.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 agent-starter-pack might be problematic. Click here for more details.
- {agent_starter_pack-0.2.3.dist-info → agent_starter_pack-0.3.1.dist-info}/METADATA +8 -4
- {agent_starter_pack-0.2.3.dist-info → agent_starter_pack-0.3.1.dist-info}/RECORD +61 -46
- agents/adk_base/README.md +14 -0
- agents/adk_base/app/agent.py +66 -0
- agents/adk_base/notebooks/adk_app_testing.ipynb +305 -0
- agents/adk_base/template/.templateconfig.yaml +21 -0
- agents/adk_base/tests/integration/test_agent.py +58 -0
- agents/agentic_rag/README.md +1 -0
- agents/agentic_rag/app/agent.py +44 -89
- agents/agentic_rag/app/templates.py +0 -25
- agents/agentic_rag/notebooks/adk_app_testing.ipynb +305 -0
- agents/agentic_rag/template/.templateconfig.yaml +3 -1
- agents/agentic_rag/tests/integration/test_agent.py +34 -27
- agents/langgraph_base_react/README.md +1 -1
- agents/langgraph_base_react/template/.templateconfig.yaml +1 -1
- src/base_template/Makefile +9 -0
- src/base_template/README.md +1 -1
- src/base_template/app/__init__.py +3 -0
- src/base_template/app/utils/tracing.py +12 -2
- src/base_template/app/utils/typing.py +54 -4
- src/base_template/deployment/terraform/dev/variables.tf +4 -0
- src/base_template/deployment/terraform/dev/vars/env.tfvars +0 -3
- src/base_template/deployment/terraform/variables.tf +4 -0
- src/base_template/deployment/terraform/vars/env.tfvars +0 -4
- src/base_template/pyproject.toml +5 -3
- src/{deployment_targets/agent_engine → base_template}/tests/unit/test_dummy.py +2 -1
- src/cli/commands/create.py +10 -2
- src/cli/commands/setup_cicd.py +3 -0
- src/cli/utils/gcp.py +1 -1
- src/cli/utils/template.py +32 -25
- src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +2 -1
- src/deployment_targets/agent_engine/app/agent_engine_app.py +62 -11
- src/deployment_targets/agent_engine/app/utils/gcs.py +1 -1
- src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +63 -0
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +9 -2
- src/deployment_targets/cloud_run/app/server.py +41 -15
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +60 -3
- src/deployment_targets/cloud_run/tests/load_test/README.md +1 -1
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +57 -24
- src/frontends/live_api_react/frontend/package-lock.json +3 -3
- src/frontends/streamlit_adk/frontend/side_bar.py +214 -0
- src/frontends/streamlit_adk/frontend/streamlit_app.py +314 -0
- src/frontends/streamlit_adk/frontend/style/app_markdown.py +37 -0
- src/frontends/streamlit_adk/frontend/utils/chat_utils.py +84 -0
- src/frontends/streamlit_adk/frontend/utils/local_chat_history.py +110 -0
- src/frontends/streamlit_adk/frontend/utils/message_editing.py +61 -0
- src/frontends/streamlit_adk/frontend/utils/multimodal_utils.py +223 -0
- src/frontends/streamlit_adk/frontend/utils/stream_handler.py +311 -0
- src/frontends/streamlit_adk/frontend/utils/title_summary.py +129 -0
- src/resources/locks/uv-adk_base-agent_engine.lock +5335 -0
- src/resources/locks/uv-adk_base-cloud_run.lock +5927 -0
- src/resources/locks/uv-agentic_rag-agent_engine.lock +882 -676
- src/resources/locks/uv-agentic_rag-cloud_run.lock +1014 -835
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +712 -606
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +770 -672
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +602 -529
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +763 -665
- src/resources/locks/uv-live_api-cloud_run.lock +760 -662
- agents/agentic_rag/notebooks/evaluating_langgraph_agent.ipynb +0 -1561
- src/base_template/tests/unit/test_utils/test_tracing_exporter.py +0 -140
- src/deployment_targets/cloud_run/tests/unit/test_server.py +0 -124
- {agent_starter_pack-0.2.3.dist-info → agent_starter_pack-0.3.1.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.2.3.dist-info → agent_starter_pack-0.3.1.dist-info}/entry_points.txt +0 -0
- {agent_starter_pack-0.2.3.dist-info → agent_starter_pack-0.3.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
# mypy: disable-error-code="arg-type"
|
|
16
|
+
import json
|
|
17
|
+
import uuid
|
|
18
|
+
from collections.abc import Sequence
|
|
19
|
+
from functools import partial
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
import streamlit as st
|
|
23
|
+
from google.adk.events.event import Event
|
|
24
|
+
from google.genai import types
|
|
25
|
+
from streamlit_feedback import streamlit_feedback
|
|
26
|
+
|
|
27
|
+
from frontend.side_bar import SideBar
|
|
28
|
+
from frontend.style.app_markdown import MARKDOWN_STR
|
|
29
|
+
from frontend.utils.local_chat_history import LocalChatMessageHistory
|
|
30
|
+
from frontend.utils.message_editing import MessageEditing
|
|
31
|
+
from frontend.utils.multimodal_utils import format_content, get_parts_from_files
|
|
32
|
+
from frontend.utils.stream_handler import Client, StreamHandler, get_chain_response
|
|
33
|
+
|
|
34
|
+
USER = "my_user"
|
|
35
|
+
EMPTY_CHAT_NAME = "Empty chat"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def setup_page() -> None:
|
|
39
|
+
"""Configure the Streamlit page settings."""
|
|
40
|
+
st.set_page_config(
|
|
41
|
+
page_title="Playground",
|
|
42
|
+
layout="wide",
|
|
43
|
+
initial_sidebar_state="auto",
|
|
44
|
+
menu_items=None,
|
|
45
|
+
)
|
|
46
|
+
st.title("Playground")
|
|
47
|
+
st.markdown(MARKDOWN_STR, unsafe_allow_html=True)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def initialize_session_state() -> None:
|
|
51
|
+
"""Initialize the session state with default values."""
|
|
52
|
+
if "user_chats" not in st.session_state:
|
|
53
|
+
st.session_state["session_id"] = str(uuid.uuid4())
|
|
54
|
+
st.session_state.uploader_key = 0
|
|
55
|
+
st.session_state.invocation_id = None
|
|
56
|
+
st.session_state.user_id = USER
|
|
57
|
+
st.session_state["gcs_uris_to_be_sent"] = ""
|
|
58
|
+
st.session_state.modified_prompt = None
|
|
59
|
+
st.session_state.session_db = LocalChatMessageHistory(
|
|
60
|
+
session_id=st.session_state["session_id"],
|
|
61
|
+
user_id=st.session_state["user_id"],
|
|
62
|
+
)
|
|
63
|
+
st.session_state.user_chats = (
|
|
64
|
+
st.session_state.session_db.get_all_conversations()
|
|
65
|
+
)
|
|
66
|
+
st.session_state.user_chats[st.session_state["session_id"]] = {
|
|
67
|
+
"title": EMPTY_CHAT_NAME,
|
|
68
|
+
"messages": [],
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def display_messages() -> None:
|
|
73
|
+
"""Display all messages in the current chat session."""
|
|
74
|
+
messages = st.session_state.user_chats[st.session_state["session_id"]]["messages"]
|
|
75
|
+
tool_calls_map = {} # Map tool_call_id to tool call input
|
|
76
|
+
for i, message in enumerate(messages):
|
|
77
|
+
# Convert message to Event if it's not already
|
|
78
|
+
event = message if isinstance(message, Event) else Event.model_validate(message)
|
|
79
|
+
|
|
80
|
+
# Check if this is a model message with function calls
|
|
81
|
+
if hasattr(event.content, "parts") and event.content.parts:
|
|
82
|
+
for part in event.content.parts:
|
|
83
|
+
if hasattr(part, "function_call") and part.function_call:
|
|
84
|
+
# Store function call info for later matching with responses
|
|
85
|
+
tool_calls_map[part.function_call.id] = {
|
|
86
|
+
"id": part.function_call.id,
|
|
87
|
+
"name": part.function_call.name,
|
|
88
|
+
"args": part.function_call.args,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Check if this is a message with function responses
|
|
92
|
+
function_responses = []
|
|
93
|
+
if hasattr(event.content, "parts") and event.content.parts:
|
|
94
|
+
for part in event.content.parts:
|
|
95
|
+
if hasattr(part, "function_response") and part.function_response:
|
|
96
|
+
function_responses.append(part.function_response)
|
|
97
|
+
|
|
98
|
+
# Display function responses if any
|
|
99
|
+
for function_response in function_responses:
|
|
100
|
+
tool_call_id = function_response.id
|
|
101
|
+
if tool_call_id in tool_calls_map:
|
|
102
|
+
# Display the tool output and remove from map
|
|
103
|
+
tool_call = tool_calls_map.pop(tool_call_id, None)
|
|
104
|
+
if tool_call:
|
|
105
|
+
display_tool_output(
|
|
106
|
+
tool_call,
|
|
107
|
+
{
|
|
108
|
+
"type": "tool",
|
|
109
|
+
"content": function_response.response,
|
|
110
|
+
"tool_call_id": tool_call_id,
|
|
111
|
+
},
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# Display regular chat messages (model or user)
|
|
115
|
+
if hasattr(event.content, "role") and event.content.role in ["model", "user"]:
|
|
116
|
+
# Only display if there's text content (skip pure function call messages)
|
|
117
|
+
has_text_content = False
|
|
118
|
+
if hasattr(event.content, "parts"):
|
|
119
|
+
for part in event.content.parts:
|
|
120
|
+
if hasattr(part, "text") and part.text:
|
|
121
|
+
has_text_content = True
|
|
122
|
+
break
|
|
123
|
+
|
|
124
|
+
if has_text_content:
|
|
125
|
+
display_chat_message(message, i)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def display_chat_message(message: dict[str, Any], index: int) -> None:
|
|
129
|
+
"""Display a single chat message with edit, refresh, and delete options."""
|
|
130
|
+
role = "assistant" if message["content"]["role"] == "model" else "user"
|
|
131
|
+
chat_message = st.chat_message(role)
|
|
132
|
+
with chat_message:
|
|
133
|
+
st.markdown(format_content(message["content"]["parts"]), unsafe_allow_html=True)
|
|
134
|
+
col1, col2, col3 = st.columns([2, 2, 94])
|
|
135
|
+
display_message_buttons(message, index, col1, col2, col3)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def display_message_buttons(
|
|
139
|
+
message: dict[str, Any], index: int, col1: Any, col2: Any, col3: Any
|
|
140
|
+
) -> None:
|
|
141
|
+
"""Display edit, refresh, and delete buttons for a chat message."""
|
|
142
|
+
edit_button = f"{index}_edit"
|
|
143
|
+
refresh_button = f"{index}_refresh"
|
|
144
|
+
delete_button = f"{index}_delete"
|
|
145
|
+
|
|
146
|
+
# Extract content from the message, handling the new event structure
|
|
147
|
+
content = ""
|
|
148
|
+
if isinstance(message, dict):
|
|
149
|
+
if "content" in message:
|
|
150
|
+
if isinstance(message["content"], dict) and "parts" in message["content"]:
|
|
151
|
+
parts = message["content"]["parts"]
|
|
152
|
+
if parts:
|
|
153
|
+
if isinstance(parts, list):
|
|
154
|
+
for part in parts:
|
|
155
|
+
if isinstance(part, dict) and "text" in part:
|
|
156
|
+
content += (
|
|
157
|
+
part["text"] if part["text"] is not None else ""
|
|
158
|
+
)
|
|
159
|
+
elif isinstance(parts, str):
|
|
160
|
+
content = parts
|
|
161
|
+
elif isinstance(message["content"], str):
|
|
162
|
+
content = message["content"]
|
|
163
|
+
|
|
164
|
+
with col1:
|
|
165
|
+
st.button(label="✎", key=edit_button, type="primary")
|
|
166
|
+
if message["content"]["role"] == "user":
|
|
167
|
+
with col2:
|
|
168
|
+
st.button(
|
|
169
|
+
label="⟳",
|
|
170
|
+
key=refresh_button,
|
|
171
|
+
type="primary",
|
|
172
|
+
on_click=partial(MessageEditing.refresh_message, st, index, content),
|
|
173
|
+
)
|
|
174
|
+
with col3:
|
|
175
|
+
st.button(
|
|
176
|
+
label="X",
|
|
177
|
+
key=delete_button,
|
|
178
|
+
type="primary",
|
|
179
|
+
on_click=partial(MessageEditing.delete_message, st, index),
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if st.session_state[edit_button]:
|
|
183
|
+
st.text_area(
|
|
184
|
+
"Edit your message:",
|
|
185
|
+
value=content,
|
|
186
|
+
key=f"edit_box_{index}",
|
|
187
|
+
on_change=partial(
|
|
188
|
+
MessageEditing.edit_message, st, index, message["content"]["role"]
|
|
189
|
+
),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def display_tool_output(
|
|
194
|
+
tool_call_input: dict[str, Any], tool_call_output: dict[str, Any]
|
|
195
|
+
) -> None:
|
|
196
|
+
"""Display the input and output of a tool call in an expander."""
|
|
197
|
+
tool_expander = st.expander(label="Tool Calls:", expanded=False)
|
|
198
|
+
with tool_expander:
|
|
199
|
+
msg = (
|
|
200
|
+
f"\n\nEnding tool: `{tool_call_input}` with\n **args:**\n"
|
|
201
|
+
f"```\n{json.dumps(tool_call_input, indent=2)}\n```\n"
|
|
202
|
+
f"\n\n**output:**\n "
|
|
203
|
+
f"```\n{json.dumps(tool_call_output, indent=2)}\n```"
|
|
204
|
+
)
|
|
205
|
+
st.markdown(msg, unsafe_allow_html=True)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def handle_user_input(side_bar: SideBar) -> None:
|
|
209
|
+
"""Process user input, generate AI response, and update chat history."""
|
|
210
|
+
prompt = st.chat_input() or st.session_state.modified_prompt
|
|
211
|
+
if prompt:
|
|
212
|
+
st.session_state.modified_prompt = None
|
|
213
|
+
parts = get_parts_from_files(
|
|
214
|
+
upload_gcs_checkbox=st.session_state.checkbox_state,
|
|
215
|
+
uploaded_files=side_bar.uploaded_files,
|
|
216
|
+
gcs_uris=side_bar.gcs_uris,
|
|
217
|
+
)
|
|
218
|
+
st.session_state["gcs_uris_to_be_sent"] = ""
|
|
219
|
+
parts.append(types.Part(text=prompt))
|
|
220
|
+
st.session_state.user_chats[st.session_state["session_id"]]["messages"].append(
|
|
221
|
+
Event(
|
|
222
|
+
content=types.Content(parts=parts, role="user"), author="user"
|
|
223
|
+
).model_dump()
|
|
224
|
+
)
|
|
225
|
+
display_user_input(parts)
|
|
226
|
+
generate_ai_response(
|
|
227
|
+
remote_agent_engine_id=side_bar.remote_agent_engine_id,
|
|
228
|
+
agent_callable_path=side_bar.agent_callable_path,
|
|
229
|
+
url=side_bar.url_input_field,
|
|
230
|
+
authenticate_request=side_bar.should_authenticate_request,
|
|
231
|
+
)
|
|
232
|
+
update_chat_title()
|
|
233
|
+
if len(parts) > 1:
|
|
234
|
+
st.session_state.uploader_key += 1
|
|
235
|
+
st.rerun()
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def display_user_input(parts: Sequence[dict[str, Any]]) -> None:
|
|
239
|
+
"""Display the user's input in the chat interface."""
|
|
240
|
+
human_message = st.chat_message("human")
|
|
241
|
+
with human_message:
|
|
242
|
+
existing_user_input = format_content(parts)
|
|
243
|
+
st.markdown(existing_user_input, unsafe_allow_html=True)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def generate_ai_response(
|
|
247
|
+
remote_agent_engine_id: str | None = None,
|
|
248
|
+
agent_callable_path: str | None = None,
|
|
249
|
+
url: str | None = None,
|
|
250
|
+
authenticate_request: bool = False,
|
|
251
|
+
) -> None:
|
|
252
|
+
"""Generate and display the AI's response to the user's input."""
|
|
253
|
+
ai_message = st.chat_message("ai")
|
|
254
|
+
with ai_message:
|
|
255
|
+
status = st.status("Generating answer🤖")
|
|
256
|
+
stream_handler = StreamHandler(st=st)
|
|
257
|
+
client = Client(
|
|
258
|
+
remote_agent_engine_id=remote_agent_engine_id,
|
|
259
|
+
agent_callable_path=agent_callable_path,
|
|
260
|
+
url=url,
|
|
261
|
+
authenticate_request=authenticate_request,
|
|
262
|
+
)
|
|
263
|
+
get_chain_response(st=st, client=client, stream_handler=stream_handler)
|
|
264
|
+
status.update(label="Finished!", state="complete", expanded=False)
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def update_chat_title() -> None:
|
|
268
|
+
"""Update the chat title if it's currently empty."""
|
|
269
|
+
if (
|
|
270
|
+
st.session_state.user_chats[st.session_state["session_id"]]["title"]
|
|
271
|
+
== EMPTY_CHAT_NAME
|
|
272
|
+
):
|
|
273
|
+
st.session_state.session_db.set_title(
|
|
274
|
+
st.session_state.user_chats[st.session_state["session_id"]]
|
|
275
|
+
)
|
|
276
|
+
st.session_state.session_db.upsert_session(
|
|
277
|
+
st.session_state.user_chats[st.session_state["session_id"]]
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def display_feedback(side_bar: SideBar) -> None:
|
|
282
|
+
"""Display a feedback component and log the feedback if provided."""
|
|
283
|
+
if st.session_state.invocation_id is not None:
|
|
284
|
+
feedback = streamlit_feedback(
|
|
285
|
+
feedback_type="faces",
|
|
286
|
+
optional_text_label="[Optional] Please provide an explanation",
|
|
287
|
+
key=f"feedback-{st.session_state.invocation_id}",
|
|
288
|
+
)
|
|
289
|
+
if feedback is not None:
|
|
290
|
+
client = Client(
|
|
291
|
+
remote_agent_engine_id=side_bar.remote_agent_engine_id,
|
|
292
|
+
agent_callable_path=side_bar.agent_callable_path,
|
|
293
|
+
url=side_bar.url_input_field,
|
|
294
|
+
authenticate_request=side_bar.should_authenticate_request,
|
|
295
|
+
)
|
|
296
|
+
client.log_feedback(
|
|
297
|
+
feedback_dict=feedback,
|
|
298
|
+
invocation_id=st.session_state.invocation_id,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def main() -> None:
|
|
303
|
+
"""Main function to set up and run the Streamlit app."""
|
|
304
|
+
setup_page()
|
|
305
|
+
initialize_session_state()
|
|
306
|
+
side_bar = SideBar(st=st)
|
|
307
|
+
side_bar.init_side_bar()
|
|
308
|
+
display_messages()
|
|
309
|
+
handle_user_input(side_bar=side_bar)
|
|
310
|
+
display_feedback(side_bar=side_bar)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
MARKDOWN_STR = """
|
|
16
|
+
<style>
|
|
17
|
+
button[kind="primary"] {
|
|
18
|
+
background: none!important;
|
|
19
|
+
border: 0;
|
|
20
|
+
padding: 20!important;
|
|
21
|
+
color: grey !important;
|
|
22
|
+
text-decoration: none;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
border: none !important;
|
|
25
|
+
# float: right;
|
|
26
|
+
}
|
|
27
|
+
button[kind="primary"]:hover {
|
|
28
|
+
text-decoration: none;
|
|
29
|
+
color: white !important;
|
|
30
|
+
}
|
|
31
|
+
button[kind="primary"]:focus {
|
|
32
|
+
outline: none !important;
|
|
33
|
+
box-shadow: none !important;
|
|
34
|
+
color: !important;
|
|
35
|
+
}
|
|
36
|
+
</style>
|
|
37
|
+
"""
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
import os
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
from google.adk.events.event import Event
|
|
21
|
+
|
|
22
|
+
SAVED_CHAT_PATH = str(os.getcwd()) + "/.saved_chats"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def clean_text(text: str) -> str:
|
|
26
|
+
"""Preprocess the input text by removing leading and trailing newlines."""
|
|
27
|
+
if not text:
|
|
28
|
+
return text
|
|
29
|
+
|
|
30
|
+
if text.startswith("\n"):
|
|
31
|
+
text = text[1:]
|
|
32
|
+
if text.endswith("\n"):
|
|
33
|
+
text = text[:-1]
|
|
34
|
+
return text
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def sanitize_messages(
|
|
38
|
+
messages: list[Event | dict],
|
|
39
|
+
) -> list[Event | dict]:
|
|
40
|
+
"""Preprocess and fix the content of messages."""
|
|
41
|
+
for message in messages:
|
|
42
|
+
# Handle Event objects
|
|
43
|
+
if isinstance(message, Event):
|
|
44
|
+
if hasattr(message.content, "parts"):
|
|
45
|
+
for part in message.content.parts:
|
|
46
|
+
if hasattr(part, "text") and part.text:
|
|
47
|
+
part.text = clean_text(part.text)
|
|
48
|
+
# Handle dictionary format
|
|
49
|
+
elif isinstance(message, dict):
|
|
50
|
+
if "content" in message:
|
|
51
|
+
if (
|
|
52
|
+
isinstance(message["content"], dict)
|
|
53
|
+
and "parts" in message["content"]
|
|
54
|
+
):
|
|
55
|
+
for part in message["content"]["parts"]:
|
|
56
|
+
if isinstance(part, dict) and "text" in part:
|
|
57
|
+
part["text"] = clean_text(part["text"])
|
|
58
|
+
elif isinstance(message["content"], str):
|
|
59
|
+
message["content"] = clean_text(message["content"])
|
|
60
|
+
elif isinstance(message["content"], list):
|
|
61
|
+
for part in message["content"]:
|
|
62
|
+
if part.get("type") == "text":
|
|
63
|
+
part["text"] = clean_text(part["text"])
|
|
64
|
+
return messages
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def save_chat(st: Any) -> None:
|
|
68
|
+
"""Save the current chat session to a YAML file."""
|
|
69
|
+
Path(SAVED_CHAT_PATH).mkdir(parents=True, exist_ok=True)
|
|
70
|
+
session_id = st.session_state["session_id"]
|
|
71
|
+
session = st.session_state.user_chats[session_id]
|
|
72
|
+
messages = session.get("messages", [])
|
|
73
|
+
if len(messages) > 0:
|
|
74
|
+
session["messages"] = sanitize_messages(session["messages"])
|
|
75
|
+
filename = f"{session_id}.yaml"
|
|
76
|
+
with open(Path(SAVED_CHAT_PATH) / filename, "w") as file:
|
|
77
|
+
yaml.dump(
|
|
78
|
+
[session],
|
|
79
|
+
file,
|
|
80
|
+
allow_unicode=True,
|
|
81
|
+
default_flow_style=False,
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
st.toast(f"Chat saved to path: ↓ {Path(SAVED_CHAT_PATH) / filename}")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
import os
|
|
16
|
+
from datetime import datetime
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
from frontend.utils.title_summary import DummySummarizer, TitleGenerator
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LocalChatMessageHistory:
|
|
24
|
+
"""Manages local storage and retrieval of chat message history."""
|
|
25
|
+
|
|
26
|
+
def __init__(
|
|
27
|
+
self,
|
|
28
|
+
user_id: str,
|
|
29
|
+
session_id: str = "default",
|
|
30
|
+
base_dir: str = ".streamlit_chats",
|
|
31
|
+
) -> None:
|
|
32
|
+
self.user_id = user_id
|
|
33
|
+
self.session_id = session_id
|
|
34
|
+
self.base_dir = base_dir
|
|
35
|
+
self.user_dir = os.path.join(self.base_dir, self.user_id)
|
|
36
|
+
self.session_file = os.path.join(self.user_dir, f"{session_id}.yaml")
|
|
37
|
+
try:
|
|
38
|
+
self.title_generator = TitleGenerator()
|
|
39
|
+
except: # noqa: E722
|
|
40
|
+
self.title_generator: TitleGenerator = DummySummarizer() # type: ignore
|
|
41
|
+
os.makedirs(self.user_dir, exist_ok=True)
|
|
42
|
+
|
|
43
|
+
def get_session(self, session_id: str) -> None:
|
|
44
|
+
"""Updates the session ID and file path for the current session."""
|
|
45
|
+
self.session_id = session_id
|
|
46
|
+
self.session_file = os.path.join(self.user_dir, f"{session_id}.yaml")
|
|
47
|
+
|
|
48
|
+
def get_all_conversations(self) -> dict[str, dict]:
|
|
49
|
+
"""Retrieves all conversations for the current user."""
|
|
50
|
+
conversations = {}
|
|
51
|
+
for filename in os.listdir(self.user_dir):
|
|
52
|
+
if filename.endswith(".yaml"):
|
|
53
|
+
file_path = os.path.join(self.user_dir, filename)
|
|
54
|
+
with open(file_path) as f:
|
|
55
|
+
conversation = yaml.safe_load(f)
|
|
56
|
+
if not isinstance(conversation, list) or len(conversation) > 1:
|
|
57
|
+
raise ValueError(
|
|
58
|
+
f"""Invalid format in {file_path}.
|
|
59
|
+
YAML file can only contain one conversation with the following
|
|
60
|
+
structure.
|
|
61
|
+
- messages:
|
|
62
|
+
- content: [message text]
|
|
63
|
+
- type: (human or ai)"""
|
|
64
|
+
)
|
|
65
|
+
conversation = conversation[0]
|
|
66
|
+
if "title" not in conversation:
|
|
67
|
+
conversation["title"] = filename
|
|
68
|
+
conversations[filename[:-5]] = conversation
|
|
69
|
+
return dict(
|
|
70
|
+
sorted(conversations.items(), key=lambda x: x[1].get("update_time", ""))
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def upsert_session(self, session: dict) -> None:
|
|
74
|
+
"""Updates or inserts a session into the local storage."""
|
|
75
|
+
session["update_time"] = datetime.now().isoformat()
|
|
76
|
+
with open(self.session_file, "w") as f:
|
|
77
|
+
yaml.dump(
|
|
78
|
+
[session],
|
|
79
|
+
f,
|
|
80
|
+
allow_unicode=True,
|
|
81
|
+
default_flow_style=False,
|
|
82
|
+
encoding="utf-8",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def set_title(self, session: dict) -> None:
|
|
86
|
+
"""
|
|
87
|
+
Set the title for the given session.
|
|
88
|
+
|
|
89
|
+
This method generates a title for the session based on its messages.
|
|
90
|
+
If the session has messages, it appends a special message to prompt
|
|
91
|
+
for title creation, generates the title using a title chain, and
|
|
92
|
+
updates the session with the new title.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
session (dict): A dictionary containing session information,
|
|
96
|
+
including messages.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
None
|
|
100
|
+
"""
|
|
101
|
+
if session["messages"]:
|
|
102
|
+
session["title"] = self.title_generator.summarize(
|
|
103
|
+
events=session["messages"]
|
|
104
|
+
)
|
|
105
|
+
self.upsert_session(session)
|
|
106
|
+
|
|
107
|
+
def clear(self) -> None:
|
|
108
|
+
"""Removes the current session file if it exists."""
|
|
109
|
+
if os.path.exists(self.session_file):
|
|
110
|
+
os.remove(self.session_file)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Copyright 2025 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
|
+
# fmt: off
|
|
16
|
+
|
|
17
|
+
from typing import Any
|
|
18
|
+
|
|
19
|
+
from google.adk.events.event import Event
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MessageEditing:
|
|
23
|
+
"""Provides methods for editing, refreshing, and deleting chat messages."""
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def edit_message(st: Any, button_idx: int, message_type: str) -> None:
|
|
27
|
+
"""Edit a message in the chat history."""
|
|
28
|
+
button_id = f"edit_box_{button_idx}"
|
|
29
|
+
# Handle Event type messages
|
|
30
|
+
message = st.session_state.user_chats[st.session_state["session_id"]]["messages"][button_idx]
|
|
31
|
+
# Convert to Event if it's not already
|
|
32
|
+
event = message if isinstance(message, Event) else Event.model_validate(message)
|
|
33
|
+
# Update the text content in the event
|
|
34
|
+
if hasattr(event.content, 'parts'):
|
|
35
|
+
for part in event.content.parts:
|
|
36
|
+
if hasattr(part, 'text'):
|
|
37
|
+
part.text = st.session_state[button_id]
|
|
38
|
+
break
|
|
39
|
+
# Update the message in the session state
|
|
40
|
+
st.session_state.user_chats[st.session_state["session_id"]]["messages"][button_idx] = event.model_dump()
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def refresh_message(st: Any, button_idx: int, content: str) -> None:
|
|
44
|
+
"""Refresh a message in the chat history."""
|
|
45
|
+
messages = st.session_state.user_chats[st.session_state["session_id"]][
|
|
46
|
+
"messages"
|
|
47
|
+
]
|
|
48
|
+
st.session_state.user_chats[st.session_state["session_id"]][
|
|
49
|
+
"messages"
|
|
50
|
+
] = messages[:button_idx]
|
|
51
|
+
st.session_state.modified_prompt = content
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def delete_message(st: Any, button_idx: int) -> None:
|
|
55
|
+
"""Delete a message from the chat history."""
|
|
56
|
+
messages = st.session_state.user_chats[st.session_state["session_id"]][
|
|
57
|
+
"messages"
|
|
58
|
+
]
|
|
59
|
+
st.session_state.user_chats[st.session_state["session_id"]][
|
|
60
|
+
"messages"
|
|
61
|
+
] = messages[:button_idx]
|