agent-starter-pack 0.0.1b0__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.

Files changed (162) hide show
  1. agent_starter_pack-0.0.1b0.dist-info/METADATA +143 -0
  2. agent_starter_pack-0.0.1b0.dist-info/RECORD +162 -0
  3. agent_starter_pack-0.0.1b0.dist-info/WHEEL +4 -0
  4. agent_starter_pack-0.0.1b0.dist-info/entry_points.txt +2 -0
  5. agent_starter_pack-0.0.1b0.dist-info/licenses/LICENSE +201 -0
  6. agents/agentic_rag_vertexai_search/README.md +22 -0
  7. agents/agentic_rag_vertexai_search/app/agent.py +145 -0
  8. agents/agentic_rag_vertexai_search/app/retrievers.py +79 -0
  9. agents/agentic_rag_vertexai_search/app/templates.py +53 -0
  10. agents/agentic_rag_vertexai_search/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  11. agents/agentic_rag_vertexai_search/template/.templateconfig.yaml +14 -0
  12. agents/agentic_rag_vertexai_search/tests/integration/test_agent.py +57 -0
  13. agents/crewai_coding_crew/README.md +34 -0
  14. agents/crewai_coding_crew/app/agent.py +86 -0
  15. agents/crewai_coding_crew/app/crew/config/agents.yaml +39 -0
  16. agents/crewai_coding_crew/app/crew/config/tasks.yaml +37 -0
  17. agents/crewai_coding_crew/app/crew/crew.py +71 -0
  18. agents/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +1571 -0
  19. agents/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  20. agents/crewai_coding_crew/template/.templateconfig.yaml +12 -0
  21. agents/crewai_coding_crew/tests/integration/test_agent.py +47 -0
  22. agents/langgraph_base_react/README.md +9 -0
  23. agents/langgraph_base_react/app/agent.py +73 -0
  24. agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
  25. agents/langgraph_base_react/template/.templateconfig.yaml +13 -0
  26. agents/langgraph_base_react/tests/integration/test_agent.py +48 -0
  27. agents/multimodal_live_api/README.md +50 -0
  28. agents/multimodal_live_api/app/agent.py +86 -0
  29. agents/multimodal_live_api/app/server.py +193 -0
  30. agents/multimodal_live_api/app/templates.py +51 -0
  31. agents/multimodal_live_api/app/vector_store.py +55 -0
  32. agents/multimodal_live_api/template/.templateconfig.yaml +15 -0
  33. agents/multimodal_live_api/tests/integration/test_server_e2e.py +254 -0
  34. agents/multimodal_live_api/tests/load_test/load_test.py +40 -0
  35. agents/multimodal_live_api/tests/unit/test_server.py +143 -0
  36. src/base_template/.gitignore +197 -0
  37. src/base_template/Makefile +37 -0
  38. src/base_template/README.md +91 -0
  39. src/base_template/app/utils/tracing.py +143 -0
  40. src/base_template/app/utils/typing.py +115 -0
  41. src/base_template/deployment/README.md +123 -0
  42. src/base_template/deployment/cd/deploy-to-prod.yaml +98 -0
  43. src/base_template/deployment/cd/staging.yaml +215 -0
  44. src/base_template/deployment/ci/pr_checks.yaml +51 -0
  45. src/base_template/deployment/terraform/apis.tf +34 -0
  46. src/base_template/deployment/terraform/build_triggers.tf +122 -0
  47. src/base_template/deployment/terraform/dev/apis.tf +42 -0
  48. src/base_template/deployment/terraform/dev/iam.tf +90 -0
  49. src/base_template/deployment/terraform/dev/log_sinks.tf +66 -0
  50. src/base_template/deployment/terraform/dev/providers.tf +29 -0
  51. src/base_template/deployment/terraform/dev/storage.tf +76 -0
  52. src/base_template/deployment/terraform/dev/variables.tf +126 -0
  53. src/base_template/deployment/terraform/dev/vars/env.tfvars +21 -0
  54. src/base_template/deployment/terraform/iam.tf +130 -0
  55. src/base_template/deployment/terraform/locals.tf +50 -0
  56. src/base_template/deployment/terraform/log_sinks.tf +72 -0
  57. src/base_template/deployment/terraform/providers.tf +35 -0
  58. src/base_template/deployment/terraform/service_accounts.tf +42 -0
  59. src/base_template/deployment/terraform/storage.tf +100 -0
  60. src/base_template/deployment/terraform/variables.tf +202 -0
  61. src/base_template/deployment/terraform/vars/env.tfvars +43 -0
  62. src/base_template/pyproject.toml +113 -0
  63. src/base_template/tests/unit/test_utils/test_tracing_exporter.py +140 -0
  64. src/cli/commands/create.py +534 -0
  65. src/cli/commands/setup_cicd.py +730 -0
  66. src/cli/main.py +35 -0
  67. src/cli/utils/__init__.py +35 -0
  68. src/cli/utils/cicd.py +662 -0
  69. src/cli/utils/gcp.py +120 -0
  70. src/cli/utils/logging.py +51 -0
  71. src/cli/utils/template.py +644 -0
  72. src/data_ingestion/README.md +79 -0
  73. src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +175 -0
  74. src/data_ingestion/data_ingestion_pipeline/components/process_data.py +321 -0
  75. src/data_ingestion/data_ingestion_pipeline/pipeline.py +58 -0
  76. src/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +184 -0
  77. src/data_ingestion/pyproject.toml +17 -0
  78. src/data_ingestion/uv.lock +999 -0
  79. src/deployment_targets/agent_engine/app/agent_engine_app.py +238 -0
  80. src/deployment_targets/agent_engine/app/utils/gcs.py +42 -0
  81. src/deployment_targets/agent_engine/deployment_metadata.json +4 -0
  82. src/deployment_targets/agent_engine/notebooks/intro_reasoning_engine.ipynb +869 -0
  83. src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +120 -0
  84. src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
  85. src/deployment_targets/agent_engine/tests/load_test/.results/report.html +264 -0
  86. src/deployment_targets/agent_engine/tests/load_test/.results/results_exceptions.csv +1 -0
  87. src/deployment_targets/agent_engine/tests/load_test/.results/results_failures.csv +1 -0
  88. src/deployment_targets/agent_engine/tests/load_test/.results/results_stats.csv +3 -0
  89. src/deployment_targets/agent_engine/tests/load_test/.results/results_stats_history.csv +22 -0
  90. src/deployment_targets/agent_engine/tests/load_test/README.md +42 -0
  91. src/deployment_targets/agent_engine/tests/load_test/load_test.py +100 -0
  92. src/deployment_targets/agent_engine/tests/unit/test_dummy.py +22 -0
  93. src/deployment_targets/cloud_run/Dockerfile +29 -0
  94. src/deployment_targets/cloud_run/app/server.py +128 -0
  95. src/deployment_targets/cloud_run/deployment/terraform/artifact_registry.tf +22 -0
  96. src/deployment_targets/cloud_run/deployment/terraform/dev/service_accounts.tf +20 -0
  97. src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +192 -0
  98. src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
  99. src/deployment_targets/cloud_run/tests/load_test/README.md +79 -0
  100. src/deployment_targets/cloud_run/tests/load_test/load_test.py +85 -0
  101. src/deployment_targets/cloud_run/tests/unit/test_server.py +142 -0
  102. src/deployment_targets/cloud_run/uv.lock +6952 -0
  103. src/frontends/live_api_react/frontend/package-lock.json +19405 -0
  104. src/frontends/live_api_react/frontend/package.json +56 -0
  105. src/frontends/live_api_react/frontend/public/favicon.ico +0 -0
  106. src/frontends/live_api_react/frontend/public/index.html +62 -0
  107. src/frontends/live_api_react/frontend/public/robots.txt +3 -0
  108. src/frontends/live_api_react/frontend/src/App.scss +189 -0
  109. src/frontends/live_api_react/frontend/src/App.test.tsx +25 -0
  110. src/frontends/live_api_react/frontend/src/App.tsx +205 -0
  111. src/frontends/live_api_react/frontend/src/components/audio-pulse/AudioPulse.tsx +64 -0
  112. src/frontends/live_api_react/frontend/src/components/audio-pulse/audio-pulse.scss +68 -0
  113. src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +217 -0
  114. src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +201 -0
  115. src/frontends/live_api_react/frontend/src/components/logger/Logger.tsx +241 -0
  116. src/frontends/live_api_react/frontend/src/components/logger/logger.scss +133 -0
  117. src/frontends/live_api_react/frontend/src/components/logger/mock-logs.ts +151 -0
  118. src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +161 -0
  119. src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +285 -0
  120. src/frontends/live_api_react/frontend/src/contexts/LiveAPIContext.tsx +48 -0
  121. src/frontends/live_api_react/frontend/src/hooks/use-live-api.ts +115 -0
  122. src/frontends/live_api_react/frontend/src/hooks/use-media-stream-mux.ts +23 -0
  123. src/frontends/live_api_react/frontend/src/hooks/use-screen-capture.ts +72 -0
  124. src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts +69 -0
  125. src/frontends/live_api_react/frontend/src/index.css +28 -0
  126. src/frontends/live_api_react/frontend/src/index.tsx +35 -0
  127. src/frontends/live_api_react/frontend/src/multimodal-live-types.ts +242 -0
  128. src/frontends/live_api_react/frontend/src/react-app-env.d.ts +17 -0
  129. src/frontends/live_api_react/frontend/src/reportWebVitals.ts +31 -0
  130. src/frontends/live_api_react/frontend/src/setupTests.ts +21 -0
  131. src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts +111 -0
  132. src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts +270 -0
  133. src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts +43 -0
  134. src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +329 -0
  135. src/frontends/live_api_react/frontend/src/utils/store-logger.ts +64 -0
  136. src/frontends/live_api_react/frontend/src/utils/utils.ts +86 -0
  137. src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts +73 -0
  138. src/frontends/live_api_react/frontend/src/utils/worklets/vol-meter.ts +65 -0
  139. src/frontends/live_api_react/frontend/tsconfig.json +25 -0
  140. src/frontends/streamlit/frontend/side_bar.py +213 -0
  141. src/frontends/streamlit/frontend/streamlit_app.py +263 -0
  142. src/frontends/streamlit/frontend/style/app_markdown.py +37 -0
  143. src/frontends/streamlit/frontend/utils/chat_utils.py +67 -0
  144. src/frontends/streamlit/frontend/utils/local_chat_history.py +125 -0
  145. src/frontends/streamlit/frontend/utils/message_editing.py +59 -0
  146. src/frontends/streamlit/frontend/utils/multimodal_utils.py +217 -0
  147. src/frontends/streamlit/frontend/utils/stream_handler.py +282 -0
  148. src/frontends/streamlit/frontend/utils/title_summary.py +77 -0
  149. src/resources/containers/data_processing/Dockerfile +25 -0
  150. src/resources/locks/uv-agentic_rag_vertexai_search-agent_engine.lock +4684 -0
  151. src/resources/locks/uv-agentic_rag_vertexai_search-cloud_run.lock +5799 -0
  152. src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +5509 -0
  153. src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +6688 -0
  154. src/resources/locks/uv-langgraph_base_react-agent_engine.lock +4595 -0
  155. src/resources/locks/uv-langgraph_base_react-cloud_run.lock +5710 -0
  156. src/resources/locks/uv-multimodal_live_api-cloud_run.lock +5665 -0
  157. src/resources/setup_cicd/cicd_variables.tf +36 -0
  158. src/resources/setup_cicd/github.tf +85 -0
  159. src/resources/setup_cicd/providers.tf +39 -0
  160. src/utils/generate_locks.py +135 -0
  161. src/utils/lock_utils.py +82 -0
  162. src/utils/watch_and_rebuild.py +190 -0
@@ -0,0 +1,125 @@
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
+ from langchain_core.chat_history import BaseChatMessageHistory
20
+
21
+ from frontend.utils.title_summary import chain_title
22
+
23
+
24
+ class LocalChatMessageHistory(BaseChatMessageHistory):
25
+ """Manages local storage and retrieval of chat message history."""
26
+
27
+ def __init__(
28
+ self,
29
+ user_id: str,
30
+ session_id: str = "default",
31
+ base_dir: str = ".streamlit_chats",
32
+ ) -> None:
33
+ self.user_id = user_id
34
+ self.session_id = session_id
35
+ self.base_dir = base_dir
36
+ self.user_dir = os.path.join(self.base_dir, self.user_id)
37
+ self.session_file = os.path.join(self.user_dir, f"{session_id}.yaml")
38
+
39
+ os.makedirs(self.user_dir, exist_ok=True)
40
+
41
+ def get_session(self, session_id: str) -> None:
42
+ """Updates the session ID and file path for the current session."""
43
+ self.session_id = session_id
44
+ self.session_file = os.path.join(self.user_dir, f"{session_id}.yaml")
45
+
46
+ def get_all_conversations(self) -> dict[str, dict]:
47
+ """Retrieves all conversations for the current user."""
48
+ conversations = {}
49
+ for filename in os.listdir(self.user_dir):
50
+ if filename.endswith(".yaml"):
51
+ file_path = os.path.join(self.user_dir, filename)
52
+ with open(file_path) as f:
53
+ conversation = yaml.safe_load(f)
54
+ if not isinstance(conversation, list) or len(conversation) > 1:
55
+ raise ValueError(
56
+ f"""Invalid format in {file_path}.
57
+ YAML file can only contain one conversation with the following
58
+ structure.
59
+ - messages:
60
+ - content: [message text]
61
+ - type: (human or ai)"""
62
+ )
63
+ conversation = conversation[0]
64
+ if "title" not in conversation:
65
+ conversation["title"] = filename
66
+ conversations[filename[:-5]] = conversation
67
+ return dict(
68
+ sorted(conversations.items(), key=lambda x: x[1].get("update_time", ""))
69
+ )
70
+
71
+ def upsert_session(self, session: dict) -> None:
72
+ """Updates or inserts a session into the local storage."""
73
+ session["update_time"] = datetime.now().isoformat()
74
+ with open(self.session_file, "w") as f:
75
+ yaml.dump(
76
+ [session],
77
+ f,
78
+ allow_unicode=True,
79
+ default_flow_style=False,
80
+ encoding="utf-8",
81
+ )
82
+
83
+ def set_title(self, session: dict) -> None:
84
+ """
85
+ Set the title for the given session.
86
+
87
+ This method generates a title for the session based on its messages.
88
+ If the session has messages, it appends a special message to prompt
89
+ for title creation, generates the title using a title chain, and
90
+ updates the session with the new title.
91
+
92
+ Args:
93
+ session (dict): A dictionary containing session information,
94
+ including messages.
95
+
96
+ Returns:
97
+ None
98
+ """
99
+ if session["messages"]:
100
+ messages = session["messages"] + [
101
+ {
102
+ "type": "human",
103
+ "content": "End of conversation - Create one single title",
104
+ }
105
+ ]
106
+ # Remove the tool calls from conversation
107
+ messages = [
108
+ msg
109
+ for msg in messages
110
+ if msg["type"] in ("ai", "human") and isinstance(msg["content"], str)
111
+ ]
112
+
113
+ response = chain_title.invoke(messages)
114
+ title = (
115
+ response.content.strip()
116
+ if isinstance(response.content, str)
117
+ else str(response.content)
118
+ )
119
+ session["title"] = title
120
+ self.upsert_session(session)
121
+
122
+ def clear(self) -> None:
123
+ """Removes the current session file if it exists."""
124
+ if os.path.exists(self.session_file):
125
+ os.remove(self.session_file)
@@ -0,0 +1,59 @@
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
+
20
+ class MessageEditing:
21
+ """Provides methods for editing, refreshing, and deleting chat messages."""
22
+
23
+ @staticmethod
24
+ def edit_message(st: Any, button_idx: int, message_type: str) -> None:
25
+ """Edit a message in the chat history."""
26
+ button_id = f"edit_box_{button_idx}"
27
+ if message_type == "human":
28
+ messages = st.session_state.user_chats[st.session_state["session_id"]][
29
+ "messages"
30
+ ]
31
+ st.session_state.user_chats[st.session_state["session_id"]][
32
+ "messages"
33
+ ] = messages[:button_idx]
34
+ st.session_state.modified_prompt = st.session_state[button_id]
35
+ else:
36
+ st.session_state.user_chats[st.session_state["session_id"]]["messages"][
37
+ button_idx
38
+ ]["content"] = st.session_state[button_id]
39
+
40
+ @staticmethod
41
+ def refresh_message(st: Any, button_idx: int, content: str) -> None:
42
+ """Refresh a message in the chat history."""
43
+ messages = st.session_state.user_chats[st.session_state["session_id"]][
44
+ "messages"
45
+ ]
46
+ st.session_state.user_chats[st.session_state["session_id"]][
47
+ "messages"
48
+ ] = messages[:button_idx]
49
+ st.session_state.modified_prompt = content
50
+
51
+ @staticmethod
52
+ def delete_message(st: Any, button_idx: int) -> None:
53
+ """Delete a message from the chat history."""
54
+ messages = st.session_state.user_chats[st.session_state["session_id"]][
55
+ "messages"
56
+ ]
57
+ st.session_state.user_chats[st.session_state["session_id"]][
58
+ "messages"
59
+ ] = messages[:button_idx]
@@ -0,0 +1,217 @@
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 base64
16
+ from typing import Any
17
+ from urllib.parse import quote
18
+
19
+ from google.cloud import storage
20
+
21
+ HELP_MESSAGE_MULTIMODALITY = (
22
+ "For Gemini models to access the URIs you provide, store them in "
23
+ "Google Cloud Storage buckets within the same project used by Gemini."
24
+ )
25
+
26
+ HELP_GCS_CHECKBOX = (
27
+ "Enabling GCS upload will increase the app observability by avoiding"
28
+ " forwarding and logging large byte strings within the app."
29
+ )
30
+
31
+
32
+ def format_content(content: str | list[dict[str, Any]]) -> str:
33
+ """Formats content as a string, handling both text and multimedia inputs."""
34
+ if isinstance(content, str):
35
+ return content
36
+ if len(content) == 1 and content[0]["type"] == "text":
37
+ return content[0]["text"]
38
+ markdown = """Media:
39
+ """
40
+ text = ""
41
+ for part in content:
42
+ if part["type"] == "text":
43
+ text = part["text"]
44
+ # Local Images:
45
+ if part["type"] == "image_url":
46
+ image_url = part["image_url"]["url"]
47
+ image_markdown = f'<img src="{image_url}" width="100">'
48
+ markdown = (
49
+ markdown
50
+ + f"""
51
+ - {image_markdown}
52
+ """
53
+ )
54
+ if part["type"] == "media":
55
+ # Local other media
56
+ if "data" in part:
57
+ markdown = markdown + f"- Local media: {part['file_name']}\n"
58
+ # From GCS:
59
+ if "file_uri" in part:
60
+ # GCS images
61
+ if "image" in part["mime_type"]:
62
+ image_url = gs_uri_to_https_url(part["file_uri"])
63
+ image_markdown = f'<img src="{image_url}" width="100">'
64
+ markdown = (
65
+ markdown
66
+ + f"""
67
+ - {image_markdown}
68
+ """
69
+ )
70
+ # GCS other media
71
+ else:
72
+ image_url = gs_uri_to_https_url(part["file_uri"])
73
+ markdown = (
74
+ markdown + f"- Remote media: "
75
+ f"[{part['file_uri']}]({image_url})\n"
76
+ )
77
+ markdown = (
78
+ markdown
79
+ + f"""
80
+
81
+ {text}"""
82
+ )
83
+ return markdown
84
+
85
+
86
+ def get_gcs_blob_mime_type(gcs_uri: str) -> str | None:
87
+ """Fetches the MIME type (content type) of a Google Cloud Storage blob.
88
+
89
+ Args:
90
+ gcs_uri (str): The GCS URI of the blob in the format "gs://bucket-name/object-name".
91
+
92
+ Returns:
93
+ str: The MIME type of the blob (e.g., "image/jpeg", "text/plain") if found,
94
+ or None if the blob does not exist or an error occurs.
95
+ """
96
+ storage_client = storage.Client()
97
+
98
+ try:
99
+ bucket_name, object_name = gcs_uri.replace("gs://", "").split("/", 1)
100
+
101
+ bucket = storage_client.bucket(bucket_name)
102
+ blob = bucket.blob(object_name)
103
+ blob.reload()
104
+ return blob.content_type
105
+ except Exception as e:
106
+ print(f"Error retrieving MIME type for {gcs_uri}: {e}")
107
+ return None # Indicate failure
108
+
109
+
110
+ def get_parts_from_files(
111
+ upload_gcs_checkbox: bool, uploaded_files: list[Any], gcs_uris: str
112
+ ) -> list[dict[str, Any]]:
113
+ """Processes uploaded files and GCS URIs to create a list of content parts."""
114
+ parts = []
115
+ # read from local directly
116
+ if not upload_gcs_checkbox:
117
+ for uploaded_file in uploaded_files:
118
+ im_bytes = uploaded_file.read()
119
+ if "image" in uploaded_file.type:
120
+ content = {
121
+ "type": "image_url",
122
+ "image_url": {
123
+ "url": f"data:{uploaded_file.type};base64,"
124
+ f"{base64.b64encode(im_bytes).decode('utf-8')}"
125
+ },
126
+ "file_name": uploaded_file.name,
127
+ }
128
+ else:
129
+ content = {
130
+ "type": "media",
131
+ "data": base64.b64encode(im_bytes).decode("utf-8"),
132
+ "file_name": uploaded_file.name,
133
+ "mime_type": uploaded_file.type,
134
+ }
135
+
136
+ parts.append(content)
137
+ if gcs_uris != "":
138
+ for uri in gcs_uris.split(","):
139
+ content = {
140
+ "type": "media",
141
+ "file_uri": uri,
142
+ "mime_type": get_gcs_blob_mime_type(uri),
143
+ }
144
+ parts.append(content)
145
+ return parts
146
+
147
+
148
+ def upload_bytes_to_gcs(
149
+ bucket_name: str,
150
+ blob_name: str,
151
+ file_bytes: bytes,
152
+ content_type: str | None = None,
153
+ ) -> str:
154
+ """Uploads a bytes object to Google Cloud Storage and returns the GCS URI.
155
+
156
+ Args:
157
+ bucket_name: The name of the GCS bucket.
158
+ blob_name: The desired name for the uploaded file in GCS.
159
+ file_bytes: The file's content as a bytes object.
160
+ content_type (optional): The MIME type of the file (e.g., "image/png").
161
+ If not provided, GCS will try to infer it.
162
+
163
+ Returns:
164
+ str: The GCS URI (gs://bucket_name/blob_name) of the uploaded file.
165
+
166
+ Raises:
167
+ GoogleCloudError: If there's an issue with the GCS operation.
168
+ """
169
+ storage_client = storage.Client()
170
+ bucket = storage_client.bucket(bucket_name)
171
+ blob = bucket.blob(blob_name)
172
+ blob.upload_from_string(data=file_bytes, content_type=content_type)
173
+ # Construct and return the GCS URI
174
+ gcs_uri = f"gs://{bucket_name}/{blob_name}"
175
+ return gcs_uri
176
+
177
+
178
+ def gs_uri_to_https_url(gs_uri: str) -> str:
179
+ """Converts a GS URI to an HTTPS URL without authentication.
180
+
181
+ Args:
182
+ gs_uri: The GS URI in the format gs://<bucket>/<object>.
183
+
184
+ Returns:
185
+ The corresponding HTTPS URL, or None if the GS URI is invalid.
186
+ """
187
+
188
+ if not gs_uri.startswith("gs://"):
189
+ raise ValueError("Invalid GS URI format")
190
+
191
+ gs_uri = gs_uri[5:]
192
+
193
+ # Extract bucket and object names, then URL encode the object name
194
+ bucket_name, object_name = gs_uri.split("/", 1)
195
+ object_name = quote(object_name)
196
+
197
+ # Construct the HTTPS URL
198
+ https_url = f"https://storage.mtls.cloud.google.com/{bucket_name}/{object_name}"
199
+ return https_url
200
+
201
+
202
+ def upload_files_to_gcs(st: Any, bucket_name: str, files_to_upload: list[Any]) -> None:
203
+ """Upload multiple files to Google Cloud Storage and store URIs in session state."""
204
+ bucket_name = bucket_name.replace("gs://", "")
205
+ uploaded_uris = []
206
+ for file in files_to_upload:
207
+ if file:
208
+ file_bytes = file.read()
209
+ gcs_uri = upload_bytes_to_gcs(
210
+ bucket_name=bucket_name,
211
+ blob_name=file.name,
212
+ file_bytes=file_bytes,
213
+ content_type=file.type,
214
+ )
215
+ uploaded_uris.append(gcs_uri)
216
+ st.session_state.uploader_key += 1
217
+ st.session_state["gcs_uris_to_be_sent"] = ",".join(uploaded_uris)
@@ -0,0 +1,282 @@
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="unreachable"
16
+ import importlib
17
+ import json
18
+ import uuid
19
+ from collections.abc import Generator
20
+ from typing import Any
21
+ from urllib.parse import urljoin
22
+
23
+ import google.auth
24
+ import google.auth.transport.requests
25
+ import google.oauth2.id_token
26
+ import requests
27
+ import streamlit as st
28
+ import vertexai
29
+ from google.auth.exceptions import DefaultCredentialsError
30
+ from langchain_core.messages import AIMessage, ToolMessage
31
+ from vertexai.preview import reasoning_engines
32
+
33
+ from frontend.utils.multimodal_utils import format_content
34
+
35
+
36
+ @st.cache_resource
37
+ def get_remote_agent(remote_agent_engine_id: str) -> Any:
38
+ """Get cached remote agent instance."""
39
+ # Extract location and engine ID from the full resource ID.
40
+ parts = remote_agent_engine_id.split("/")
41
+ project_id = parts[1]
42
+ location = parts[3]
43
+ vertexai.init(project=project_id, location=location)
44
+ return reasoning_engines.ReasoningEngine(remote_agent_engine_id)
45
+
46
+
47
+ @st.cache_resource
48
+ def get_remote_url_config(url: str, authenticate_request: bool) -> dict[str, Any]:
49
+ """Get cached remote URL agent configuration."""
50
+ stream_url = urljoin(url, "stream_messages")
51
+ creds, _ = google.auth.default()
52
+ id_token = None
53
+ if authenticate_request:
54
+ auth_req = google.auth.transport.requests.Request()
55
+ try:
56
+ id_token = google.oauth2.id_token.fetch_id_token(auth_req, stream_url)
57
+ except DefaultCredentialsError:
58
+ creds.refresh(auth_req)
59
+ id_token = creds.id_token
60
+ return {
61
+ "url": stream_url,
62
+ "authenticate_request": authenticate_request,
63
+ "creds": creds,
64
+ "id_token": id_token,
65
+ }
66
+
67
+
68
+ class Client:
69
+ """A client for streaming events from a server."""
70
+
71
+ def __init__(
72
+ self,
73
+ agent_callable_path: str | None = None,
74
+ remote_agent_engine_id: str | None = None,
75
+ url: str | None = None,
76
+ authenticate_request: bool = False,
77
+ ) -> None:
78
+ """Initialize the Client with appropriate configuration.
79
+
80
+ Args:
81
+ agent_callable_path: Path to local agent class
82
+ remote_agent_engine_id: ID of remote reasoning engine
83
+ url: URL for remote service
84
+ authenticate_request: Whether to authenticate requests to remote URL
85
+ """
86
+ if url:
87
+ remote_config = get_remote_url_config(url, authenticate_request)
88
+ self.url = remote_config["url"]
89
+ self.authenticate_request = remote_config["authenticate_request"]
90
+ self.creds = remote_config["creds"]
91
+ self.id_token = remote_config["id_token"]
92
+ self.agent = None
93
+ elif remote_agent_engine_id:
94
+ self.agent = get_remote_agent(remote_agent_engine_id)
95
+ self.url = None
96
+ else:
97
+ # Force reload all submodules to get latest changes
98
+ self.url = None
99
+ if agent_callable_path is None:
100
+ raise ValueError("agent_callable_path cannot be None")
101
+ module_path, class_name = agent_callable_path.rsplit(".", 1)
102
+ module = importlib.import_module(module_path)
103
+ self.agent = getattr(module, class_name)()
104
+ self.agent.set_up()
105
+
106
+ def log_feedback(self, feedback_dict: dict[str, Any], run_id: str) -> None:
107
+ """Log user feedback for a specific run."""
108
+ score = feedback_dict["score"]
109
+ if score == "😞":
110
+ score = 0.0
111
+ elif score == "🙁":
112
+ score = 0.25
113
+ elif score == "😐":
114
+ score = 0.5
115
+ elif score == "🙂":
116
+ score = 0.75
117
+ elif score == "😀":
118
+ score = 1.0
119
+ feedback_dict["score"] = score
120
+ feedback_dict["run_id"] = run_id
121
+ feedback_dict["log_type"] = "feedback"
122
+ feedback_dict.pop("type")
123
+ url = urljoin(self.url, "feedback")
124
+ headers = {
125
+ "Content-Type": "application/json",
126
+ }
127
+ if self.authenticate_request:
128
+ headers["Authorization"] = f"Bearer {self.id_token}"
129
+ requests.post(url, data=json.dumps(feedback_dict), headers=headers, timeout=10)
130
+
131
+ def stream_messages(
132
+ self, data: dict[str, Any]
133
+ ) -> Generator[dict[str, Any], None, None]:
134
+ """Stream events from the server, yielding parsed event data."""
135
+ if self.url:
136
+ headers = {
137
+ "Content-Type": "application/json",
138
+ "Accept": "text/event-stream",
139
+ }
140
+ if self.authenticate_request:
141
+ headers["Authorization"] = f"Bearer {self.id_token}"
142
+ with requests.post(
143
+ self.url, json={"input": data}, headers=headers, stream=True, timeout=10
144
+ ) as response:
145
+ for line in response.iter_lines():
146
+ if line:
147
+ try:
148
+ event = json.loads(line.decode("utf-8"))
149
+ yield event
150
+ except json.JSONDecodeError:
151
+ print(f"Failed to parse event: {line.decode('utf-8')}")
152
+ elif self.agent is not None:
153
+ yield from self.agent.stream_query(input=data)
154
+
155
+
156
+ class StreamHandler:
157
+ """Handles streaming updates to a Streamlit interface."""
158
+
159
+ def __init__(self, st: Any, initial_text: str = "") -> None:
160
+ """Initialize the StreamHandler with Streamlit context and initial text."""
161
+ self.st = st
162
+ self.tool_expander = st.expander("Tool Calls:", expanded=False)
163
+ self.container = st.empty()
164
+ self.text = initial_text
165
+ self.tools_logs = initial_text
166
+
167
+ def new_token(self, token: str) -> None:
168
+ """Add a new token to the main text display."""
169
+ self.text += token
170
+ self.container.markdown(format_content(self.text), unsafe_allow_html=True)
171
+
172
+ def new_status(self, status_update: str) -> None:
173
+ """Add a new status update to the tool calls expander."""
174
+ self.tools_logs += status_update
175
+ self.tool_expander.markdown(status_update)
176
+
177
+
178
+ class EventProcessor:
179
+ """Processes events from the stream and updates the UI accordingly."""
180
+
181
+ def __init__(self, st: Any, client: Client, stream_handler: StreamHandler) -> None:
182
+ """Initialize the EventProcessor with Streamlit context, client, and stream handler."""
183
+ self.st = st
184
+ self.client = client
185
+ self.stream_handler = stream_handler
186
+ self.final_content = ""
187
+ self.tool_calls: list[dict[str, Any]] = []
188
+ self.current_run_id: str | None = None
189
+ self.additional_kwargs: dict[str, Any] = {}
190
+
191
+ def process_events(self, run_id: str | None = None) -> None:
192
+ """Process events from the stream, handling each event type appropriately."""
193
+ messages = self.st.session_state.user_chats[
194
+ self.st.session_state["session_id"]
195
+ ]["messages"]
196
+ self.current_run_id = run_id or str(uuid.uuid4())
197
+ # Set run_id in session state at start of processing
198
+ self.st.session_state["run_id"] = self.current_run_id
199
+ stream = self.client.stream_messages(
200
+ data={
201
+ "messages": messages,
202
+ "config": {
203
+ "run_id": self.current_run_id,
204
+ "metadata": {
205
+ "user_id": self.st.session_state["user_id"],
206
+ "session_id": self.st.session_state["session_id"],
207
+ },
208
+ },
209
+ }
210
+ )
211
+ # Each event is a tuple message, metadata. https://langchain-ai.github.io/langgraph/how-tos/streaming/#messages
212
+ for message, _ in stream:
213
+ if isinstance(message, dict):
214
+ if message.get("type") == "constructor":
215
+ message = message["kwargs"]
216
+
217
+ # Handle tool calls
218
+ if message.get("tool_calls"):
219
+ tool_calls = message["tool_calls"]
220
+ ai_message = AIMessage(content="", tool_calls=tool_calls)
221
+ self.tool_calls.append(ai_message.model_dump())
222
+ for tool_call in tool_calls:
223
+ msg = f"\n\nCalling tool: `{tool_call['name']}` with args: `{tool_call['args']}`"
224
+ self.stream_handler.new_status(msg)
225
+
226
+ # Handle tool responses
227
+ elif message.get("tool_call_id"):
228
+ content = message["content"]
229
+ tool_call_id = message["tool_call_id"]
230
+ tool_message = ToolMessage(
231
+ content=content, type="tool", tool_call_id=tool_call_id
232
+ ).model_dump()
233
+ self.tool_calls.append(tool_message)
234
+ msg = f"\n\nTool response: `{content}`"
235
+ self.stream_handler.new_status(msg)
236
+
237
+ # Handle AI responses
238
+ elif content := message.get("content"):
239
+ self.final_content += content
240
+ self.stream_handler.new_token(content)
241
+
242
+ # Handle end of stream
243
+ if self.final_content:
244
+ final_message = AIMessage(
245
+ content=self.final_content,
246
+ id=self.current_run_id,
247
+ additional_kwargs=self.additional_kwargs,
248
+ ).model_dump()
249
+ session = self.st.session_state["session_id"]
250
+ self.st.session_state.user_chats[session]["messages"] = (
251
+ self.st.session_state.user_chats[session]["messages"] + self.tool_calls
252
+ )
253
+ self.st.session_state.user_chats[session]["messages"].append(final_message)
254
+ self.st.session_state.run_id = self.current_run_id
255
+
256
+
257
+ def get_chain_response(st: Any, client: Client, stream_handler: StreamHandler) -> None:
258
+ """Process the chain response update the Streamlit UI.
259
+
260
+ This function initiates the event processing for a chain of operations,
261
+ involving an AI model's response generation and potential tool calls.
262
+ It creates an EventProcessor instance and starts the event processing loop.
263
+
264
+ Args:
265
+ st (Any): The Streamlit app instance, used for accessing session state
266
+ and updating the UI.
267
+ client (Client): An instance of the Client class used to stream events
268
+ from the server.
269
+ stream_handler (StreamHandler): An instance of the StreamHandler class
270
+ used to update the Streamlit UI with
271
+ streaming content.
272
+
273
+ Returns:
274
+ None
275
+
276
+ Side effects:
277
+ - Updates the Streamlit UI with streaming tokens and tool call information.
278
+ - Modifies the session state to include the final AI message and run ID.
279
+ - Handles various events like chain starts/ends, tool calls, and model outputs.
280
+ """
281
+ processor = EventProcessor(st, client, stream_handler)
282
+ processor.process_events()