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.
- agent_starter_pack-0.0.1b0.dist-info/METADATA +143 -0
- agent_starter_pack-0.0.1b0.dist-info/RECORD +162 -0
- agent_starter_pack-0.0.1b0.dist-info/WHEEL +4 -0
- agent_starter_pack-0.0.1b0.dist-info/entry_points.txt +2 -0
- agent_starter_pack-0.0.1b0.dist-info/licenses/LICENSE +201 -0
- agents/agentic_rag_vertexai_search/README.md +22 -0
- agents/agentic_rag_vertexai_search/app/agent.py +145 -0
- agents/agentic_rag_vertexai_search/app/retrievers.py +79 -0
- agents/agentic_rag_vertexai_search/app/templates.py +53 -0
- agents/agentic_rag_vertexai_search/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/agentic_rag_vertexai_search/template/.templateconfig.yaml +14 -0
- agents/agentic_rag_vertexai_search/tests/integration/test_agent.py +57 -0
- agents/crewai_coding_crew/README.md +34 -0
- agents/crewai_coding_crew/app/agent.py +86 -0
- agents/crewai_coding_crew/app/crew/config/agents.yaml +39 -0
- agents/crewai_coding_crew/app/crew/config/tasks.yaml +37 -0
- agents/crewai_coding_crew/app/crew/crew.py +71 -0
- agents/crewai_coding_crew/notebooks/evaluating_crewai_agent.ipynb +1571 -0
- agents/crewai_coding_crew/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/crewai_coding_crew/template/.templateconfig.yaml +12 -0
- agents/crewai_coding_crew/tests/integration/test_agent.py +47 -0
- agents/langgraph_base_react/README.md +9 -0
- agents/langgraph_base_react/app/agent.py +73 -0
- agents/langgraph_base_react/notebooks/evaluating_langgraph_agent.ipynb +1561 -0
- agents/langgraph_base_react/template/.templateconfig.yaml +13 -0
- agents/langgraph_base_react/tests/integration/test_agent.py +48 -0
- agents/multimodal_live_api/README.md +50 -0
- agents/multimodal_live_api/app/agent.py +86 -0
- agents/multimodal_live_api/app/server.py +193 -0
- agents/multimodal_live_api/app/templates.py +51 -0
- agents/multimodal_live_api/app/vector_store.py +55 -0
- agents/multimodal_live_api/template/.templateconfig.yaml +15 -0
- agents/multimodal_live_api/tests/integration/test_server_e2e.py +254 -0
- agents/multimodal_live_api/tests/load_test/load_test.py +40 -0
- agents/multimodal_live_api/tests/unit/test_server.py +143 -0
- src/base_template/.gitignore +197 -0
- src/base_template/Makefile +37 -0
- src/base_template/README.md +91 -0
- src/base_template/app/utils/tracing.py +143 -0
- src/base_template/app/utils/typing.py +115 -0
- src/base_template/deployment/README.md +123 -0
- src/base_template/deployment/cd/deploy-to-prod.yaml +98 -0
- src/base_template/deployment/cd/staging.yaml +215 -0
- src/base_template/deployment/ci/pr_checks.yaml +51 -0
- src/base_template/deployment/terraform/apis.tf +34 -0
- src/base_template/deployment/terraform/build_triggers.tf +122 -0
- src/base_template/deployment/terraform/dev/apis.tf +42 -0
- src/base_template/deployment/terraform/dev/iam.tf +90 -0
- src/base_template/deployment/terraform/dev/log_sinks.tf +66 -0
- src/base_template/deployment/terraform/dev/providers.tf +29 -0
- src/base_template/deployment/terraform/dev/storage.tf +76 -0
- src/base_template/deployment/terraform/dev/variables.tf +126 -0
- src/base_template/deployment/terraform/dev/vars/env.tfvars +21 -0
- src/base_template/deployment/terraform/iam.tf +130 -0
- src/base_template/deployment/terraform/locals.tf +50 -0
- src/base_template/deployment/terraform/log_sinks.tf +72 -0
- src/base_template/deployment/terraform/providers.tf +35 -0
- src/base_template/deployment/terraform/service_accounts.tf +42 -0
- src/base_template/deployment/terraform/storage.tf +100 -0
- src/base_template/deployment/terraform/variables.tf +202 -0
- src/base_template/deployment/terraform/vars/env.tfvars +43 -0
- src/base_template/pyproject.toml +113 -0
- src/base_template/tests/unit/test_utils/test_tracing_exporter.py +140 -0
- src/cli/commands/create.py +534 -0
- src/cli/commands/setup_cicd.py +730 -0
- src/cli/main.py +35 -0
- src/cli/utils/__init__.py +35 -0
- src/cli/utils/cicd.py +662 -0
- src/cli/utils/gcp.py +120 -0
- src/cli/utils/logging.py +51 -0
- src/cli/utils/template.py +644 -0
- src/data_ingestion/README.md +79 -0
- src/data_ingestion/data_ingestion_pipeline/components/ingest_data.py +175 -0
- src/data_ingestion/data_ingestion_pipeline/components/process_data.py +321 -0
- src/data_ingestion/data_ingestion_pipeline/pipeline.py +58 -0
- src/data_ingestion/data_ingestion_pipeline/submit_pipeline.py +184 -0
- src/data_ingestion/pyproject.toml +17 -0
- src/data_ingestion/uv.lock +999 -0
- src/deployment_targets/agent_engine/app/agent_engine_app.py +238 -0
- src/deployment_targets/agent_engine/app/utils/gcs.py +42 -0
- src/deployment_targets/agent_engine/deployment_metadata.json +4 -0
- src/deployment_targets/agent_engine/notebooks/intro_reasoning_engine.ipynb +869 -0
- src/deployment_targets/agent_engine/tests/integration/test_agent_engine_app.py +120 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/report.html +264 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_exceptions.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_failures.csv +1 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats.csv +3 -0
- src/deployment_targets/agent_engine/tests/load_test/.results/results_stats_history.csv +22 -0
- src/deployment_targets/agent_engine/tests/load_test/README.md +42 -0
- src/deployment_targets/agent_engine/tests/load_test/load_test.py +100 -0
- src/deployment_targets/agent_engine/tests/unit/test_dummy.py +22 -0
- src/deployment_targets/cloud_run/Dockerfile +29 -0
- src/deployment_targets/cloud_run/app/server.py +128 -0
- src/deployment_targets/cloud_run/deployment/terraform/artifact_registry.tf +22 -0
- src/deployment_targets/cloud_run/deployment/terraform/dev/service_accounts.tf +20 -0
- src/deployment_targets/cloud_run/tests/integration/test_server_e2e.py +192 -0
- src/deployment_targets/cloud_run/tests/load_test/.results/.placeholder +0 -0
- src/deployment_targets/cloud_run/tests/load_test/README.md +79 -0
- src/deployment_targets/cloud_run/tests/load_test/load_test.py +85 -0
- src/deployment_targets/cloud_run/tests/unit/test_server.py +142 -0
- src/deployment_targets/cloud_run/uv.lock +6952 -0
- src/frontends/live_api_react/frontend/package-lock.json +19405 -0
- src/frontends/live_api_react/frontend/package.json +56 -0
- src/frontends/live_api_react/frontend/public/favicon.ico +0 -0
- src/frontends/live_api_react/frontend/public/index.html +62 -0
- src/frontends/live_api_react/frontend/public/robots.txt +3 -0
- src/frontends/live_api_react/frontend/src/App.scss +189 -0
- src/frontends/live_api_react/frontend/src/App.test.tsx +25 -0
- src/frontends/live_api_react/frontend/src/App.tsx +205 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/AudioPulse.tsx +64 -0
- src/frontends/live_api_react/frontend/src/components/audio-pulse/audio-pulse.scss +68 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/ControlTray.tsx +217 -0
- src/frontends/live_api_react/frontend/src/components/control-tray/control-tray.scss +201 -0
- src/frontends/live_api_react/frontend/src/components/logger/Logger.tsx +241 -0
- src/frontends/live_api_react/frontend/src/components/logger/logger.scss +133 -0
- src/frontends/live_api_react/frontend/src/components/logger/mock-logs.ts +151 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/SidePanel.tsx +161 -0
- src/frontends/live_api_react/frontend/src/components/side-panel/side-panel.scss +285 -0
- src/frontends/live_api_react/frontend/src/contexts/LiveAPIContext.tsx +48 -0
- src/frontends/live_api_react/frontend/src/hooks/use-live-api.ts +115 -0
- src/frontends/live_api_react/frontend/src/hooks/use-media-stream-mux.ts +23 -0
- src/frontends/live_api_react/frontend/src/hooks/use-screen-capture.ts +72 -0
- src/frontends/live_api_react/frontend/src/hooks/use-webcam.ts +69 -0
- src/frontends/live_api_react/frontend/src/index.css +28 -0
- src/frontends/live_api_react/frontend/src/index.tsx +35 -0
- src/frontends/live_api_react/frontend/src/multimodal-live-types.ts +242 -0
- src/frontends/live_api_react/frontend/src/react-app-env.d.ts +17 -0
- src/frontends/live_api_react/frontend/src/reportWebVitals.ts +31 -0
- src/frontends/live_api_react/frontend/src/setupTests.ts +21 -0
- src/frontends/live_api_react/frontend/src/utils/audio-recorder.ts +111 -0
- src/frontends/live_api_react/frontend/src/utils/audio-streamer.ts +270 -0
- src/frontends/live_api_react/frontend/src/utils/audioworklet-registry.ts +43 -0
- src/frontends/live_api_react/frontend/src/utils/multimodal-live-client.ts +329 -0
- src/frontends/live_api_react/frontend/src/utils/store-logger.ts +64 -0
- src/frontends/live_api_react/frontend/src/utils/utils.ts +86 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/audio-processing.ts +73 -0
- src/frontends/live_api_react/frontend/src/utils/worklets/vol-meter.ts +65 -0
- src/frontends/live_api_react/frontend/tsconfig.json +25 -0
- src/frontends/streamlit/frontend/side_bar.py +213 -0
- src/frontends/streamlit/frontend/streamlit_app.py +263 -0
- src/frontends/streamlit/frontend/style/app_markdown.py +37 -0
- src/frontends/streamlit/frontend/utils/chat_utils.py +67 -0
- src/frontends/streamlit/frontend/utils/local_chat_history.py +125 -0
- src/frontends/streamlit/frontend/utils/message_editing.py +59 -0
- src/frontends/streamlit/frontend/utils/multimodal_utils.py +217 -0
- src/frontends/streamlit/frontend/utils/stream_handler.py +282 -0
- src/frontends/streamlit/frontend/utils/title_summary.py +77 -0
- src/resources/containers/data_processing/Dockerfile +25 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-agent_engine.lock +4684 -0
- src/resources/locks/uv-agentic_rag_vertexai_search-cloud_run.lock +5799 -0
- src/resources/locks/uv-crewai_coding_crew-agent_engine.lock +5509 -0
- src/resources/locks/uv-crewai_coding_crew-cloud_run.lock +6688 -0
- src/resources/locks/uv-langgraph_base_react-agent_engine.lock +4595 -0
- src/resources/locks/uv-langgraph_base_react-cloud_run.lock +5710 -0
- src/resources/locks/uv-multimodal_live_api-cloud_run.lock +5665 -0
- src/resources/setup_cicd/cicd_variables.tf +36 -0
- src/resources/setup_cicd/github.tf +85 -0
- src/resources/setup_cicd/providers.tf +39 -0
- src/utils/generate_locks.py +135 -0
- src/utils/lock_utils.py +82 -0
- src/utils/watch_and_rebuild.py +190 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { Content, GenerativeContentBlob, Part } from "@google/generative-ai";
|
|
18
|
+
import { EventEmitter } from "eventemitter3";
|
|
19
|
+
import { difference } from "lodash";
|
|
20
|
+
import {
|
|
21
|
+
ClientContentMessage,
|
|
22
|
+
isInterrupted,
|
|
23
|
+
isModelTurn,
|
|
24
|
+
isServerContenteMessage,
|
|
25
|
+
isSetupCompleteMessage,
|
|
26
|
+
isToolCallCancellationMessage,
|
|
27
|
+
isToolCallMessage,
|
|
28
|
+
isTurnComplete,
|
|
29
|
+
LiveIncomingMessage,
|
|
30
|
+
ModelTurn,
|
|
31
|
+
RealtimeInputMessage,
|
|
32
|
+
ServerContent,
|
|
33
|
+
StreamingLog,
|
|
34
|
+
ToolCall,
|
|
35
|
+
ToolCallCancellation,
|
|
36
|
+
ToolResponseMessage,
|
|
37
|
+
type LiveConfig,
|
|
38
|
+
} from "../multimodal-live-types";
|
|
39
|
+
import { blobToJSON, base64ToArrayBuffer } from "./utils";
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* the events that this client will emit
|
|
43
|
+
*/
|
|
44
|
+
interface MultimodalLiveClientEventTypes {
|
|
45
|
+
open: () => void;
|
|
46
|
+
log: (log: StreamingLog) => void;
|
|
47
|
+
close: (event: CloseEvent) => void;
|
|
48
|
+
audio: (data: ArrayBuffer) => void;
|
|
49
|
+
content: (data: ServerContent) => void;
|
|
50
|
+
interrupted: () => void;
|
|
51
|
+
setupcomplete: () => void;
|
|
52
|
+
status: (status: string) => void;
|
|
53
|
+
turncomplete: () => void;
|
|
54
|
+
toolcall: (toolCall: ToolCall) => void;
|
|
55
|
+
toolcallcancellation: (toolcallCancellation: ToolCallCancellation) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type MultimodalLiveAPIClientConnection = {
|
|
59
|
+
url?: string;
|
|
60
|
+
runId?: string;
|
|
61
|
+
userId?: string;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* A event-emitting class that manages the connection to the websocket and emits
|
|
66
|
+
* events to the rest of the application.
|
|
67
|
+
* If you dont want to use react you can still use this.
|
|
68
|
+
*/
|
|
69
|
+
export class MultimodalLiveClient extends EventEmitter<MultimodalLiveClientEventTypes> {
|
|
70
|
+
public ws: WebSocket | null = null;
|
|
71
|
+
protected config: LiveConfig | null = null;
|
|
72
|
+
public url: string = "";
|
|
73
|
+
private runId: string;
|
|
74
|
+
private userId?: string;
|
|
75
|
+
constructor({ url, userId, runId }: MultimodalLiveAPIClientConnection) {
|
|
76
|
+
super();
|
|
77
|
+
url = url || `ws://localhost:8000/ws`;
|
|
78
|
+
this.url = new URL("ws", url).href;
|
|
79
|
+
this.userId = userId;
|
|
80
|
+
this.runId = runId || crypto.randomUUID(); // Ensure runId is always a string by providing default
|
|
81
|
+
this.send = this.send.bind(this);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
log(type: string, message: StreamingLog["message"]) {
|
|
85
|
+
const log: StreamingLog = {
|
|
86
|
+
date: new Date(),
|
|
87
|
+
type,
|
|
88
|
+
message,
|
|
89
|
+
};
|
|
90
|
+
this.emit("log", log);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
connect(newRunId?: string): Promise<boolean> {
|
|
94
|
+
const ws = new WebSocket(this.url);
|
|
95
|
+
|
|
96
|
+
// Update runId if provided
|
|
97
|
+
if (newRunId) {
|
|
98
|
+
this.runId = newRunId;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
ws.addEventListener("message", async (evt: MessageEvent) => {
|
|
102
|
+
if (evt.data instanceof Blob) {
|
|
103
|
+
this.receive(evt.data);
|
|
104
|
+
} else if (typeof evt.data === "string") {
|
|
105
|
+
try {
|
|
106
|
+
const jsonData = JSON.parse(evt.data);
|
|
107
|
+
if (jsonData.status) {
|
|
108
|
+
this.log("server.status", jsonData.status);
|
|
109
|
+
console.log("Status:", jsonData.status); // This will show in console
|
|
110
|
+
}
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.error("Error parsing message:", error);
|
|
113
|
+
}
|
|
114
|
+
} else {
|
|
115
|
+
console.log("Unhandled message type:", evt);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return new Promise((resolve, reject) => {
|
|
120
|
+
const onError = (ev: Event) => {
|
|
121
|
+
this.disconnect(ws);
|
|
122
|
+
const message = `Could not connect to "${this.url}"`;
|
|
123
|
+
this.log(`server.${ev.type}`, message);
|
|
124
|
+
reject(new Error(message));
|
|
125
|
+
};
|
|
126
|
+
ws.addEventListener("error", onError);
|
|
127
|
+
ws.addEventListener("open", (ev: Event) => {
|
|
128
|
+
this.log(`client.${ev.type}`, `connected to socket`);
|
|
129
|
+
this.emit("open");
|
|
130
|
+
|
|
131
|
+
this.ws = ws;
|
|
132
|
+
// Send initial setup message with runId
|
|
133
|
+
const setupMessage = {
|
|
134
|
+
setup: {
|
|
135
|
+
run_id: this.runId,
|
|
136
|
+
user_id: this.userId,
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
this._sendDirect(setupMessage);
|
|
140
|
+
ws.removeEventListener("error", onError);
|
|
141
|
+
ws.addEventListener("close", (ev: CloseEvent) => {
|
|
142
|
+
console.log(ev);
|
|
143
|
+
this.disconnect(ws);
|
|
144
|
+
let reason = ev.reason || "";
|
|
145
|
+
if (reason.toLowerCase().includes("error")) {
|
|
146
|
+
const prelude = "ERROR]";
|
|
147
|
+
const preludeIndex = reason.indexOf(prelude);
|
|
148
|
+
if (preludeIndex > 0) {
|
|
149
|
+
reason = reason.slice(
|
|
150
|
+
preludeIndex + prelude.length + 1,
|
|
151
|
+
Infinity,
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
this.log(
|
|
156
|
+
`server.${ev.type}`,
|
|
157
|
+
`disconnected ${reason ? `with reason: ${reason}` : ``}`,
|
|
158
|
+
);
|
|
159
|
+
this.emit("close", ev);
|
|
160
|
+
});
|
|
161
|
+
resolve(true);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
disconnect(ws?: WebSocket) {
|
|
167
|
+
// could be that this is an old websocket and there's already a new instance
|
|
168
|
+
// only close it if its still the correct reference
|
|
169
|
+
if ((!ws || this.ws === ws) && this.ws) {
|
|
170
|
+
this.ws.close();
|
|
171
|
+
this.ws = null;
|
|
172
|
+
this.log("client.close", `Disconnected`);
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
protected async receive(blob: Blob) {
|
|
178
|
+
const response = (await blobToJSON(blob)) as LiveIncomingMessage;
|
|
179
|
+
console.log("Parsed response:", response);
|
|
180
|
+
|
|
181
|
+
if (isToolCallMessage(response)) {
|
|
182
|
+
this.log("server.toolCall", response);
|
|
183
|
+
this.emit("toolcall", response.toolCall);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (isToolCallCancellationMessage(response)) {
|
|
187
|
+
this.log("receive.toolCallCancellation", response);
|
|
188
|
+
this.emit("toolcallcancellation", response.toolCallCancellation);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (isSetupCompleteMessage(response)) {
|
|
193
|
+
this.log("server.send", "setupComplete");
|
|
194
|
+
this.emit("setupcomplete");
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// this json also might be `contentUpdate { interrupted: true }`
|
|
199
|
+
// or contentUpdate { end_of_turn: true }
|
|
200
|
+
if (isServerContenteMessage(response)) {
|
|
201
|
+
const { serverContent } = response;
|
|
202
|
+
if (isInterrupted(serverContent)) {
|
|
203
|
+
this.log("receive.serverContent", "interrupted");
|
|
204
|
+
this.emit("interrupted");
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (isTurnComplete(serverContent)) {
|
|
208
|
+
this.log("server.send", "turnComplete");
|
|
209
|
+
this.emit("turncomplete");
|
|
210
|
+
//plausible there's more to the message, continue
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isModelTurn(serverContent)) {
|
|
214
|
+
let parts: Part[] = serverContent.modelTurn.parts;
|
|
215
|
+
|
|
216
|
+
// when its audio that is returned for modelTurn
|
|
217
|
+
const audioParts = parts.filter(
|
|
218
|
+
(p) => p.inlineData && p.inlineData.mimeType.startsWith("audio/pcm"),
|
|
219
|
+
);
|
|
220
|
+
const base64s = audioParts.map((p) => p.inlineData?.data);
|
|
221
|
+
|
|
222
|
+
// strip the audio parts out of the modelTurn
|
|
223
|
+
const otherParts = difference(parts, audioParts);
|
|
224
|
+
// console.log("otherParts", otherParts);
|
|
225
|
+
|
|
226
|
+
base64s.forEach((b64) => {
|
|
227
|
+
if (b64) {
|
|
228
|
+
const data = base64ToArrayBuffer(b64);
|
|
229
|
+
this.emit("audio", data);
|
|
230
|
+
this.log(`server.audio`, `buffer (${data.byteLength})`);
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
if (!otherParts.length) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
parts = otherParts;
|
|
238
|
+
|
|
239
|
+
const content: ModelTurn = { modelTurn: { parts } };
|
|
240
|
+
this.emit("content", content);
|
|
241
|
+
this.log(`server.content`, response);
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
console.log("received unmatched message", response);
|
|
245
|
+
this.log("received unmatched message", response);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* send realtimeInput, this is base64 chunks of "audio/pcm" and/or "image/jpg"
|
|
251
|
+
*/
|
|
252
|
+
sendRealtimeInput(chunks: GenerativeContentBlob[]) {
|
|
253
|
+
let hasAudio = false;
|
|
254
|
+
let hasVideo = false;
|
|
255
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
256
|
+
const ch = chunks[i];
|
|
257
|
+
if (ch.mimeType.includes("audio")) {
|
|
258
|
+
hasAudio = true;
|
|
259
|
+
}
|
|
260
|
+
if (ch.mimeType.includes("image")) {
|
|
261
|
+
hasVideo = true;
|
|
262
|
+
}
|
|
263
|
+
if (hasAudio && hasVideo) {
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const message =
|
|
268
|
+
hasAudio && hasVideo
|
|
269
|
+
? "audio + video"
|
|
270
|
+
: hasAudio
|
|
271
|
+
? "audio"
|
|
272
|
+
: hasVideo
|
|
273
|
+
? "video"
|
|
274
|
+
: "unknown";
|
|
275
|
+
|
|
276
|
+
const data: RealtimeInputMessage = {
|
|
277
|
+
realtimeInput: {
|
|
278
|
+
mediaChunks: chunks,
|
|
279
|
+
},
|
|
280
|
+
};
|
|
281
|
+
this._sendDirect(data);
|
|
282
|
+
this.log(`client.realtimeInput`, message);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* send a response to a function call and provide the id of the functions you are responding to
|
|
287
|
+
*/
|
|
288
|
+
sendToolResponse(toolResponse: ToolResponseMessage["toolResponse"]) {
|
|
289
|
+
const message: ToolResponseMessage = {
|
|
290
|
+
toolResponse,
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
this._sendDirect(message);
|
|
294
|
+
this.log(`client.toolResponse`, message);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* send normal content parts such as { text }
|
|
299
|
+
*/
|
|
300
|
+
send(parts: Part | Part[], turnComplete: boolean = true) {
|
|
301
|
+
parts = Array.isArray(parts) ? parts : [parts];
|
|
302
|
+
const content: Content = {
|
|
303
|
+
role: "user",
|
|
304
|
+
parts,
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const clientContentRequest: ClientContentMessage = {
|
|
308
|
+
clientContent: {
|
|
309
|
+
turns: [content],
|
|
310
|
+
turnComplete,
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
this._sendDirect(clientContentRequest);
|
|
315
|
+
this.log(`client.send`, clientContentRequest);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* used internally to send all messages
|
|
320
|
+
* don't use directly unless trying to send an unsupported message type
|
|
321
|
+
*/
|
|
322
|
+
_sendDirect(request: object) {
|
|
323
|
+
if (!this.ws) {
|
|
324
|
+
throw new Error("WebSocket is not connected");
|
|
325
|
+
}
|
|
326
|
+
const str = JSON.stringify(request);
|
|
327
|
+
this.ws.send(str);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { create } from "zustand";
|
|
18
|
+
import { StreamingLog } from "../multimodal-live-types";
|
|
19
|
+
|
|
20
|
+
interface StoreLoggerState {
|
|
21
|
+
maxLogs: number;
|
|
22
|
+
logs: StreamingLog[];
|
|
23
|
+
log: (streamingLog: StreamingLog) => void;
|
|
24
|
+
clearLogs: () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const useLoggerStore = create<StoreLoggerState>((set, get) => ({
|
|
28
|
+
maxLogs: 500,
|
|
29
|
+
logs: [], //mockLogs,
|
|
30
|
+
log: ({ date, type, message }: StreamingLog) => {
|
|
31
|
+
set((state) => {
|
|
32
|
+
const prevLog = state.logs.at(-1);
|
|
33
|
+
if (prevLog && prevLog.type === type && prevLog.message === message) {
|
|
34
|
+
return {
|
|
35
|
+
logs: [
|
|
36
|
+
...state.logs.slice(0, -1),
|
|
37
|
+
{
|
|
38
|
+
date,
|
|
39
|
+
type,
|
|
40
|
+
message,
|
|
41
|
+
count: prevLog.count ? prevLog.count + 1 : 1,
|
|
42
|
+
} as StreamingLog,
|
|
43
|
+
],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
logs: [
|
|
48
|
+
...state.logs.slice(-(get().maxLogs - 1)),
|
|
49
|
+
{
|
|
50
|
+
date,
|
|
51
|
+
type,
|
|
52
|
+
message,
|
|
53
|
+
} as StreamingLog,
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
clearLogs: () => {
|
|
60
|
+
console.log("clear log");
|
|
61
|
+
set({ logs: [] });
|
|
62
|
+
},
|
|
63
|
+
setMaxLogs: (n: number) => set({ maxLogs: n }),
|
|
64
|
+
}));
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type GetAudioContextOptions = AudioContextOptions & {
|
|
18
|
+
id?: string;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const map: Map<string, AudioContext> = new Map();
|
|
22
|
+
|
|
23
|
+
export const audioContext: (
|
|
24
|
+
options?: GetAudioContextOptions,
|
|
25
|
+
) => Promise<AudioContext> = (() => {
|
|
26
|
+
const didInteract = new Promise((res) => {
|
|
27
|
+
window.addEventListener("pointerdown", res, { once: true });
|
|
28
|
+
window.addEventListener("keydown", res, { once: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
return async (options?: GetAudioContextOptions) => {
|
|
32
|
+
try {
|
|
33
|
+
const a = new Audio();
|
|
34
|
+
a.src =
|
|
35
|
+
"data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA";
|
|
36
|
+
await a.play();
|
|
37
|
+
if (options?.id && map.has(options.id)) {
|
|
38
|
+
const ctx = map.get(options.id);
|
|
39
|
+
if (ctx) {
|
|
40
|
+
return ctx;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const ctx = new AudioContext(options);
|
|
44
|
+
if (options?.id) {
|
|
45
|
+
map.set(options.id, ctx);
|
|
46
|
+
}
|
|
47
|
+
return ctx;
|
|
48
|
+
} catch (e) {
|
|
49
|
+
await didInteract;
|
|
50
|
+
if (options?.id && map.has(options.id)) {
|
|
51
|
+
const ctx = map.get(options.id);
|
|
52
|
+
if (ctx) {
|
|
53
|
+
return ctx;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const ctx = new AudioContext(options);
|
|
57
|
+
if (options?.id) {
|
|
58
|
+
map.set(options.id, ctx);
|
|
59
|
+
}
|
|
60
|
+
return ctx;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
})();
|
|
64
|
+
|
|
65
|
+
export const blobToJSON = (blob: Blob) =>
|
|
66
|
+
new Promise((resolve, reject) => {
|
|
67
|
+
const reader = new FileReader();
|
|
68
|
+
reader.onload = () => {
|
|
69
|
+
if (reader.result) {
|
|
70
|
+
const json = JSON.parse(reader.result as string);
|
|
71
|
+
resolve(json);
|
|
72
|
+
} else {
|
|
73
|
+
reject("oops");
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
reader.readAsText(blob);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
export function base64ToArrayBuffer(base64: string) {
|
|
80
|
+
var binaryString = atob(base64);
|
|
81
|
+
var bytes = new Uint8Array(binaryString.length);
|
|
82
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
83
|
+
bytes[i] = binaryString.charCodeAt(i);
|
|
84
|
+
}
|
|
85
|
+
return bytes.buffer;
|
|
86
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const AudioRecordingWorklet = `
|
|
18
|
+
class AudioProcessingWorklet extends AudioWorkletProcessor {
|
|
19
|
+
|
|
20
|
+
// send and clear buffer every 2048 samples,
|
|
21
|
+
// which at 16khz is about 8 times a second
|
|
22
|
+
buffer = new Int16Array(2048);
|
|
23
|
+
|
|
24
|
+
// current write index
|
|
25
|
+
bufferWriteIndex = 0;
|
|
26
|
+
|
|
27
|
+
constructor() {
|
|
28
|
+
super();
|
|
29
|
+
this.hasAudio = false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param inputs Float32Array[][] [input#][channel#][sample#] so to access first inputs 1st channel inputs[0][0]
|
|
34
|
+
* @param outputs Float32Array[][]
|
|
35
|
+
*/
|
|
36
|
+
process(inputs) {
|
|
37
|
+
if (inputs[0].length) {
|
|
38
|
+
const channel0 = inputs[0][0];
|
|
39
|
+
this.processChunk(channel0);
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
sendAndClearBuffer(){
|
|
45
|
+
this.port.postMessage({
|
|
46
|
+
event: "chunk",
|
|
47
|
+
data: {
|
|
48
|
+
int16arrayBuffer: this.buffer.slice(0, this.bufferWriteIndex).buffer,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
this.bufferWriteIndex = 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
processChunk(float32Array) {
|
|
55
|
+
const l = float32Array.length;
|
|
56
|
+
|
|
57
|
+
for (let i = 0; i < l; i++) {
|
|
58
|
+
// convert float32 -1 to 1 to int16 -32768 to 32767
|
|
59
|
+
const int16Value = float32Array[i] * 32768;
|
|
60
|
+
this.buffer[this.bufferWriteIndex++] = int16Value;
|
|
61
|
+
if(this.bufferWriteIndex >= this.buffer.length) {
|
|
62
|
+
this.sendAndClearBuffer();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if(this.bufferWriteIndex >= this.buffer.length) {
|
|
67
|
+
this.sendAndClearBuffer();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
`;
|
|
72
|
+
|
|
73
|
+
export default AudioRecordingWorklet;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2024 Google LLC
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
* you may not use this file except in compliance with the License.
|
|
6
|
+
* You may obtain a copy of the License at
|
|
7
|
+
*
|
|
8
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
*
|
|
10
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
* See the License for the specific language governing permissions and
|
|
14
|
+
* limitations under the License.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const VolMeterWorket = `
|
|
18
|
+
class VolMeter extends AudioWorkletProcessor {
|
|
19
|
+
volume
|
|
20
|
+
updateIntervalInMS
|
|
21
|
+
nextUpdateFrame
|
|
22
|
+
|
|
23
|
+
constructor() {
|
|
24
|
+
super()
|
|
25
|
+
this.volume = 0
|
|
26
|
+
this.updateIntervalInMS = 25
|
|
27
|
+
this.nextUpdateFrame = this.updateIntervalInMS
|
|
28
|
+
this.port.onmessage = event => {
|
|
29
|
+
if (event.data.updateIntervalInMS) {
|
|
30
|
+
this.updateIntervalInMS = event.data.updateIntervalInMS
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get intervalInFrames() {
|
|
36
|
+
return (this.updateIntervalInMS / 1000) * sampleRate
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process(inputs) {
|
|
40
|
+
const input = inputs[0]
|
|
41
|
+
|
|
42
|
+
if (input.length > 0) {
|
|
43
|
+
const samples = input[0]
|
|
44
|
+
let sum = 0
|
|
45
|
+
let rms = 0
|
|
46
|
+
|
|
47
|
+
for (let i = 0; i < samples.length; ++i) {
|
|
48
|
+
sum += samples[i] * samples[i]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
rms = Math.sqrt(sum / samples.length)
|
|
52
|
+
this.volume = Math.max(rms, this.volume * 0.7)
|
|
53
|
+
|
|
54
|
+
this.nextUpdateFrame -= samples.length
|
|
55
|
+
if (this.nextUpdateFrame < 0) {
|
|
56
|
+
this.nextUpdateFrame += this.intervalInFrames
|
|
57
|
+
this.port.postMessage({volume: this.volume})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
}`;
|
|
64
|
+
|
|
65
|
+
export default VolMeterWorket;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2022",
|
|
4
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
5
|
+
"allowJs": true,
|
|
6
|
+
"skipLibCheck": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"forceConsistentCasingInFileNames": true,
|
|
11
|
+
"noFallthroughCasesInSwitch": true,
|
|
12
|
+
"module": "esnext",
|
|
13
|
+
"moduleResolution": "node",
|
|
14
|
+
"resolveJsonModule": true,
|
|
15
|
+
"isolatedModules": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"jsx": "preserve"
|
|
18
|
+
},
|
|
19
|
+
"include": ["src", "src/**/*"],
|
|
20
|
+
"ts-node": {
|
|
21
|
+
"compilerOptions": {
|
|
22
|
+
"module": "commonjs"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|