@jay-framework/gemini-agent-plugin 0.12.0
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.
- package/dist/actions/send-message.jay-action +26 -0
- package/dist/actions/submit-tool-results.jay-action +29 -0
- package/dist/contracts/gemini-chat.jay-contract +69 -0
- package/dist/contracts/gemini-chat.jay-contract.d.ts +51 -0
- package/dist/index.client.js +242 -0
- package/dist/index.d.ts +244 -0
- package/dist/index.js +376 -0
- package/package.json +56 -0
- package/plugin.yaml +15 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: sendMessage
|
|
2
|
+
description: Send a user message to the Gemini agent. Includes conversation history, page automation tool definitions, and current page state. Returns either a text response or pending tool calls.
|
|
3
|
+
|
|
4
|
+
inputSchema:
|
|
5
|
+
message: string
|
|
6
|
+
history:
|
|
7
|
+
- role: string
|
|
8
|
+
parts:
|
|
9
|
+
- {}
|
|
10
|
+
toolDefinitions:
|
|
11
|
+
- name: string
|
|
12
|
+
description: string
|
|
13
|
+
inputSchema: {}
|
|
14
|
+
category: string
|
|
15
|
+
pageState: {}
|
|
16
|
+
|
|
17
|
+
outputSchema:
|
|
18
|
+
type: string
|
|
19
|
+
message?: string
|
|
20
|
+
calls?:
|
|
21
|
+
- id: string
|
|
22
|
+
name: string
|
|
23
|
+
args: {}
|
|
24
|
+
category: string
|
|
25
|
+
history:
|
|
26
|
+
- {}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
name: submitToolResults
|
|
2
|
+
description: Submit tool execution results back to continue the Gemini conversation. Called after the client executes page automation tools. Returns either a text response or more pending tool calls.
|
|
3
|
+
|
|
4
|
+
inputSchema:
|
|
5
|
+
results:
|
|
6
|
+
- callId: string
|
|
7
|
+
result: string
|
|
8
|
+
isError: boolean
|
|
9
|
+
history:
|
|
10
|
+
- role: string
|
|
11
|
+
parts:
|
|
12
|
+
- {}
|
|
13
|
+
toolDefinitions:
|
|
14
|
+
- name: string
|
|
15
|
+
description: string
|
|
16
|
+
inputSchema: {}
|
|
17
|
+
category: string
|
|
18
|
+
pageState: {}
|
|
19
|
+
|
|
20
|
+
outputSchema:
|
|
21
|
+
type: string
|
|
22
|
+
message?: string
|
|
23
|
+
calls?:
|
|
24
|
+
- id: string
|
|
25
|
+
name: string
|
|
26
|
+
args: {}
|
|
27
|
+
category: string
|
|
28
|
+
history:
|
|
29
|
+
- {}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
name: gemini-chat
|
|
2
|
+
description: AI chat agent powered by Gemini with page automation capabilities
|
|
3
|
+
tags:
|
|
4
|
+
# Message list — repeated sub-contract for full chat history
|
|
5
|
+
- tag: messages
|
|
6
|
+
type: sub-contract
|
|
7
|
+
repeated: true
|
|
8
|
+
trackBy: index
|
|
9
|
+
phase: fast+interactive
|
|
10
|
+
tags:
|
|
11
|
+
- tag: index
|
|
12
|
+
type: data
|
|
13
|
+
dataType: number
|
|
14
|
+
- tag: role
|
|
15
|
+
type: [data, variant]
|
|
16
|
+
dataType: enum (user | assistant)
|
|
17
|
+
- tag: content
|
|
18
|
+
type: data
|
|
19
|
+
dataType: string
|
|
20
|
+
|
|
21
|
+
# Compact mode shortcuts — avoid iterating full list
|
|
22
|
+
- tag: lastUserMessage
|
|
23
|
+
type: data
|
|
24
|
+
dataType: string
|
|
25
|
+
phase: fast+interactive
|
|
26
|
+
- tag: lastAssistantMessage
|
|
27
|
+
type: data
|
|
28
|
+
dataType: string
|
|
29
|
+
phase: fast+interactive
|
|
30
|
+
|
|
31
|
+
# Chat input
|
|
32
|
+
- tag: messageInput
|
|
33
|
+
type: [data, interactive]
|
|
34
|
+
dataType: string
|
|
35
|
+
elementType: HTMLInputElement
|
|
36
|
+
|
|
37
|
+
# Send button
|
|
38
|
+
- tag: sendMessage
|
|
39
|
+
type: interactive
|
|
40
|
+
elementType: HTMLButtonElement
|
|
41
|
+
|
|
42
|
+
# Expand/collapse toggle for compact mode
|
|
43
|
+
- tag: toggleExpand
|
|
44
|
+
type: interactive
|
|
45
|
+
elementType: HTMLButtonElement
|
|
46
|
+
|
|
47
|
+
# State variants
|
|
48
|
+
- tag: isLoading
|
|
49
|
+
type: variant
|
|
50
|
+
dataType: boolean
|
|
51
|
+
phase: fast+interactive
|
|
52
|
+
- tag: isExpanded
|
|
53
|
+
type: variant
|
|
54
|
+
dataType: boolean
|
|
55
|
+
phase: fast+interactive
|
|
56
|
+
- tag: hasError
|
|
57
|
+
type: variant
|
|
58
|
+
dataType: boolean
|
|
59
|
+
phase: fast+interactive
|
|
60
|
+
- tag: hasMessages
|
|
61
|
+
type: variant
|
|
62
|
+
dataType: boolean
|
|
63
|
+
phase: fast+interactive
|
|
64
|
+
|
|
65
|
+
# Error message
|
|
66
|
+
- tag: errorMessage
|
|
67
|
+
type: data
|
|
68
|
+
dataType: string
|
|
69
|
+
phase: fast+interactive
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import {HTMLElementCollectionProxy, HTMLElementProxy, JayContract} from "@jay-framework/runtime";
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
export enum Role {
|
|
5
|
+
user,
|
|
6
|
+
assistant
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface MessageOfGeminiChatViewState {
|
|
10
|
+
index: number,
|
|
11
|
+
role: Role,
|
|
12
|
+
content: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface GeminiChatViewState {
|
|
16
|
+
messages: Array<MessageOfGeminiChatViewState>,
|
|
17
|
+
lastUserMessage: string,
|
|
18
|
+
lastAssistantMessage: string,
|
|
19
|
+
messageInput: string,
|
|
20
|
+
isLoading: boolean,
|
|
21
|
+
isExpanded: boolean,
|
|
22
|
+
hasError: boolean,
|
|
23
|
+
hasMessages: boolean,
|
|
24
|
+
errorMessage: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type GeminiChatSlowViewState = {};
|
|
28
|
+
|
|
29
|
+
export type GeminiChatFastViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
|
|
30
|
+
messages: Array<GeminiChatViewState['messages'][number]>;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type GeminiChatInteractiveViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
|
|
34
|
+
messages: Array<GeminiChatViewState['messages'][number]>;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
export interface GeminiChatRefs {
|
|
39
|
+
messageInput: HTMLElementProxy<GeminiChatViewState, HTMLInputElement>,
|
|
40
|
+
sendMessage: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>,
|
|
41
|
+
toggleExpand: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
export interface GeminiChatRepeatedRefs {
|
|
46
|
+
messageInput: HTMLElementCollectionProxy<GeminiChatViewState, HTMLInputElement>,
|
|
47
|
+
sendMessage: HTMLElementCollectionProxy<GeminiChatViewState, HTMLButtonElement>,
|
|
48
|
+
toggleExpand: HTMLElementCollectionProxy<GeminiChatViewState, HTMLButtonElement>
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export type GeminiChatContract = JayContract<GeminiChatViewState, GeminiChatRefs, GeminiChatSlowViewState, GeminiChatFastViewState, GeminiChatInteractiveViewState>
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import { createJayService, makeJayInit, makeJayStackComponent } from "@jay-framework/fullstack-component";
|
|
2
|
+
import { createSignal, createMemo } from "@jay-framework/component";
|
|
3
|
+
import { createActionCaller } from "@jay-framework/stack-client-runtime";
|
|
4
|
+
const GEMINI_SERVICE = createJayService("GeminiService");
|
|
5
|
+
const init = makeJayInit();
|
|
6
|
+
const callSendMessage = createActionCaller("geminiAgent.sendMessage", "POST");
|
|
7
|
+
const callSubmitToolResults = createActionCaller("geminiAgent.submitToolResults", "POST");
|
|
8
|
+
function buildSerializedTools(automation) {
|
|
9
|
+
const { interactions } = automation.getPageState();
|
|
10
|
+
const tools = [];
|
|
11
|
+
for (const group of interactions) {
|
|
12
|
+
const sample = group.items[0];
|
|
13
|
+
if (!sample)
|
|
14
|
+
continue;
|
|
15
|
+
const elementType = sample.element.constructor.name;
|
|
16
|
+
const isFillable = [
|
|
17
|
+
"HTMLInputElement",
|
|
18
|
+
"HTMLTextAreaElement",
|
|
19
|
+
"HTMLSelectElement"
|
|
20
|
+
].includes(elementType);
|
|
21
|
+
const isCheckbox = elementType === "HTMLInputElement" && ["checkbox", "radio"].includes(sample.element.type);
|
|
22
|
+
const isForEach = group.items.length > 1 || sample.coordinate.length > 1;
|
|
23
|
+
const prefix = isCheckbox ? "toggle" : isFillable ? "fill" : "click";
|
|
24
|
+
const toolName = `${prefix}-${toKebab(group.refName)}`;
|
|
25
|
+
const humanName = toHumanReadable(group.refName);
|
|
26
|
+
const description = group.description || `${isCheckbox ? "Toggle" : isFillable ? "Fill" : "Click"} ${humanName}${isForEach ? " for a specific item" : ""}`;
|
|
27
|
+
const properties = {};
|
|
28
|
+
const required = [];
|
|
29
|
+
if (isForEach) {
|
|
30
|
+
properties.coordinate = {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: `Item coordinate (e.g. "${sample.coordinate.join("/")}")`
|
|
33
|
+
};
|
|
34
|
+
required.push("coordinate");
|
|
35
|
+
}
|
|
36
|
+
if (isFillable && !isCheckbox) {
|
|
37
|
+
properties.value = {
|
|
38
|
+
type: "string",
|
|
39
|
+
description: `Value to set in ${humanName}`
|
|
40
|
+
};
|
|
41
|
+
required.push("value");
|
|
42
|
+
}
|
|
43
|
+
tools.push({
|
|
44
|
+
name: toolName,
|
|
45
|
+
description,
|
|
46
|
+
inputSchema: {
|
|
47
|
+
type: "object",
|
|
48
|
+
properties,
|
|
49
|
+
required: required.length > 0 ? required : void 0
|
|
50
|
+
},
|
|
51
|
+
category: "page-automation"
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return tools;
|
|
55
|
+
}
|
|
56
|
+
function toKebab(s) {
|
|
57
|
+
return s.replace(/([a-z])([A-Z])/g, "$1-$2").toLowerCase();
|
|
58
|
+
}
|
|
59
|
+
function toHumanReadable(s) {
|
|
60
|
+
return s.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/[-_]/g, " ").toLowerCase();
|
|
61
|
+
}
|
|
62
|
+
function executePageAutomationTool(automation, call) {
|
|
63
|
+
try {
|
|
64
|
+
const { interactions } = automation.getPageState();
|
|
65
|
+
const toolName = call.name;
|
|
66
|
+
const args = call.args;
|
|
67
|
+
const parts = toolName.split("-");
|
|
68
|
+
const prefix = parts[0];
|
|
69
|
+
const refName = parts.slice(1).join("-");
|
|
70
|
+
const group = interactions.find((g) => toKebab(g.refName) === refName);
|
|
71
|
+
if (!group) {
|
|
72
|
+
return {
|
|
73
|
+
callId: call.id,
|
|
74
|
+
result: JSON.stringify({ error: `Interaction '${refName}' not found` }),
|
|
75
|
+
isError: true
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
let item = group.items[0];
|
|
79
|
+
if (args.coordinate && typeof args.coordinate === "string") {
|
|
80
|
+
const coord = args.coordinate.split("/");
|
|
81
|
+
item = group.items.find((i) => i.coordinate.join("/") === args.coordinate) || group.items[0];
|
|
82
|
+
}
|
|
83
|
+
if (!item) {
|
|
84
|
+
return {
|
|
85
|
+
callId: call.id,
|
|
86
|
+
result: JSON.stringify({ error: "Item not found" }),
|
|
87
|
+
isError: true
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (prefix === "fill" && args.value != null) {
|
|
91
|
+
const el = item.element;
|
|
92
|
+
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
|
|
93
|
+
if (nativeInputValueSetter) {
|
|
94
|
+
nativeInputValueSetter.call(el, String(args.value));
|
|
95
|
+
} else {
|
|
96
|
+
el.value = String(args.value);
|
|
97
|
+
}
|
|
98
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
99
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
100
|
+
} else {
|
|
101
|
+
item.element.click();
|
|
102
|
+
}
|
|
103
|
+
const newState = automation.getPageState();
|
|
104
|
+
return {
|
|
105
|
+
callId: call.id,
|
|
106
|
+
result: JSON.stringify({ success: true, pageState: newState.viewState })
|
|
107
|
+
};
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
callId: call.id,
|
|
111
|
+
result: JSON.stringify({ error: error.message }),
|
|
112
|
+
isError: true
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function GeminiChatInteractive(_props, refs, fastViewState, _carryForward) {
|
|
117
|
+
const [getMessages, setMessages] = createSignal([]);
|
|
118
|
+
const [getHistory, setHistory] = createSignal([]);
|
|
119
|
+
const [getInputValue, setInputValue] = createSignal("");
|
|
120
|
+
const [getIsLoading, setIsLoading] = createSignal(false);
|
|
121
|
+
const [getIsExpanded, setIsExpanded] = createSignal(false);
|
|
122
|
+
const [getError, setError] = createSignal(null);
|
|
123
|
+
const hasMessages = createMemo(() => getMessages().length > 0);
|
|
124
|
+
const hasError = createMemo(() => getError() !== null);
|
|
125
|
+
const lastUserMessage = createMemo(() => {
|
|
126
|
+
const msgs = getMessages();
|
|
127
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
128
|
+
if (msgs[i].role === "user")
|
|
129
|
+
return msgs[i].content;
|
|
130
|
+
}
|
|
131
|
+
return "";
|
|
132
|
+
});
|
|
133
|
+
const lastAssistantMessage = createMemo(() => {
|
|
134
|
+
const msgs = getMessages();
|
|
135
|
+
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
136
|
+
if (msgs[i].role === "assistant")
|
|
137
|
+
return msgs[i].content;
|
|
138
|
+
}
|
|
139
|
+
return "";
|
|
140
|
+
});
|
|
141
|
+
const getAutomation = () => window.__jay?.automation || null;
|
|
142
|
+
function getToolsAndState() {
|
|
143
|
+
const automation = getAutomation();
|
|
144
|
+
if (!automation) {
|
|
145
|
+
return { toolDefinitions: [], pageState: {} };
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
toolDefinitions: buildSerializedTools(automation),
|
|
149
|
+
pageState: automation.getPageState().viewState
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function handleToolCalls(output) {
|
|
153
|
+
while (output.type === "tool-calls") {
|
|
154
|
+
setHistory(output.history);
|
|
155
|
+
const automation = getAutomation();
|
|
156
|
+
if (!automation) {
|
|
157
|
+
setError("AutomationAPI not available");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const results = output.calls.filter((c) => c.category === "page-automation").map((call) => executePageAutomationTool(automation, call));
|
|
161
|
+
const { toolDefinitions, pageState } = getToolsAndState();
|
|
162
|
+
const nextOutput = await callSubmitToolResults({
|
|
163
|
+
results,
|
|
164
|
+
history: output.history,
|
|
165
|
+
toolDefinitions,
|
|
166
|
+
pageState
|
|
167
|
+
});
|
|
168
|
+
output = nextOutput;
|
|
169
|
+
}
|
|
170
|
+
const finalOutput = output;
|
|
171
|
+
if (finalOutput.type === "response") {
|
|
172
|
+
setHistory(finalOutput.history);
|
|
173
|
+
setMessages((msgs) => [
|
|
174
|
+
...msgs,
|
|
175
|
+
{
|
|
176
|
+
index: msgs.length,
|
|
177
|
+
role: "assistant",
|
|
178
|
+
content: finalOutput.message
|
|
179
|
+
}
|
|
180
|
+
]);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
async function sendMessage() {
|
|
184
|
+
const message = getInputValue().trim();
|
|
185
|
+
if (!message || getIsLoading())
|
|
186
|
+
return;
|
|
187
|
+
setError(null);
|
|
188
|
+
setIsLoading(true);
|
|
189
|
+
setInputValue("");
|
|
190
|
+
setMessages((msgs) => [...msgs, { index: msgs.length, role: "user", content: message }]);
|
|
191
|
+
try {
|
|
192
|
+
const { toolDefinitions, pageState } = getToolsAndState();
|
|
193
|
+
const output = await callSendMessage({
|
|
194
|
+
message,
|
|
195
|
+
history: getHistory(),
|
|
196
|
+
toolDefinitions,
|
|
197
|
+
pageState
|
|
198
|
+
});
|
|
199
|
+
await handleToolCalls(output);
|
|
200
|
+
} catch (error) {
|
|
201
|
+
setError(error.message || "Failed to send message");
|
|
202
|
+
} finally {
|
|
203
|
+
setIsLoading(false);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
refs.sendMessage.onclick(sendMessage);
|
|
207
|
+
refs.toggleExpand.onclick(() => {
|
|
208
|
+
setIsExpanded((v) => !v);
|
|
209
|
+
});
|
|
210
|
+
refs.messageInput.oninput((jayEvent) => {
|
|
211
|
+
setInputValue(jayEvent.event.target.value);
|
|
212
|
+
});
|
|
213
|
+
refs.messageInput.onkeydown((jayEvent) => {
|
|
214
|
+
if (jayEvent.event.key === "Enter" && !jayEvent.event.shiftKey) {
|
|
215
|
+
jayEvent.event.preventDefault();
|
|
216
|
+
sendMessage();
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
return {
|
|
220
|
+
render: () => ({
|
|
221
|
+
messages: () => getMessages().map((m) => ({
|
|
222
|
+
index: m.index,
|
|
223
|
+
role: m.role,
|
|
224
|
+
content: m.content
|
|
225
|
+
})),
|
|
226
|
+
lastUserMessage,
|
|
227
|
+
lastAssistantMessage,
|
|
228
|
+
messageInput: getInputValue,
|
|
229
|
+
isLoading: getIsLoading,
|
|
230
|
+
isExpanded: getIsExpanded,
|
|
231
|
+
hasError,
|
|
232
|
+
hasMessages,
|
|
233
|
+
errorMessage: () => getError() || ""
|
|
234
|
+
})
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
const geminiChat = makeJayStackComponent().withProps().withInteractive(GeminiChatInteractive);
|
|
238
|
+
export {
|
|
239
|
+
GEMINI_SERVICE,
|
|
240
|
+
geminiChat,
|
|
241
|
+
init
|
|
242
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import * as _jay_framework_fullstack_component from '@jay-framework/fullstack-component';
|
|
2
|
+
import { Signals } from '@jay-framework/fullstack-component';
|
|
3
|
+
import * as _google_genai from '@google/genai';
|
|
4
|
+
import { PluginSetupContext, PluginSetupResult, ActionMetadata } from '@jay-framework/stack-server-runtime';
|
|
5
|
+
import * as _jay_framework_component from '@jay-framework/component';
|
|
6
|
+
import { HTMLElementProxy } from '@jay-framework/runtime';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared type definitions for the Gemini agent plugin.
|
|
10
|
+
*/
|
|
11
|
+
interface GeminiMessage {
|
|
12
|
+
role: 'user' | 'model';
|
|
13
|
+
parts: GeminiPart[];
|
|
14
|
+
}
|
|
15
|
+
type GeminiPart = GeminiTextPart | GeminiFunctionCallPart | GeminiFunctionResponsePart;
|
|
16
|
+
interface GeminiTextPart {
|
|
17
|
+
text: string;
|
|
18
|
+
}
|
|
19
|
+
interface GeminiFunctionCallPart {
|
|
20
|
+
functionCall: {
|
|
21
|
+
name: string;
|
|
22
|
+
args: Record<string, unknown>;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
interface GeminiFunctionResponsePart {
|
|
26
|
+
functionResponse: {
|
|
27
|
+
name: string;
|
|
28
|
+
response: Record<string, unknown>;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
interface SerializedToolDef {
|
|
32
|
+
name: string;
|
|
33
|
+
description: string;
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object';
|
|
36
|
+
properties: Record<string, any>;
|
|
37
|
+
required?: string[];
|
|
38
|
+
};
|
|
39
|
+
category: 'page-automation';
|
|
40
|
+
}
|
|
41
|
+
interface SendMessageInput {
|
|
42
|
+
/** User's message text */
|
|
43
|
+
message: string;
|
|
44
|
+
/** Full conversation history (Gemini format) */
|
|
45
|
+
history: GeminiMessage[];
|
|
46
|
+
/** Page automation tool definitions (serialized from client) */
|
|
47
|
+
toolDefinitions: SerializedToolDef[];
|
|
48
|
+
/** Current page state snapshot */
|
|
49
|
+
pageState: object;
|
|
50
|
+
}
|
|
51
|
+
interface PendingToolCall {
|
|
52
|
+
id: string;
|
|
53
|
+
name: string;
|
|
54
|
+
args: Record<string, unknown>;
|
|
55
|
+
category: 'page-automation' | 'server-action';
|
|
56
|
+
}
|
|
57
|
+
type SendMessageOutput = {
|
|
58
|
+
type: 'response';
|
|
59
|
+
message: string;
|
|
60
|
+
history: GeminiMessage[];
|
|
61
|
+
} | {
|
|
62
|
+
type: 'tool-calls';
|
|
63
|
+
calls: PendingToolCall[];
|
|
64
|
+
history: GeminiMessage[];
|
|
65
|
+
};
|
|
66
|
+
interface SubmitToolResultsInput {
|
|
67
|
+
/** Tool execution results */
|
|
68
|
+
results: ToolCallResult[];
|
|
69
|
+
/** Full conversation history (including the tool call turn) */
|
|
70
|
+
history: GeminiMessage[];
|
|
71
|
+
/** Resend tool definitions (page state may have changed) */
|
|
72
|
+
toolDefinitions: SerializedToolDef[];
|
|
73
|
+
/** Fresh page state (may have changed after tool execution) */
|
|
74
|
+
pageState: object;
|
|
75
|
+
}
|
|
76
|
+
interface ToolCallResult {
|
|
77
|
+
callId: string;
|
|
78
|
+
/** JSON-stringified result */
|
|
79
|
+
result: string;
|
|
80
|
+
isError?: boolean;
|
|
81
|
+
}
|
|
82
|
+
type SubmitToolResultsOutput = SendMessageOutput;
|
|
83
|
+
interface GeminiFunctionDeclaration {
|
|
84
|
+
name: string;
|
|
85
|
+
description: string;
|
|
86
|
+
parameters: {
|
|
87
|
+
type: 'object';
|
|
88
|
+
properties: Record<string, any>;
|
|
89
|
+
required?: string[];
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
interface GeminiServiceConfig {
|
|
93
|
+
apiKey: string;
|
|
94
|
+
model: string;
|
|
95
|
+
systemPrompt?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
declare class GeminiService {
|
|
99
|
+
private config;
|
|
100
|
+
private client;
|
|
101
|
+
constructor(config: GeminiServiceConfig);
|
|
102
|
+
get model(): string;
|
|
103
|
+
get systemPromptPrefix(): string | undefined;
|
|
104
|
+
generateWithTools(messages: GeminiMessage[], tools: GeminiFunctionDeclaration[], systemPrompt: string): Promise<_google_genai.GenerateContentResponse>;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
declare const GEMINI_SERVICE: _jay_framework_fullstack_component.ServiceMarker<GeminiService>;
|
|
108
|
+
declare const init: _jay_framework_fullstack_component.JayInitBuilderWithServer<{}>;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Setup handler for gemini-agent plugin.
|
|
112
|
+
*
|
|
113
|
+
* Creates config/.gemini.yaml template if missing, validates the API key.
|
|
114
|
+
*/
|
|
115
|
+
|
|
116
|
+
declare function setupGeminiAgent(ctx: PluginSetupContext): Promise<PluginSetupResult>;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Main entry point for chat messages.
|
|
120
|
+
*
|
|
121
|
+
* Receives the user message, full conversation history, tool definitions,
|
|
122
|
+
* and page state. Returns either a text response or pending tool calls.
|
|
123
|
+
*/
|
|
124
|
+
declare const sendMessage: _jay_framework_fullstack_component.JayAction<SendMessageInput, SendMessageOutput> & _jay_framework_fullstack_component.JayActionDefinition<SendMessageInput, SendMessageOutput, [GeminiService]>;
|
|
125
|
+
/**
|
|
126
|
+
* Continue after client executes page automation tools.
|
|
127
|
+
*
|
|
128
|
+
* Receives tool execution results, the updated history, and fresh page state.
|
|
129
|
+
* Returns either a text response or more pending tool calls.
|
|
130
|
+
*/
|
|
131
|
+
declare const submitToolResults: _jay_framework_fullstack_component.JayAction<SubmitToolResultsInput, SendMessageOutput> & _jay_framework_fullstack_component.JayActionDefinition<SubmitToolResultsInput, SendMessageOutput, [GeminiService]>;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Configuration for the Gemini agent plugin.
|
|
135
|
+
*/
|
|
136
|
+
interface GeminiAgentConfig {
|
|
137
|
+
/** Gemini API key */
|
|
138
|
+
apiKey: string;
|
|
139
|
+
/** Model name (default: gemini-2.0-flash) */
|
|
140
|
+
model: string;
|
|
141
|
+
/** Optional system prompt prefix prepended to the generated system prompt */
|
|
142
|
+
systemPrompt?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
declare enum Role {
|
|
146
|
+
user,
|
|
147
|
+
assistant
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface MessageOfGeminiChatViewState {
|
|
151
|
+
index: number,
|
|
152
|
+
role: Role,
|
|
153
|
+
content: string
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
interface GeminiChatViewState {
|
|
157
|
+
messages: Array<MessageOfGeminiChatViewState>,
|
|
158
|
+
lastUserMessage: string,
|
|
159
|
+
lastAssistantMessage: string,
|
|
160
|
+
messageInput: string,
|
|
161
|
+
isLoading: boolean,
|
|
162
|
+
isExpanded: boolean,
|
|
163
|
+
hasError: boolean,
|
|
164
|
+
hasMessages: boolean,
|
|
165
|
+
errorMessage: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
type GeminiChatSlowViewState = {};
|
|
169
|
+
|
|
170
|
+
type GeminiChatFastViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
|
|
171
|
+
messages: Array<GeminiChatViewState['messages'][number]>;
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
type GeminiChatInteractiveViewState = Pick<GeminiChatViewState, 'lastUserMessage' | 'lastAssistantMessage' | 'messageInput' | 'isLoading' | 'isExpanded' | 'hasError' | 'hasMessages' | 'errorMessage'> & {
|
|
175
|
+
messages: Array<GeminiChatViewState['messages'][number]>;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
interface GeminiChatRefs {
|
|
180
|
+
messageInput: HTMLElementProxy<GeminiChatViewState, HTMLInputElement>,
|
|
181
|
+
sendMessage: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>,
|
|
182
|
+
toggleExpand: HTMLElementProxy<GeminiChatViewState, HTMLButtonElement>
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface GeminiChatProps {
|
|
186
|
+
}
|
|
187
|
+
interface ChatCarryForward {
|
|
188
|
+
}
|
|
189
|
+
declare const geminiChat: _jay_framework_fullstack_component.JayStackComponentDefinition<GeminiChatRefs, GeminiChatSlowViewState, GeminiChatFastViewState, GeminiChatInteractiveViewState, [], [Signals<GeminiChatFastViewState>, ChatCarryForward], GeminiChatProps, {}, _jay_framework_component.JayComponentCore<GeminiChatProps, GeminiChatInteractiveViewState>>;
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Tool Bridge — converts jay-stack tool descriptors and action metadata
|
|
193
|
+
* to Gemini FunctionDeclarations.
|
|
194
|
+
*
|
|
195
|
+
* Only actions with .jay-action metadata are exposed to the AI agent.
|
|
196
|
+
* Page automation tools (from AutomationAPI) are always included.
|
|
197
|
+
*/
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Converts page automation tools and server actions into Gemini function declarations.
|
|
201
|
+
*
|
|
202
|
+
* - Client tools are included directly (already have schema).
|
|
203
|
+
* - Server actions are only included if they have .jay-action metadata.
|
|
204
|
+
* Actions without metadata are silently skipped (opt-in mechanism).
|
|
205
|
+
*/
|
|
206
|
+
declare function toGeminiTools(clientTools: SerializedToolDef[], serverActions: Array<{
|
|
207
|
+
actionName: string;
|
|
208
|
+
metadata: ActionMetadata;
|
|
209
|
+
}>): GeminiFunctionDeclaration[];
|
|
210
|
+
/**
|
|
211
|
+
* Maps a Gemini function call name back to its original action name.
|
|
212
|
+
* Reverses the `action_` prefix and `_` → `.` replacement.
|
|
213
|
+
*/
|
|
214
|
+
declare function resolveToolCallTarget(toolName: string, clientToolNames: Set<string>): {
|
|
215
|
+
category: 'page-automation';
|
|
216
|
+
name: string;
|
|
217
|
+
} | {
|
|
218
|
+
category: 'server-action';
|
|
219
|
+
name: string;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* System Prompt Builder — constructs the system prompt for Gemini.
|
|
224
|
+
*
|
|
225
|
+
* Page state and available server actions are included as context
|
|
226
|
+
* (not tools), so the LLM always knows the current state without
|
|
227
|
+
* wasting tool calls.
|
|
228
|
+
*/
|
|
229
|
+
interface ServerActionSummary {
|
|
230
|
+
name: string;
|
|
231
|
+
description?: string;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Builds the system prompt for a Gemini conversation turn.
|
|
235
|
+
*
|
|
236
|
+
* The prompt includes:
|
|
237
|
+
* 1. Custom prefix (from config) or default greeting
|
|
238
|
+
* 2. Current page state as JSON context
|
|
239
|
+
* 3. List of available server actions with descriptions
|
|
240
|
+
* 4. Instructions for tool use
|
|
241
|
+
*/
|
|
242
|
+
declare function buildSystemPrompt(pageState: object, serverActions: ServerActionSummary[], customPrefix?: string): string;
|
|
243
|
+
|
|
244
|
+
export { GEMINI_SERVICE, type GeminiAgentConfig, type GeminiFunctionCallPart, type GeminiFunctionDeclaration, type GeminiFunctionResponsePart, type GeminiMessage, type GeminiPart, GeminiService, type GeminiServiceConfig, type GeminiTextPart, type PendingToolCall, type SendMessageInput, type SendMessageOutput, type SerializedToolDef, type ServerActionSummary, type SubmitToolResultsInput, type SubmitToolResultsOutput, type ToolCallResult, buildSystemPrompt, geminiChat, init, resolveToolCallTarget, sendMessage, setupGeminiAgent, submitToolResults, toGeminiTools };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __publicField = (obj, key, value) => {
|
|
4
|
+
__defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
return value;
|
|
6
|
+
};
|
|
7
|
+
import { createJayService, makeJayInit, makeJayAction, makeJayStackComponent, RenderPipeline } from "@jay-framework/fullstack-component";
|
|
8
|
+
import { registerService, actionRegistry } from "@jay-framework/stack-server-runtime";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import * as yaml from "js-yaml";
|
|
12
|
+
import { Type, GoogleGenAI } from "@google/genai";
|
|
13
|
+
const CONFIG_FILE_NAME$1 = ".gemini.yaml";
|
|
14
|
+
const DEFAULT_MODEL = "gemini-2.0-flash";
|
|
15
|
+
function loadConfig() {
|
|
16
|
+
const configPath = path.join(process.cwd(), "config", CONFIG_FILE_NAME$1);
|
|
17
|
+
if (!fs.existsSync(configPath)) {
|
|
18
|
+
throw new Error(
|
|
19
|
+
`Gemini config file not found at: ${configPath}
|
|
20
|
+
Run "jay-stack setup gemini-agent" to create it.`
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
const fileContents = fs.readFileSync(configPath, "utf8");
|
|
24
|
+
const config = yaml.load(fileContents);
|
|
25
|
+
if (!config) {
|
|
26
|
+
throw new Error("Gemini config file is empty or invalid");
|
|
27
|
+
}
|
|
28
|
+
if (config.apiKey === void 0 || config.apiKey === null) {
|
|
29
|
+
throw new Error('Config validation failed: "apiKey" is required in .gemini.yaml');
|
|
30
|
+
}
|
|
31
|
+
if (typeof config.apiKey !== "string" || config.apiKey.trim() === "") {
|
|
32
|
+
throw new Error('Config validation failed: "apiKey" must be a non-empty string');
|
|
33
|
+
}
|
|
34
|
+
if (config.apiKey.startsWith("<")) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'Config validation failed: "apiKey" still has placeholder value. Replace it with your Gemini API key.'
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
apiKey: config.apiKey,
|
|
41
|
+
model: config.model || DEFAULT_MODEL,
|
|
42
|
+
systemPrompt: config.systemPrompt || void 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const TYPE_MAP = {
|
|
46
|
+
object: Type.OBJECT,
|
|
47
|
+
string: Type.STRING,
|
|
48
|
+
number: Type.NUMBER,
|
|
49
|
+
integer: Type.INTEGER,
|
|
50
|
+
boolean: Type.BOOLEAN,
|
|
51
|
+
array: Type.ARRAY
|
|
52
|
+
};
|
|
53
|
+
function toGeminiSchema(schema) {
|
|
54
|
+
const result = {};
|
|
55
|
+
if (schema.type) {
|
|
56
|
+
result.type = TYPE_MAP[schema.type] || Type.STRING;
|
|
57
|
+
}
|
|
58
|
+
if (schema.description) {
|
|
59
|
+
result.description = schema.description;
|
|
60
|
+
}
|
|
61
|
+
if (schema.enum) {
|
|
62
|
+
result.enum = schema.enum;
|
|
63
|
+
}
|
|
64
|
+
if (schema.items) {
|
|
65
|
+
result.items = toGeminiSchema(schema.items);
|
|
66
|
+
}
|
|
67
|
+
if (schema.properties) {
|
|
68
|
+
result.properties = {};
|
|
69
|
+
for (const [key, value] of Object.entries(schema.properties)) {
|
|
70
|
+
result.properties[key] = toGeminiSchema(value);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (schema.required) {
|
|
74
|
+
result.required = schema.required;
|
|
75
|
+
}
|
|
76
|
+
return result;
|
|
77
|
+
}
|
|
78
|
+
function toSdkDeclaration(decl) {
|
|
79
|
+
return {
|
|
80
|
+
name: decl.name,
|
|
81
|
+
description: decl.description,
|
|
82
|
+
parameters: toGeminiSchema(decl.parameters)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
class GeminiService {
|
|
86
|
+
constructor(config) {
|
|
87
|
+
__publicField(this, "client");
|
|
88
|
+
this.config = config;
|
|
89
|
+
this.client = new GoogleGenAI({ apiKey: config.apiKey });
|
|
90
|
+
}
|
|
91
|
+
get model() {
|
|
92
|
+
return this.config.model;
|
|
93
|
+
}
|
|
94
|
+
get systemPromptPrefix() {
|
|
95
|
+
return this.config.systemPrompt;
|
|
96
|
+
}
|
|
97
|
+
async generateWithTools(messages, tools, systemPrompt) {
|
|
98
|
+
const sdkDeclarations = tools.map(toSdkDeclaration);
|
|
99
|
+
const response = await this.client.models.generateContent({
|
|
100
|
+
model: this.config.model,
|
|
101
|
+
contents: messages,
|
|
102
|
+
config: {
|
|
103
|
+
systemInstruction: systemPrompt,
|
|
104
|
+
tools: sdkDeclarations.length > 0 ? [{ functionDeclarations: sdkDeclarations }] : void 0
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
return response;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const GEMINI_SERVICE = createJayService("GeminiService");
|
|
111
|
+
const init = makeJayInit().withServer(() => {
|
|
112
|
+
const config = loadConfig();
|
|
113
|
+
const service = new GeminiService({
|
|
114
|
+
apiKey: config.apiKey,
|
|
115
|
+
model: config.model,
|
|
116
|
+
systemPrompt: config.systemPrompt
|
|
117
|
+
});
|
|
118
|
+
registerService(GEMINI_SERVICE, service);
|
|
119
|
+
console.log(`[gemini-agent] Initialized (model: ${config.model})`);
|
|
120
|
+
return {};
|
|
121
|
+
});
|
|
122
|
+
const CONFIG_FILE_NAME = ".gemini.yaml";
|
|
123
|
+
const CONFIG_TEMPLATE = `# Gemini Agent Configuration
|
|
124
|
+
#
|
|
125
|
+
# This file contains credentials for the Gemini AI agent plugin.
|
|
126
|
+
# Get your API key from: https://aistudio.google.com/apikey
|
|
127
|
+
#
|
|
128
|
+
# IMPORTANT: This file contains secrets. Add config/.gemini.yaml to .gitignore.
|
|
129
|
+
|
|
130
|
+
# Required: Your Gemini API key
|
|
131
|
+
apiKey: "<your-gemini-api-key>"
|
|
132
|
+
|
|
133
|
+
# Optional: Model name (default: gemini-2.0-flash)
|
|
134
|
+
# model: gemini-2.0-flash
|
|
135
|
+
|
|
136
|
+
# Optional: Custom system prompt prefix (prepended to the generated system prompt)
|
|
137
|
+
# systemPrompt: "You are a helpful assistant for this web application."
|
|
138
|
+
`;
|
|
139
|
+
async function setupGeminiAgent(ctx) {
|
|
140
|
+
const configPath = path.join(ctx.configDir, CONFIG_FILE_NAME);
|
|
141
|
+
if (!fs.existsSync(configPath)) {
|
|
142
|
+
if (!fs.existsSync(ctx.configDir)) {
|
|
143
|
+
fs.mkdirSync(ctx.configDir, { recursive: true });
|
|
144
|
+
}
|
|
145
|
+
fs.writeFileSync(configPath, CONFIG_TEMPLATE, "utf-8");
|
|
146
|
+
return {
|
|
147
|
+
status: "needs-config",
|
|
148
|
+
configCreated: [`config/${CONFIG_FILE_NAME}`],
|
|
149
|
+
message: "Fill in your Gemini API key and re-run: jay-stack setup gemini-agent"
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
try {
|
|
153
|
+
const configContent = fs.readFileSync(configPath, "utf-8");
|
|
154
|
+
const config = yaml.load(configContent);
|
|
155
|
+
if (!config) {
|
|
156
|
+
return {
|
|
157
|
+
status: "error",
|
|
158
|
+
message: `Config file is empty: config/${CONFIG_FILE_NAME}`
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const apiKey = config.apiKey || "";
|
|
162
|
+
const hasPlaceholder = apiKey.startsWith("<");
|
|
163
|
+
const isEmpty = !apiKey;
|
|
164
|
+
if (hasPlaceholder || isEmpty) {
|
|
165
|
+
return {
|
|
166
|
+
status: "needs-config",
|
|
167
|
+
message: `Config has placeholder value. Set your Gemini API key in config/${CONFIG_FILE_NAME}`
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
if (ctx.initError) {
|
|
171
|
+
return {
|
|
172
|
+
status: "error",
|
|
173
|
+
message: `Gemini initialization failed: ${ctx.initError.message}`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
status: "configured",
|
|
178
|
+
message: `Gemini agent configured (model: ${config.model || "gemini-2.0-flash"})`
|
|
179
|
+
};
|
|
180
|
+
} catch (error) {
|
|
181
|
+
return {
|
|
182
|
+
status: "error",
|
|
183
|
+
message: `Failed to read config: ${error.message}`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function toGeminiTools(clientTools, serverActions) {
|
|
188
|
+
const tools = [];
|
|
189
|
+
for (const tool of clientTools) {
|
|
190
|
+
tools.push({
|
|
191
|
+
name: tool.name,
|
|
192
|
+
description: tool.description,
|
|
193
|
+
parameters: tool.inputSchema
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
for (const { actionName, metadata } of serverActions) {
|
|
197
|
+
tools.push({
|
|
198
|
+
name: `action_${actionName.replace(/\./g, "_")}`,
|
|
199
|
+
description: metadata.description,
|
|
200
|
+
parameters: metadata.inputSchema
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return tools;
|
|
204
|
+
}
|
|
205
|
+
function resolveToolCallTarget(toolName, clientToolNames) {
|
|
206
|
+
if (clientToolNames.has(toolName)) {
|
|
207
|
+
return { category: "page-automation", name: toolName };
|
|
208
|
+
}
|
|
209
|
+
if (toolName.startsWith("action_")) {
|
|
210
|
+
const actionName = toolName.slice("action_".length).replace(/_/g, ".");
|
|
211
|
+
return { category: "server-action", name: actionName };
|
|
212
|
+
}
|
|
213
|
+
return { category: "page-automation", name: toolName };
|
|
214
|
+
}
|
|
215
|
+
function buildSystemPrompt(pageState, serverActions, customPrefix) {
|
|
216
|
+
const parts = [
|
|
217
|
+
customPrefix || "You are a helpful assistant for this web application.",
|
|
218
|
+
"",
|
|
219
|
+
"## Current Page State",
|
|
220
|
+
JSON.stringify(pageState, null, 2),
|
|
221
|
+
""
|
|
222
|
+
];
|
|
223
|
+
if (serverActions.length > 0) {
|
|
224
|
+
parts.push("## Available Server Actions");
|
|
225
|
+
for (const action of serverActions) {
|
|
226
|
+
parts.push(`- ${action.name}${action.description ? `: ${action.description}` : ""}`);
|
|
227
|
+
}
|
|
228
|
+
parts.push("");
|
|
229
|
+
}
|
|
230
|
+
parts.push(
|
|
231
|
+
"Use the provided tools to interact with the page and call server actions.",
|
|
232
|
+
"After using tools, describe what you did to the user.",
|
|
233
|
+
"The page state above is refreshed each turn — use it to understand what the user sees."
|
|
234
|
+
);
|
|
235
|
+
return parts.join("\n");
|
|
236
|
+
}
|
|
237
|
+
async function processGeminiTurn(service, history, tools, systemPrompt, clientToolNames) {
|
|
238
|
+
const response = await service.generateWithTools(history, tools, systemPrompt);
|
|
239
|
+
const candidate = response.candidates?.[0];
|
|
240
|
+
if (!candidate?.content?.parts) {
|
|
241
|
+
return {
|
|
242
|
+
type: "response",
|
|
243
|
+
message: "I apologize, but I was unable to generate a response.",
|
|
244
|
+
history
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const parts = candidate.content.parts;
|
|
248
|
+
const functionCalls = parts.filter(
|
|
249
|
+
(p) => p.functionCall != null
|
|
250
|
+
);
|
|
251
|
+
const responseParts = candidate.content.parts;
|
|
252
|
+
if (functionCalls.length === 0) {
|
|
253
|
+
const textParts = parts.filter((p) => p.text != null);
|
|
254
|
+
const message = textParts.map((p) => p.text).join("");
|
|
255
|
+
const updatedHistory2 = [
|
|
256
|
+
...history,
|
|
257
|
+
{ role: "model", parts: responseParts }
|
|
258
|
+
];
|
|
259
|
+
return {
|
|
260
|
+
type: "response",
|
|
261
|
+
message,
|
|
262
|
+
history: updatedHistory2
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const updatedHistory = [...history, { role: "model", parts: responseParts }];
|
|
266
|
+
const pendingClientCalls = [];
|
|
267
|
+
const serverCallResults = [];
|
|
268
|
+
for (const fc of functionCalls) {
|
|
269
|
+
const target = resolveToolCallTarget(fc.functionCall.name, clientToolNames);
|
|
270
|
+
if (target.category === "server-action") {
|
|
271
|
+
const result = await actionRegistry.execute(target.name, fc.functionCall.args);
|
|
272
|
+
serverCallResults.push({
|
|
273
|
+
functionResponse: {
|
|
274
|
+
name: fc.functionCall.name,
|
|
275
|
+
response: result.success ? { result: result.data } : { error: result.error?.message || "Unknown error" }
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
} else {
|
|
279
|
+
pendingClientCalls.push({
|
|
280
|
+
id: fc.functionCall.name,
|
|
281
|
+
name: fc.functionCall.name,
|
|
282
|
+
args: fc.functionCall.args || {},
|
|
283
|
+
category: "page-automation"
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
if (pendingClientCalls.length > 0) {
|
|
288
|
+
[
|
|
289
|
+
...pendingClientCalls,
|
|
290
|
+
// Server actions already executed — include as completed calls
|
|
291
|
+
...serverCallResults.map((r) => ({
|
|
292
|
+
id: r.functionResponse.name,
|
|
293
|
+
name: r.functionResponse.name,
|
|
294
|
+
args: {},
|
|
295
|
+
category: "server-action"
|
|
296
|
+
}))
|
|
297
|
+
];
|
|
298
|
+
return {
|
|
299
|
+
type: "tool-calls",
|
|
300
|
+
calls: pendingClientCalls,
|
|
301
|
+
history: updatedHistory
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const historyWithResults = [
|
|
305
|
+
...updatedHistory,
|
|
306
|
+
{ role: "user", parts: serverCallResults }
|
|
307
|
+
];
|
|
308
|
+
return processGeminiTurn(service, historyWithResults, tools, systemPrompt, clientToolNames);
|
|
309
|
+
}
|
|
310
|
+
async function handleConversation(service, history, toolDefinitions, pageState) {
|
|
311
|
+
const serverActions = actionRegistry.getActionsWithMetadata();
|
|
312
|
+
const tools = toGeminiTools(toolDefinitions, serverActions);
|
|
313
|
+
const serverActionSummaries = serverActions.map((a) => ({
|
|
314
|
+
name: a.actionName,
|
|
315
|
+
description: a.metadata.description
|
|
316
|
+
}));
|
|
317
|
+
const systemPrompt = buildSystemPrompt(
|
|
318
|
+
pageState,
|
|
319
|
+
serverActionSummaries,
|
|
320
|
+
service.systemPromptPrefix
|
|
321
|
+
);
|
|
322
|
+
const clientToolNames = new Set(toolDefinitions.map((t) => t.name));
|
|
323
|
+
return processGeminiTurn(service, history, tools, systemPrompt, clientToolNames);
|
|
324
|
+
}
|
|
325
|
+
const sendMessage = makeJayAction("geminiAgent.sendMessage").withServices(GEMINI_SERVICE).withHandler(async (input, service) => {
|
|
326
|
+
const { message, history, toolDefinitions, pageState } = input;
|
|
327
|
+
const updatedHistory = [
|
|
328
|
+
...history,
|
|
329
|
+
{ role: "user", parts: [{ text: message }] }
|
|
330
|
+
];
|
|
331
|
+
return handleConversation(service, updatedHistory, toolDefinitions, pageState);
|
|
332
|
+
});
|
|
333
|
+
const submitToolResults = makeJayAction("geminiAgent.submitToolResults").withServices(GEMINI_SERVICE).withHandler(async (input, service) => {
|
|
334
|
+
const { results, history, toolDefinitions, pageState } = input;
|
|
335
|
+
const functionResponses = results.map((r) => ({
|
|
336
|
+
functionResponse: {
|
|
337
|
+
name: r.callId,
|
|
338
|
+
response: r.isError ? { error: r.result } : JSON.parse(r.result)
|
|
339
|
+
}
|
|
340
|
+
}));
|
|
341
|
+
const updatedHistory = [
|
|
342
|
+
...history,
|
|
343
|
+
{ role: "user", parts: functionResponses }
|
|
344
|
+
];
|
|
345
|
+
return handleConversation(service, updatedHistory, toolDefinitions, pageState);
|
|
346
|
+
});
|
|
347
|
+
async function fastRender() {
|
|
348
|
+
const Pipeline = RenderPipeline.for();
|
|
349
|
+
return Pipeline.ok({}).toPhaseOutput(() => ({
|
|
350
|
+
viewState: {
|
|
351
|
+
messages: [],
|
|
352
|
+
lastUserMessage: "",
|
|
353
|
+
lastAssistantMessage: "",
|
|
354
|
+
messageInput: "",
|
|
355
|
+
isLoading: false,
|
|
356
|
+
isExpanded: false,
|
|
357
|
+
hasError: false,
|
|
358
|
+
hasMessages: false,
|
|
359
|
+
errorMessage: ""
|
|
360
|
+
},
|
|
361
|
+
carryForward: {}
|
|
362
|
+
}));
|
|
363
|
+
}
|
|
364
|
+
const geminiChat = makeJayStackComponent().withProps().withFastRender(fastRender);
|
|
365
|
+
export {
|
|
366
|
+
GEMINI_SERVICE,
|
|
367
|
+
GeminiService,
|
|
368
|
+
buildSystemPrompt,
|
|
369
|
+
geminiChat,
|
|
370
|
+
init,
|
|
371
|
+
resolveToolCallTarget,
|
|
372
|
+
sendMessage,
|
|
373
|
+
setupGeminiAgent,
|
|
374
|
+
submitToolResults,
|
|
375
|
+
toGeminiTools
|
|
376
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jay-framework/gemini-agent-plugin",
|
|
3
|
+
"version": "0.12.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"description": "Gemini AI agent plugin for jay-stack — embedded chat agent with page automation and server action calling",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": "./dist/index.js",
|
|
11
|
+
"./client": "./dist/index.client.js",
|
|
12
|
+
"./gemini-chat.jay-contract": "./dist/contracts/gemini-chat.jay-contract",
|
|
13
|
+
"./send-message.jay-action": "./dist/actions/send-message.jay-action",
|
|
14
|
+
"./submit-tool-results.jay-action": "./dist/actions/submit-tool-results.jay-action",
|
|
15
|
+
"./plugin.yaml": "./plugin.yaml"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"plugin.yaml"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "npm run definitions && npm run build:client && npm run build:server && npm run build:copy-contract && npm run build:types",
|
|
23
|
+
"build:client": "vite build",
|
|
24
|
+
"build:server": "vite build --ssr",
|
|
25
|
+
"build:copy-contract": "mkdir -p dist/contracts && cp lib/contracts/*.jay-contract* dist/contracts/ && mkdir -p dist/actions && cp lib/actions/*.jay-action dist/actions/",
|
|
26
|
+
"build:types": "tsup lib/index.ts --dts-only --format esm",
|
|
27
|
+
"build:check-types": "tsc",
|
|
28
|
+
"definitions": "jay-cli definitions lib",
|
|
29
|
+
"clean": "rimraf dist",
|
|
30
|
+
"confirm": "npm run clean && npm run build && npm run build:check-types && npm run test",
|
|
31
|
+
"test": "vitest run",
|
|
32
|
+
"test:watch": "vitest"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@google/genai": "^0.14.0",
|
|
36
|
+
"@jay-framework/component": "^0.12.0",
|
|
37
|
+
"@jay-framework/fullstack-component": "^0.12.0",
|
|
38
|
+
"@jay-framework/runtime": "^0.12.0",
|
|
39
|
+
"@jay-framework/runtime-automation": "^0.12.0",
|
|
40
|
+
"@jay-framework/stack-client-runtime": "^0.12.0",
|
|
41
|
+
"@jay-framework/stack-server-runtime": "^0.12.0",
|
|
42
|
+
"js-yaml": "^4.1.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"@jay-framework/compiler-jay-stack": "^0.12.0",
|
|
46
|
+
"@jay-framework/dev-environment": "^0.12.0",
|
|
47
|
+
"@jay-framework/jay-cli": "^0.12.0",
|
|
48
|
+
"@types/js-yaml": "^4.0.9",
|
|
49
|
+
"@types/node": "^22.15.21",
|
|
50
|
+
"rimraf": "^5.0.5",
|
|
51
|
+
"tsup": "^8.0.1",
|
|
52
|
+
"typescript": "^5.3.3",
|
|
53
|
+
"vite": "^5.0.11",
|
|
54
|
+
"vitest": "^1.2.1"
|
|
55
|
+
}
|
|
56
|
+
}
|
package/plugin.yaml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: gemini-agent
|
|
2
|
+
global: true
|
|
3
|
+
contracts:
|
|
4
|
+
- name: gemini-chat
|
|
5
|
+
contract: gemini-chat.jay-contract
|
|
6
|
+
component: geminiChat
|
|
7
|
+
description: AI chat agent powered by Gemini with page automation capabilities
|
|
8
|
+
actions:
|
|
9
|
+
- name: sendMessage
|
|
10
|
+
action: send-message.jay-action
|
|
11
|
+
- name: submitToolResults
|
|
12
|
+
action: submit-tool-results.jay-action
|
|
13
|
+
setup:
|
|
14
|
+
handler: setupGeminiAgent
|
|
15
|
+
description: Configure Gemini API key
|