@mobileai/react-native 0.1.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/LICENSE +20 -0
- package/README.md +190 -0
- package/lib/module/components/AIAgent.js +149 -0
- package/lib/module/components/AIAgent.js.map +1 -0
- package/lib/module/components/AgentChatBar.js +120 -0
- package/lib/module/components/AgentChatBar.js.map +1 -0
- package/lib/module/components/AgentOverlay.js +53 -0
- package/lib/module/components/AgentOverlay.js.map +1 -0
- package/lib/module/core/AgentRuntime.js +498 -0
- package/lib/module/core/AgentRuntime.js.map +1 -0
- package/lib/module/core/FiberTreeWalker.js +308 -0
- package/lib/module/core/FiberTreeWalker.js.map +1 -0
- package/lib/module/core/MCPBridge.js +98 -0
- package/lib/module/core/MCPBridge.js.map +1 -0
- package/lib/module/core/ScreenDehydrator.js +46 -0
- package/lib/module/core/ScreenDehydrator.js.map +1 -0
- package/lib/module/core/types.js +2 -0
- package/lib/module/core/types.js.map +1 -0
- package/lib/module/hooks/useAction.js +32 -0
- package/lib/module/hooks/useAction.js.map +1 -0
- package/lib/module/index.js +17 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/providers/GeminiProvider.js +178 -0
- package/lib/module/providers/GeminiProvider.js.map +1 -0
- package/lib/module/utils/logger.js +17 -0
- package/lib/module/utils/logger.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/components/AIAgent.d.ts +57 -0
- package/lib/typescript/src/components/AIAgent.d.ts.map +1 -0
- package/lib/typescript/src/components/AgentChatBar.d.ts +14 -0
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +1 -0
- package/lib/typescript/src/components/AgentOverlay.d.ts +10 -0
- package/lib/typescript/src/components/AgentOverlay.d.ts.map +1 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts +37 -0
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +1 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts +26 -0
- package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +1 -0
- package/lib/typescript/src/core/MCPBridge.d.ts +23 -0
- package/lib/typescript/src/core/MCPBridge.d.ts.map +1 -0
- package/lib/typescript/src/core/ScreenDehydrator.d.ts +20 -0
- package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +1 -0
- package/lib/typescript/src/core/types.d.ts +138 -0
- package/lib/typescript/src/core/types.d.ts.map +1 -0
- package/lib/typescript/src/hooks/useAction.d.ts +13 -0
- package/lib/typescript/src/hooks/useAction.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +10 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/providers/GeminiProvider.d.ts +23 -0
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +1 -0
- package/lib/typescript/src/utils/logger.d.ts +7 -0
- package/lib/typescript/src/utils/logger.d.ts.map +1 -0
- package/package.json +143 -0
- package/src/components/AIAgent.tsx +222 -0
- package/src/components/AgentChatBar.tsx +136 -0
- package/src/components/AgentOverlay.tsx +48 -0
- package/src/core/AgentRuntime.ts +505 -0
- package/src/core/FiberTreeWalker.ts +349 -0
- package/src/core/MCPBridge.ts +110 -0
- package/src/core/ScreenDehydrator.ts +53 -0
- package/src/core/types.ts +185 -0
- package/src/hooks/useAction.ts +40 -0
- package/src/index.ts +22 -0
- package/src/providers/GeminiProvider.ts +210 -0
- package/src/utils/logger.ts +21 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAction — Optional hook to register non-UI actions for the AI agent.
|
|
3
|
+
*
|
|
4
|
+
* Use this for business logic that doesn't correspond to a visible UI element,
|
|
5
|
+
* e.g., API calls, cart operations, calculations.
|
|
6
|
+
*
|
|
7
|
+
* The Fiber tree walker handles visible UI elements automatically.
|
|
8
|
+
* useAction is for invisible operations the AI should be able to trigger.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { useEffect, useContext, createContext } from 'react';
|
|
12
|
+
import type { AgentRuntime } from '../core/AgentRuntime';
|
|
13
|
+
|
|
14
|
+
// Re-export context creation — AIAgent.tsx will provide the value
|
|
15
|
+
export const AgentContext = createContext<AgentRuntime | null>(null);
|
|
16
|
+
|
|
17
|
+
export function useAction(
|
|
18
|
+
name: string,
|
|
19
|
+
description: string,
|
|
20
|
+
parameters: Record<string, string>,
|
|
21
|
+
handler: (args: Record<string, any>) => any,
|
|
22
|
+
): void {
|
|
23
|
+
const agentRuntime = useContext(AgentContext);
|
|
24
|
+
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!agentRuntime) return;
|
|
27
|
+
|
|
28
|
+
agentRuntime.registerAction({
|
|
29
|
+
name,
|
|
30
|
+
description,
|
|
31
|
+
parameters,
|
|
32
|
+
handler,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return () => {
|
|
36
|
+
agentRuntime.unregisterAction(name);
|
|
37
|
+
};
|
|
38
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
39
|
+
}, [name, description]);
|
|
40
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @mobileai/react-native — Page-Agent Architecture
|
|
3
|
+
*
|
|
4
|
+
* Zero-wrapper AI agent for React Native.
|
|
5
|
+
* Auto-detects interactive elements via React Fiber tree traversal.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ─── Components ──────────────────────────────────────────────
|
|
9
|
+
export { AIAgent } from './components/AIAgent';
|
|
10
|
+
|
|
11
|
+
// ─── Hooks ───────────────────────────────────────────────────
|
|
12
|
+
export { useAction } from './hooks/useAction';
|
|
13
|
+
|
|
14
|
+
// ─── Types ───────────────────────────────────────────────────
|
|
15
|
+
export type {
|
|
16
|
+
AgentConfig,
|
|
17
|
+
ExecutionResult,
|
|
18
|
+
InteractiveElement,
|
|
19
|
+
DehydratedScreen,
|
|
20
|
+
ToolDefinition,
|
|
21
|
+
ActionDefinition,
|
|
22
|
+
} from './core/types';
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GeminiProvider — Simplified Gemini API integration.
|
|
3
|
+
* Sends dehydrated screen state + tools to Gemini and parses tool call responses.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { logger } from '../utils/logger';
|
|
7
|
+
import type { AIProvider, ToolDefinition, AgentStep } from '../core/types';
|
|
8
|
+
|
|
9
|
+
// ─── Gemini API Types ──────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
interface GeminiTool {
|
|
12
|
+
functionDeclarations: GeminiFunctionDeclaration[];
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface GeminiFunctionDeclaration {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
parameters: {
|
|
19
|
+
type: string;
|
|
20
|
+
properties: Record<string, { type: string; description: string; enum?: string[] }>;
|
|
21
|
+
required: string[];
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface GeminiContent {
|
|
26
|
+
role: 'user' | 'model';
|
|
27
|
+
parts: Array<{ text?: string; functionCall?: { name: string; args: any }; functionResponse?: { name: string; response: any } }>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// ─── Provider ──────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
export class GeminiProvider implements AIProvider {
|
|
33
|
+
private apiKey: string;
|
|
34
|
+
private model: string;
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
constructor(apiKey: string, model: string = 'gemini-2.5-flash') {
|
|
38
|
+
this.apiKey = apiKey;
|
|
39
|
+
this.model = model;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async generateContent(
|
|
43
|
+
systemPrompt: string,
|
|
44
|
+
userMessage: string,
|
|
45
|
+
tools: ToolDefinition[],
|
|
46
|
+
history: AgentStep[],
|
|
47
|
+
): Promise<{ toolCalls: Array<{ name: string; args: Record<string, any> }>; text?: string }> {
|
|
48
|
+
|
|
49
|
+
logger.info('GeminiProvider', `Sending request. Model: ${this.model}, Tools: ${tools.length}`);
|
|
50
|
+
|
|
51
|
+
// Build Gemini tools
|
|
52
|
+
const geminiTools = this.buildGeminiTools(tools);
|
|
53
|
+
|
|
54
|
+
// Build conversation history
|
|
55
|
+
const contents = this.buildContents(userMessage, history);
|
|
56
|
+
|
|
57
|
+
// Make API request
|
|
58
|
+
const url = `https://generativelanguage.googleapis.com/v1beta/models/${this.model}:generateContent?key=${this.apiKey}`;
|
|
59
|
+
|
|
60
|
+
const body: any = {
|
|
61
|
+
contents,
|
|
62
|
+
tools: geminiTools.length > 0 ? geminiTools : undefined,
|
|
63
|
+
systemInstruction: { parts: [{ text: systemPrompt }] },
|
|
64
|
+
generationConfig: {
|
|
65
|
+
temperature: 0.2,
|
|
66
|
+
maxOutputTokens: 2048,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
const response = await fetch(url, {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify(body),
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const elapsed = Date.now() - startTime;
|
|
80
|
+
logger.info('GeminiProvider', `Response received in ${elapsed}ms`);
|
|
81
|
+
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
const errorText = await response.text();
|
|
84
|
+
logger.error('GeminiProvider', `API error ${response.status}: ${errorText}`);
|
|
85
|
+
throw new Error(`Gemini API error ${response.status}: ${errorText}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
|
|
90
|
+
// Parse response
|
|
91
|
+
return this.parseResponse(data);
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
logger.error('GeminiProvider', 'Request failed:', error.message);
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─── Build Gemini Tools ────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
private buildGeminiTools(tools: ToolDefinition[]): GeminiTool[] {
|
|
101
|
+
const declarations: GeminiFunctionDeclaration[] = tools.map(tool => ({
|
|
102
|
+
name: tool.name,
|
|
103
|
+
description: tool.description,
|
|
104
|
+
parameters: {
|
|
105
|
+
type: 'OBJECT',
|
|
106
|
+
properties: Object.fromEntries(
|
|
107
|
+
Object.entries(tool.parameters).map(([key, param]) => [
|
|
108
|
+
key,
|
|
109
|
+
{
|
|
110
|
+
type: this.mapParamType(param.type),
|
|
111
|
+
description: param.description,
|
|
112
|
+
...(param.enum ? { enum: param.enum } : {}),
|
|
113
|
+
},
|
|
114
|
+
]),
|
|
115
|
+
),
|
|
116
|
+
required: Object.entries(tool.parameters)
|
|
117
|
+
.filter(([, param]) => param.required !== false)
|
|
118
|
+
.map(([key]) => key),
|
|
119
|
+
},
|
|
120
|
+
}));
|
|
121
|
+
|
|
122
|
+
return [{ functionDeclarations: declarations }];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private mapParamType(type: string): string {
|
|
126
|
+
switch (type) {
|
|
127
|
+
case 'number': return 'NUMBER';
|
|
128
|
+
case 'boolean': return 'BOOLEAN';
|
|
129
|
+
case 'string':
|
|
130
|
+
default: return 'STRING';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── Build Contents ────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
private buildContents(userMessage: string, history: AgentStep[]): GeminiContent[] {
|
|
137
|
+
const contents: GeminiContent[] = [];
|
|
138
|
+
|
|
139
|
+
// Add history as conversation turns
|
|
140
|
+
for (const step of history) {
|
|
141
|
+
// User turn (screen state was sent)
|
|
142
|
+
contents.push({
|
|
143
|
+
role: 'user',
|
|
144
|
+
parts: [{ text: `Step ${step.stepIndex + 1} result: ${step.action.output}` }],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Current user message
|
|
149
|
+
contents.push({
|
|
150
|
+
role: 'user',
|
|
151
|
+
parts: [{ text: userMessage }],
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Ensure alternating roles (Gemini requirement)
|
|
155
|
+
return this.ensureAlternatingRoles(contents);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private ensureAlternatingRoles(contents: GeminiContent[]): GeminiContent[] {
|
|
159
|
+
if (contents.length <= 1) return contents;
|
|
160
|
+
|
|
161
|
+
const merged: GeminiContent[] = [contents[0]!];
|
|
162
|
+
|
|
163
|
+
for (let i = 1; i < contents.length; i++) {
|
|
164
|
+
const prev = merged[merged.length - 1]!;
|
|
165
|
+
const curr = contents[i]!;
|
|
166
|
+
|
|
167
|
+
if (prev.role === curr.role) {
|
|
168
|
+
// Merge same-role messages
|
|
169
|
+
prev.parts.push(...curr.parts);
|
|
170
|
+
} else {
|
|
171
|
+
merged.push(curr);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return merged;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ─── Parse Response ────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
private parseResponse(data: any): { toolCalls: Array<{ name: string; args: Record<string, any> }>; text?: string } {
|
|
181
|
+
const toolCalls: Array<{ name: string; args: Record<string, any> }> = [];
|
|
182
|
+
let text: string | undefined;
|
|
183
|
+
|
|
184
|
+
if (!data.candidates || data.candidates.length === 0) {
|
|
185
|
+
logger.warn('GeminiProvider', 'No candidates in response');
|
|
186
|
+
return { toolCalls, text: 'No response generated.' };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const candidate = data.candidates[0];
|
|
190
|
+
const parts = candidate.content?.parts || [];
|
|
191
|
+
|
|
192
|
+
for (const part of parts) {
|
|
193
|
+
if (part.functionCall) {
|
|
194
|
+
toolCalls.push({
|
|
195
|
+
name: part.functionCall.name,
|
|
196
|
+
args: part.functionCall.args || {},
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
if (part.text) {
|
|
200
|
+
text = (text || '') + part.text;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
logger.info('GeminiProvider', `Parsed: ${toolCalls.length} tool calls, text: ${text ? 'yes' : 'no'}`);
|
|
205
|
+
|
|
206
|
+
return { toolCalls, text };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logger utility — prefixed console output for easy filtering.
|
|
3
|
+
*/
|
|
4
|
+
const TAG = '[AIAgent]';
|
|
5
|
+
|
|
6
|
+
export const logger = {
|
|
7
|
+
info: (context: string, ...args: any[]) =>
|
|
8
|
+
console.log(`${TAG} [${context}]`, ...args),
|
|
9
|
+
|
|
10
|
+
warn: (context: string, ...args: any[]) =>
|
|
11
|
+
console.warn(`${TAG} [${context}]`, ...args),
|
|
12
|
+
|
|
13
|
+
error: (context: string, ...args: any[]) =>
|
|
14
|
+
console.error(`${TAG} [${context}]`, ...args),
|
|
15
|
+
|
|
16
|
+
debug: (context: string, ...args: any[]) => {
|
|
17
|
+
if (__DEV__) {
|
|
18
|
+
console.log(`${TAG} [${context}] 🐛`, ...args);
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
};
|