agent-starter-pack 0.2.2__py3-none-any.whl → 0.3.0__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.2.dist-info → agent_starter_pack-0.3.0.dist-info}/METADATA +14 -16
- {agent_starter_pack-0.2.2.dist-info → agent_starter_pack-0.3.0.dist-info}/RECORD +69 -54
- 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 +15 -4
- src/base_template/README.md +8 -2
- src/base_template/app/__init__.py +3 -0
- src/base_template/app/utils/tracing.py +11 -1
- src/base_template/app/utils/typing.py +54 -4
- src/base_template/deployment/README.md +4 -1
- src/base_template/deployment/cd/deploy-to-prod.yaml +3 -3
- src/base_template/deployment/cd/staging.yaml +4 -4
- src/base_template/deployment/ci/pr_checks.yaml +1 -1
- src/base_template/deployment/terraform/build_triggers.tf +3 -0
- 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 +45 -11
- src/cli/commands/setup_cicd.py +25 -6
- src/cli/utils/gcp.py +1 -1
- src/cli/utils/template.py +27 -25
- src/data_ingestion/README.md +37 -50
- src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +2 -1
- src/deployment_targets/agent_engine/app/agent_engine_app.py +68 -22
- 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/Dockerfile +1 -1
- 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/frontend/utils/stream_handler.py +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 +939 -732
- src/resources/locks/uv-agentic_rag-cloud_run.lock +1087 -907
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +778 -671
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +852 -753
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +665 -591
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +842 -743
- src/resources/locks/uv-live_api-cloud_run.lock +830 -731
- 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.2.dist-info → agent_starter_pack-0.3.0.dist-info}/WHEEL +0 -0
- {agent_starter_pack-0.2.2.dist-info → agent_starter_pack-0.3.0.dist-info}/entry_points.txt +0 -0
- {agent_starter_pack-0.2.2.dist-info → agent_starter_pack-0.3.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -12,6 +12,53 @@
|
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
14
|
|
|
15
|
+
# mypy: disable-error-code="attr-defined"
|
|
16
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
17
|
+
import datetime
|
|
18
|
+
import json
|
|
19
|
+
import logging
|
|
20
|
+
from collections.abc import Mapping, Sequence
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import google.auth
|
|
24
|
+
import vertexai
|
|
25
|
+
from google.cloud import logging as google_cloud_logging
|
|
26
|
+
from opentelemetry import trace
|
|
27
|
+
from opentelemetry.sdk.trace import TracerProvider, export
|
|
28
|
+
from vertexai import agent_engines
|
|
29
|
+
from vertexai.preview.reasoning_engines import AdkApp
|
|
30
|
+
|
|
31
|
+
from app.agent import root_agent
|
|
32
|
+
from app.utils.gcs import create_bucket_if_not_exists
|
|
33
|
+
from app.utils.tracing import CloudTraceLoggingSpanExporter
|
|
34
|
+
from app.utils.typing import Feedback
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class AgentEngineApp(AdkApp):
|
|
38
|
+
def set_up(self) -> None:
|
|
39
|
+
"""Set up logging and tracing for the agent engine app."""
|
|
40
|
+
super().set_up()
|
|
41
|
+
logging_client = google_cloud_logging.Client()
|
|
42
|
+
self.logger = logging_client.logger(__name__)
|
|
43
|
+
provider = TracerProvider()
|
|
44
|
+
processor = export.BatchSpanProcessor(CloudTraceLoggingSpanExporter())
|
|
45
|
+
provider.add_span_processor(processor)
|
|
46
|
+
trace.set_tracer_provider(provider)
|
|
47
|
+
|
|
48
|
+
def register_feedback(self, feedback: dict[str, Any]) -> None:
|
|
49
|
+
"""Collect and log feedback."""
|
|
50
|
+
feedback_obj = Feedback.model_validate(feedback)
|
|
51
|
+
self.logger.log_struct(feedback_obj.model_dump(), severity="INFO")
|
|
52
|
+
|
|
53
|
+
def register_operations(self) -> Mapping[str, Sequence]:
|
|
54
|
+
"""Registers the operations of the Agent.
|
|
55
|
+
|
|
56
|
+
Extends the base operations to include feedback registration functionality.
|
|
57
|
+
"""
|
|
58
|
+
operations = super().register_operations()
|
|
59
|
+
operations[""] = operations[""] + ["register_feedback"]
|
|
60
|
+
return operations
|
|
61
|
+
{%- else %}
|
|
15
62
|
import datetime
|
|
16
63
|
import json
|
|
17
64
|
import logging
|
|
@@ -26,16 +73,12 @@ import vertexai
|
|
|
26
73
|
from google.cloud import logging as google_cloud_logging
|
|
27
74
|
from langchain_core.runnables import RunnableConfig
|
|
28
75
|
from traceloop.sdk import Instruments, Traceloop
|
|
29
|
-
from vertexai
|
|
76
|
+
from vertexai import agent_engines
|
|
30
77
|
|
|
31
78
|
from app.utils.gcs import create_bucket_if_not_exists
|
|
32
79
|
from app.utils.tracing import CloudTraceLoggingSpanExporter
|
|
33
80
|
from app.utils.typing import Feedback, InputChat, dumpd, ensure_valid_config
|
|
34
81
|
|
|
35
|
-
logging.basicConfig(
|
|
36
|
-
level=logging.INFO,
|
|
37
|
-
)
|
|
38
|
-
|
|
39
82
|
|
|
40
83
|
class AgentEngineApp:
|
|
41
84
|
"""Class for managing agent engine functionality."""
|
|
@@ -70,7 +113,6 @@ class AgentEngineApp:
|
|
|
70
113
|
)
|
|
71
114
|
except Exception as e:
|
|
72
115
|
logging.error("Failed to initialize Telemetry: %s", str(e))
|
|
73
|
-
|
|
74
116
|
self.runnable = agent
|
|
75
117
|
|
|
76
118
|
# Add any additional variables here that should be included in the tracing logs
|
|
@@ -111,11 +153,6 @@ class AgentEngineApp:
|
|
|
111
153
|
dumped_chunk = dumpd(chunk)
|
|
112
154
|
yield dumped_chunk
|
|
113
155
|
|
|
114
|
-
def register_feedback(self, feedback: dict[str, Any]) -> None:
|
|
115
|
-
"""Collect and log feedback."""
|
|
116
|
-
feedback_obj = Feedback.model_validate(feedback)
|
|
117
|
-
self.logger.log_struct(feedback_obj.model_dump(), severity="INFO")
|
|
118
|
-
|
|
119
156
|
def query(
|
|
120
157
|
self,
|
|
121
158
|
*,
|
|
@@ -124,8 +161,15 @@ class AgentEngineApp:
|
|
|
124
161
|
**kwargs: Any,
|
|
125
162
|
) -> Any:
|
|
126
163
|
"""Process a single input and return the agent's response."""
|
|
164
|
+
config = ensure_valid_config(config)
|
|
165
|
+
self.set_tracing_properties(config=config)
|
|
127
166
|
return dumpd(self.runnable.invoke(input=input, config=config, **kwargs))
|
|
128
167
|
|
|
168
|
+
def register_feedback(self, feedback: dict[str, Any]) -> None:
|
|
169
|
+
"""Collect and log feedback."""
|
|
170
|
+
feedback_obj = Feedback.model_validate(feedback)
|
|
171
|
+
self.logger.log_struct(feedback_obj.model_dump(), severity="INFO")
|
|
172
|
+
|
|
129
173
|
def register_operations(self) -> Mapping[str, Sequence]:
|
|
130
174
|
"""Registers the operations of the Agent.
|
|
131
175
|
|
|
@@ -142,6 +186,7 @@ class AgentEngineApp:
|
|
|
142
186
|
"": ["query", "register_feedback"],
|
|
143
187
|
"stream": ["stream_query"],
|
|
144
188
|
}
|
|
189
|
+
{%- endif %}
|
|
145
190
|
|
|
146
191
|
|
|
147
192
|
def deploy_agent_engine_app(
|
|
@@ -151,7 +196,7 @@ def deploy_agent_engine_app(
|
|
|
151
196
|
requirements_file: str = ".requirements.txt",
|
|
152
197
|
extra_packages: list[str] = ["./app"],
|
|
153
198
|
env_vars: dict[str, str] | None = None,
|
|
154
|
-
) ->
|
|
199
|
+
) -> agent_engines.AgentEngine:
|
|
155
200
|
"""Deploy the agent engine app to Vertex AI."""
|
|
156
201
|
|
|
157
202
|
staging_bucket = f"gs://{project}-agent-engine"
|
|
@@ -164,24 +209,25 @@ def deploy_agent_engine_app(
|
|
|
164
209
|
# Read requirements
|
|
165
210
|
with open(requirements_file) as f:
|
|
166
211
|
requirements = f.read().strip().split("\n")
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
212
|
+
{% if "adk" in cookiecutter.tags %}
|
|
213
|
+
agent_engine = AgentEngineApp(
|
|
214
|
+
agent=root_agent, env_vars=env_vars, enable_tracing=True
|
|
215
|
+
)
|
|
216
|
+
{% else %}
|
|
217
|
+
agent_engine = AgentEngineApp(project_id=project, env_vars=env_vars)
|
|
218
|
+
{% endif %}
|
|
170
219
|
# Common configuration for both create and update operations
|
|
171
220
|
agent_config = {
|
|
172
|
-
"
|
|
221
|
+
"agent_engine": agent_engine,
|
|
173
222
|
"display_name": agent_name,
|
|
174
|
-
"description": "
|
|
223
|
+
"description": "{{cookiecutter.agent_description}}",
|
|
175
224
|
"extra_packages": extra_packages,
|
|
176
225
|
}
|
|
177
226
|
logging.info(f"Agent config: {agent_config}")
|
|
178
227
|
agent_config["requirements"] = requirements
|
|
179
228
|
|
|
180
229
|
# Check if an agent with this name already exists
|
|
181
|
-
existing_agents =
|
|
182
|
-
filter=f"display_name={agent_name}"
|
|
183
|
-
)
|
|
184
|
-
|
|
230
|
+
existing_agents = list(agent_engines.list(filter=f"display_name={agent_name}"))
|
|
185
231
|
if existing_agents:
|
|
186
232
|
# Update the existing agent with new configuration
|
|
187
233
|
logging.info(f"Updating existing agent: {agent_name}")
|
|
@@ -189,7 +235,7 @@ def deploy_agent_engine_app(
|
|
|
189
235
|
else:
|
|
190
236
|
# Create a new agent if none exists
|
|
191
237
|
logging.info(f"Creating new agent: {agent_name}")
|
|
192
|
-
remote_agent =
|
|
238
|
+
remote_agent = agent_engines.create(**agent_config)
|
|
193
239
|
|
|
194
240
|
config = {
|
|
195
241
|
"remote_agent_engine_id": remote_agent.resource_name,
|
|
@@ -15,18 +15,80 @@
|
|
|
15
15
|
import logging
|
|
16
16
|
|
|
17
17
|
import pytest
|
|
18
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
19
|
+
from google.adk.events.event import Event
|
|
18
20
|
|
|
21
|
+
from app.agent import root_agent
|
|
19
22
|
from app.agent_engine_app import AgentEngineApp
|
|
23
|
+
{%- else %}
|
|
24
|
+
|
|
25
|
+
from app.agent_engine_app import AgentEngineApp
|
|
26
|
+
{%- endif %}
|
|
20
27
|
|
|
21
28
|
|
|
22
29
|
@pytest.fixture
|
|
23
30
|
def agent_app() -> AgentEngineApp:
|
|
24
31
|
"""Fixture to create and set up AgentEngineApp instance"""
|
|
32
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
33
|
+
app = AgentEngineApp(agent=root_agent)
|
|
34
|
+
{%- else %}
|
|
25
35
|
app = AgentEngineApp()
|
|
36
|
+
{%- endif %}
|
|
26
37
|
app.set_up()
|
|
27
38
|
return app
|
|
28
39
|
|
|
40
|
+
{% if "adk" in cookiecutter.tags %}
|
|
41
|
+
def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Integration test for the agent stream query functionality.
|
|
44
|
+
Tests that the agent returns valid streaming responses.
|
|
45
|
+
"""
|
|
46
|
+
# Create message and events for the stream_query
|
|
47
|
+
message = "What's the weather in San Francisco?"
|
|
48
|
+
events = list(agent_app.stream_query(message=message, user_id="test"))
|
|
49
|
+
assert len(events) > 0, "Expected at least one chunk in response"
|
|
50
|
+
|
|
51
|
+
# Check for valid content in the response
|
|
52
|
+
has_text_content = False
|
|
53
|
+
for event in events:
|
|
54
|
+
validated_event = Event.model_validate(event)
|
|
55
|
+
content = validated_event.content
|
|
56
|
+
if (
|
|
57
|
+
content is not None
|
|
58
|
+
and content.parts
|
|
59
|
+
and any(part.text for part in content.parts)
|
|
60
|
+
):
|
|
61
|
+
has_text_content = True
|
|
62
|
+
break
|
|
63
|
+
|
|
64
|
+
assert has_text_content, "Expected at least one event with text content"
|
|
29
65
|
|
|
66
|
+
|
|
67
|
+
def test_agent_feedback(agent_app: AgentEngineApp) -> None:
|
|
68
|
+
"""
|
|
69
|
+
Integration test for the agent feedback functionality.
|
|
70
|
+
Tests that feedback can be registered successfully.
|
|
71
|
+
"""
|
|
72
|
+
feedback_data = {
|
|
73
|
+
"score": 5,
|
|
74
|
+
"text": "Great response!",
|
|
75
|
+
"invocation_id": "test-run-123",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# Should not raise any exceptions
|
|
79
|
+
agent_app.register_feedback(feedback_data)
|
|
80
|
+
|
|
81
|
+
# Test invalid feedback
|
|
82
|
+
with pytest.raises(ValueError):
|
|
83
|
+
invalid_feedback = {
|
|
84
|
+
"score": "invalid", # Score must be numeric
|
|
85
|
+
"text": "Bad feedback",
|
|
86
|
+
"invocation_id": "test-run-123",
|
|
87
|
+
}
|
|
88
|
+
agent_app.register_feedback(invalid_feedback)
|
|
89
|
+
|
|
90
|
+
logging.info("All assertions passed for agent feedback test")
|
|
91
|
+
{% else %}
|
|
30
92
|
def test_agent_stream_query(agent_app: AgentEngineApp) -> None:
|
|
31
93
|
"""
|
|
32
94
|
Integration test for the agent stream query functionality.
|
|
@@ -118,3 +180,4 @@ def test_agent_feedback(agent_app: AgentEngineApp) -> None:
|
|
|
118
180
|
agent_app.register_feedback(invalid_feedback)
|
|
119
181
|
|
|
120
182
|
logging.info("All assertions passed for agent feedback test")
|
|
183
|
+
{% endif %}
|
|
@@ -54,7 +54,14 @@ class ChatStreamUser(HttpUser):
|
|
|
54
54
|
"""Simulates a chat stream interaction."""
|
|
55
55
|
headers = {"Content-Type": "application/json"}
|
|
56
56
|
headers["Authorization"] = f"Bearer {os.environ['_AUTH_TOKEN']}"
|
|
57
|
-
|
|
57
|
+
{% if "adk" in cookiecutter.tags %}
|
|
58
|
+
data = {
|
|
59
|
+
"input": {
|
|
60
|
+
"message": "What's the weather in San Francisco?",
|
|
61
|
+
"user_id": "test",
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
{% else %}
|
|
58
65
|
data = {
|
|
59
66
|
"input": {
|
|
60
67
|
"input": {
|
|
@@ -69,7 +76,7 @@ class ChatStreamUser(HttpUser):
|
|
|
69
76
|
},
|
|
70
77
|
}
|
|
71
78
|
}
|
|
72
|
-
|
|
79
|
+
{% endif %}
|
|
73
80
|
start_time = time.time()
|
|
74
81
|
with self.client.post(
|
|
75
82
|
url_path,
|
|
@@ -11,7 +11,32 @@
|
|
|
11
11
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
12
|
# See the License for the specific language governing permissions and
|
|
13
13
|
# limitations under the License.
|
|
14
|
+
{% if "adk" in cookiecutter.tags %}
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
from fastapi import FastAPI
|
|
18
|
+
from google.adk.cli.fast_api import get_fast_api_app
|
|
19
|
+
from google.cloud import logging as google_cloud_logging
|
|
20
|
+
from opentelemetry import trace
|
|
21
|
+
from opentelemetry.sdk.trace import TracerProvider, export
|
|
22
|
+
|
|
23
|
+
from app.utils.tracing import CloudTraceLoggingSpanExporter
|
|
24
|
+
from app.utils.typing import Feedback
|
|
25
|
+
|
|
26
|
+
logging_client = google_cloud_logging.Client()
|
|
27
|
+
logger = logging_client.logger(__name__)
|
|
28
|
+
|
|
29
|
+
provider = TracerProvider()
|
|
30
|
+
processor = export.BatchSpanProcessor(CloudTraceLoggingSpanExporter())
|
|
31
|
+
provider.add_span_processor(processor)
|
|
32
|
+
trace.set_tracer_provider(provider)
|
|
14
33
|
|
|
34
|
+
AGENT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
35
|
+
app: FastAPI = get_fast_api_app(agent_dir=AGENT_DIR, web=False)
|
|
36
|
+
|
|
37
|
+
app.title = "{{cookiecutter.project_name}}"
|
|
38
|
+
app.description = "API for interacting with the Agent {{cookiecutter.project_name}}"
|
|
39
|
+
{%- else %}
|
|
15
40
|
import logging
|
|
16
41
|
import os
|
|
17
42
|
from collections.abc import Generator
|
|
@@ -40,7 +65,7 @@ try:
|
|
|
40
65
|
app_name=app.title,
|
|
41
66
|
disable_batch=False,
|
|
42
67
|
exporter=CloudTraceLoggingSpanExporter(),
|
|
43
|
-
instruments={
|
|
68
|
+
instruments={Instruments.LANGCHAIN, Instruments.CREW},
|
|
44
69
|
)
|
|
45
70
|
except Exception as e:
|
|
46
71
|
logging.error("Failed to initialize Telemetry: %s", str(e))
|
|
@@ -91,20 +116,6 @@ def redirect_root_to_docs() -> RedirectResponse:
|
|
|
91
116
|
return RedirectResponse(url="/docs")
|
|
92
117
|
|
|
93
118
|
|
|
94
|
-
@app.post("/feedback")
|
|
95
|
-
def collect_feedback(feedback: Feedback) -> dict[str, str]:
|
|
96
|
-
"""Collect and log feedback.
|
|
97
|
-
|
|
98
|
-
Args:
|
|
99
|
-
feedback: The feedback data to log
|
|
100
|
-
|
|
101
|
-
Returns:
|
|
102
|
-
Success message
|
|
103
|
-
"""
|
|
104
|
-
logger.log_struct(feedback.model_dump(), severity="INFO")
|
|
105
|
-
return {"status": "success"}
|
|
106
|
-
|
|
107
|
-
|
|
108
119
|
@app.post("/stream_messages")
|
|
109
120
|
def stream_chat_events(request: Request) -> StreamingResponse:
|
|
110
121
|
"""Stream chat events in response to an input request.
|
|
@@ -119,6 +130,21 @@ def stream_chat_events(request: Request) -> StreamingResponse:
|
|
|
119
130
|
stream_messages(input=request.input, config=request.config),
|
|
120
131
|
media_type="text/event-stream",
|
|
121
132
|
)
|
|
133
|
+
{%- endif %}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@app.post("/feedback")
|
|
137
|
+
def collect_feedback(feedback: Feedback) -> dict[str, str]:
|
|
138
|
+
"""Collect and log feedback.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
feedback: The feedback data to log
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Success message
|
|
145
|
+
"""
|
|
146
|
+
logger.log_struct(feedback.model_dump(), severity="INFO")
|
|
147
|
+
return {"status": "success"}
|
|
122
148
|
|
|
123
149
|
|
|
124
150
|
# Main execution
|
|
@@ -32,7 +32,11 @@ logging.basicConfig(level=logging.INFO)
|
|
|
32
32
|
logger = logging.getLogger(__name__)
|
|
33
33
|
|
|
34
34
|
BASE_URL = "http://127.0.0.1:8000/"
|
|
35
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
36
|
+
STREAM_URL = BASE_URL + "run_sse"
|
|
37
|
+
{%- else %}
|
|
35
38
|
STREAM_URL = BASE_URL + "stream_messages"
|
|
39
|
+
{%- endif %}
|
|
36
40
|
FEEDBACK_URL = BASE_URL + "feedback"
|
|
37
41
|
|
|
38
42
|
HEADERS = {"Content-Type": "application/json"}
|
|
@@ -116,23 +120,72 @@ def server_fixture(request: Any) -> Iterator[subprocess.Popen[str]]:
|
|
|
116
120
|
def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
|
|
117
121
|
"""Test the chat stream functionality."""
|
|
118
122
|
logger.info("Starting chat stream test")
|
|
123
|
+
{% if "adk" in cookiecutter.tags %}
|
|
124
|
+
# Create session first
|
|
125
|
+
user_id = "user_123"
|
|
126
|
+
session_id = "session_abc"
|
|
127
|
+
session_data = {"state": {"preferred_language": "English", "visit_count": 5}}
|
|
128
|
+
session_response = requests.post(
|
|
129
|
+
f"{BASE_URL}/apps/app/users/{user_id}/sessions/{session_id}",
|
|
130
|
+
headers=HEADERS,
|
|
131
|
+
json=session_data,
|
|
132
|
+
timeout=10,
|
|
133
|
+
)
|
|
134
|
+
assert session_response.status_code == 200
|
|
119
135
|
|
|
136
|
+
# Then send chat message
|
|
137
|
+
data = {
|
|
138
|
+
"app_name": "app",
|
|
139
|
+
"user_id": user_id,
|
|
140
|
+
"session_id": session_id,
|
|
141
|
+
"new_message": {
|
|
142
|
+
"role": "user",
|
|
143
|
+
"parts": [{"text": "What's the weather in San Francisco?"}],
|
|
144
|
+
},
|
|
145
|
+
"streaming": True,
|
|
146
|
+
}
|
|
147
|
+
{% else %}
|
|
120
148
|
data = {
|
|
121
149
|
"input": {
|
|
122
150
|
"messages": [
|
|
123
151
|
{"type": "human", "content": "Hello, AI!"},
|
|
124
152
|
{"type": "ai", "content": "Hello!"},
|
|
125
|
-
{"type": "human", "content": "
|
|
153
|
+
{"type": "human", "content": "Who are you?"},
|
|
126
154
|
]
|
|
127
155
|
},
|
|
128
156
|
"config": {"metadata": {"user_id": "test-user", "session_id": "test-session"}},
|
|
129
157
|
}
|
|
130
|
-
|
|
158
|
+
{% endif %}
|
|
131
159
|
response = requests.post(
|
|
132
160
|
STREAM_URL, headers=HEADERS, json=data, stream=True, timeout=10
|
|
133
161
|
)
|
|
134
162
|
assert response.status_code == 200
|
|
135
163
|
|
|
164
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
165
|
+
# Parse SSE events from response
|
|
166
|
+
events = []
|
|
167
|
+
for line in response.iter_lines():
|
|
168
|
+
if line:
|
|
169
|
+
# SSE format is "data: {json}"
|
|
170
|
+
line_str = line.decode("utf-8")
|
|
171
|
+
if line_str.startswith("data: "):
|
|
172
|
+
event_json = line_str[6:] # Remove "data: " prefix
|
|
173
|
+
event = json.loads(event_json)
|
|
174
|
+
events.append(event)
|
|
175
|
+
|
|
176
|
+
assert events, "No events received from stream"
|
|
177
|
+
# Check for valid content in the response
|
|
178
|
+
has_text_content = False
|
|
179
|
+
for event in events:
|
|
180
|
+
content = event.get("content")
|
|
181
|
+
if (
|
|
182
|
+
content is not None
|
|
183
|
+
and content.get("parts")
|
|
184
|
+
and any(part.get("text") for part in content["parts"])
|
|
185
|
+
):
|
|
186
|
+
has_text_content = True
|
|
187
|
+
break
|
|
188
|
+
{%- else %}
|
|
136
189
|
events = [json.loads(line) for line in response.iter_lines() if line]
|
|
137
190
|
assert events, "No events received from stream"
|
|
138
191
|
|
|
@@ -155,12 +208,12 @@ def test_chat_stream(server_fixture: subprocess.Popen[str]) -> None:
|
|
|
155
208
|
has_content = True
|
|
156
209
|
break
|
|
157
210
|
assert has_content, "At least one message should have content"
|
|
211
|
+
{%- endif %}
|
|
158
212
|
|
|
159
213
|
|
|
160
214
|
def test_chat_stream_error_handling(server_fixture: subprocess.Popen[str]) -> None:
|
|
161
215
|
"""Test the chat stream error handling."""
|
|
162
216
|
logger.info("Starting chat stream error handling test")
|
|
163
|
-
|
|
164
217
|
data = {
|
|
165
218
|
"input": {"messages": [{"type": "invalid_type", "content": "Cause an error"}]}
|
|
166
219
|
}
|
|
@@ -182,7 +235,11 @@ def test_collect_feedback(server_fixture: subprocess.Popen[str]) -> None:
|
|
|
182
235
|
# Create sample feedback data
|
|
183
236
|
feedback_data = {
|
|
184
237
|
"score": 4,
|
|
238
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
239
|
+
"invocation_id": str(uuid.uuid4()),
|
|
240
|
+
{%- else %}
|
|
185
241
|
"run_id": str(uuid.uuid4()),
|
|
242
|
+
{%- endif %}
|
|
186
243
|
"text": "Great response!",
|
|
187
244
|
}
|
|
188
245
|
|
|
@@ -28,7 +28,7 @@ Trigger the Locust load test with the following command:
|
|
|
28
28
|
locust -f tests/load_test/load_test.py \
|
|
29
29
|
-H http://127.0.0.1:8000 \
|
|
30
30
|
--headless \
|
|
31
|
-
-t 30s -u
|
|
31
|
+
-t 30s -u 10 -r 2 \
|
|
32
32
|
--csv=tests/load_test/.results/results \
|
|
33
33
|
--html=tests/load_test/.results/report.html
|
|
34
34
|
```
|
|
@@ -15,9 +15,20 @@
|
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
17
|
import time
|
|
18
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
19
|
+
import uuid
|
|
18
20
|
|
|
21
|
+
import requests
|
|
19
22
|
from locust import HttpUser, between, task
|
|
23
|
+
{%- else %}
|
|
20
24
|
|
|
25
|
+
from locust import HttpUser, between, task
|
|
26
|
+
{%- endif %}
|
|
27
|
+
{% if "adk" in cookiecutter.tags %}
|
|
28
|
+
ENDPOINT = "/run_sse"
|
|
29
|
+
{% else %}
|
|
30
|
+
ENDPOINT = "/stream_messages"
|
|
31
|
+
{% endif %}
|
|
21
32
|
|
|
22
33
|
class ChatStreamUser(HttpUser):
|
|
23
34
|
"""Simulates a user interacting with the chat stream API."""
|
|
@@ -30,7 +41,30 @@ class ChatStreamUser(HttpUser):
|
|
|
30
41
|
headers = {"Content-Type": "application/json"}
|
|
31
42
|
if os.environ.get("_ID_TOKEN"):
|
|
32
43
|
headers["Authorization"] = f"Bearer {os.environ['_ID_TOKEN']}"
|
|
44
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
45
|
+
# Create session first
|
|
46
|
+
user_id = f"user_{uuid.uuid4()}"
|
|
47
|
+
session_id = f"session_{uuid.uuid4()}"
|
|
48
|
+
session_data = {"state": {"preferred_language": "English", "visit_count": 5}}
|
|
49
|
+
requests.post(
|
|
50
|
+
f"{self.client.base_url}/apps/app/users/{user_id}/sessions/{session_id}",
|
|
51
|
+
headers=headers,
|
|
52
|
+
json=session_data,
|
|
53
|
+
timeout=10,
|
|
54
|
+
)
|
|
33
55
|
|
|
56
|
+
# Send chat message
|
|
57
|
+
data = {
|
|
58
|
+
"app_name": "app",
|
|
59
|
+
"user_id": user_id,
|
|
60
|
+
"session_id": session_id,
|
|
61
|
+
"new_message": {
|
|
62
|
+
"role": "user",
|
|
63
|
+
"parts": [{"text": "What's the weather in San Francisco?"}],
|
|
64
|
+
},
|
|
65
|
+
"streaming": True,
|
|
66
|
+
}
|
|
67
|
+
{%- else %}
|
|
34
68
|
data = {
|
|
35
69
|
"input": {
|
|
36
70
|
"messages": [
|
|
@@ -43,43 +77,42 @@ class ChatStreamUser(HttpUser):
|
|
|
43
77
|
"metadata": {"user_id": "test-user", "session_id": "test-session"}
|
|
44
78
|
},
|
|
45
79
|
}
|
|
46
|
-
|
|
80
|
+
{%- endif %}
|
|
47
81
|
start_time = time.time()
|
|
48
82
|
|
|
49
83
|
with self.client.post(
|
|
50
|
-
|
|
84
|
+
ENDPOINT,
|
|
85
|
+
name=f"{ENDPOINT} message",
|
|
51
86
|
headers=headers,
|
|
52
87
|
json=data,
|
|
53
88
|
catch_response=True,
|
|
54
|
-
name="/stream_messages first message",
|
|
55
89
|
stream=True,
|
|
90
|
+
params={"alt": "sse"},
|
|
56
91
|
) as response:
|
|
57
92
|
if response.status_code == 200:
|
|
58
93
|
events = []
|
|
59
94
|
for line in response.iter_lines():
|
|
60
95
|
if line:
|
|
96
|
+
{%- if "adk" in cookiecutter.tags %}
|
|
97
|
+
# SSE format is "data: {json}"
|
|
98
|
+
line_str = line.decode("utf-8")
|
|
99
|
+
if line_str.startswith("data: "):
|
|
100
|
+
event_json = line_str[6:] # Remove "data: " prefix
|
|
101
|
+
event = json.loads(event_json)
|
|
102
|
+
events.append(event)
|
|
103
|
+
{%- else %}
|
|
61
104
|
event = json.loads(line)
|
|
62
105
|
events.append(event)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
request_type="POST",
|
|
75
|
-
name="/stream_messages end",
|
|
76
|
-
response_time=total_time
|
|
77
|
-
* 1000, # Convert to milliseconds
|
|
78
|
-
response_length=len(json.dumps(events)),
|
|
79
|
-
response=response,
|
|
80
|
-
context={},
|
|
81
|
-
)
|
|
82
|
-
return
|
|
83
|
-
response.failure("No valid response content received")
|
|
106
|
+
{%- endif %}
|
|
107
|
+
end_time = time.time()
|
|
108
|
+
total_time = end_time - start_time
|
|
109
|
+
self.environment.events.request.fire(
|
|
110
|
+
request_type="POST",
|
|
111
|
+
name=f"{ENDPOINT} end",
|
|
112
|
+
response_time=total_time * 1000, # Convert to milliseconds
|
|
113
|
+
response_length=len(json.dumps(events)),
|
|
114
|
+
response=response,
|
|
115
|
+
context={},
|
|
116
|
+
)
|
|
84
117
|
else:
|
|
85
118
|
response.failure(f"Unexpected status code: {response.status_code}")
|
|
@@ -2027,9 +2027,9 @@
|
|
|
2027
2027
|
}
|
|
2028
2028
|
},
|
|
2029
2029
|
"node_modules/@babel/runtime": {
|
|
2030
|
-
"version": "7.
|
|
2031
|
-
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.
|
|
2032
|
-
"integrity": "sha512-
|
|
2030
|
+
"version": "7.27.0",
|
|
2031
|
+
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz",
|
|
2032
|
+
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
|
|
2033
2033
|
"license": "MIT",
|
|
2034
2034
|
"dependencies": {
|
|
2035
2035
|
"regenerator-runtime": "^0.14.0"
|
|
@@ -28,7 +28,7 @@ import streamlit as st
|
|
|
28
28
|
import vertexai
|
|
29
29
|
from google.auth.exceptions import DefaultCredentialsError
|
|
30
30
|
from langchain_core.messages import AIMessage, ToolMessage
|
|
31
|
-
from vertexai
|
|
31
|
+
from vertexai import agent_engines
|
|
32
32
|
|
|
33
33
|
from frontend.utils.multimodal_utils import format_content
|
|
34
34
|
|
|
@@ -43,7 +43,7 @@ def get_remote_agent(remote_agent_engine_id: str) -> Any:
|
|
|
43
43
|
project_id = parts[1]
|
|
44
44
|
location = parts[3]
|
|
45
45
|
vertexai.init(project=project_id, location=location)
|
|
46
|
-
return
|
|
46
|
+
return agent_engines.AgentEngine(remote_agent_engine_id)
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
@st.cache_resource
|
|
@@ -159,7 +159,7 @@ class Client:
|
|
|
159
159
|
if self.authenticate_request:
|
|
160
160
|
headers["Authorization"] = f"Bearer {self.id_token}"
|
|
161
161
|
with requests.post(
|
|
162
|
-
self.url, json=data, headers=headers, stream=True, timeout=
|
|
162
|
+
self.url, json=data, headers=headers, stream=True, timeout=60
|
|
163
163
|
) as response:
|
|
164
164
|
for line in response.iter_lines():
|
|
165
165
|
if line:
|