@mobileai/react-native 0.9.16 → 0.9.18
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/README.md +2 -2
- package/package.json +5 -8
- package/lib/module/__cli_tmp__.js.map +0 -1
- package/lib/module/components/AIAgent.js.map +0 -1
- package/lib/module/components/AIZone.js.map +0 -1
- package/lib/module/components/AgentChatBar.js.map +0 -1
- package/lib/module/components/AgentErrorBoundary.js.map +0 -1
- package/lib/module/components/AgentOverlay.js.map +0 -1
- package/lib/module/components/DiscoveryTooltip.js.map +0 -1
- package/lib/module/components/HighlightOverlay.js.map +0 -1
- package/lib/module/components/Icons.js.map +0 -1
- package/lib/module/components/ProactiveHint.js.map +0 -1
- package/lib/module/components/cards/InfoCard.js.map +0 -1
- package/lib/module/components/cards/ReviewSummary.js.map +0 -1
- package/lib/module/config/endpoints.js.map +0 -1
- package/lib/module/core/ActionRegistry.js.map +0 -1
- package/lib/module/core/AgentRuntime.js.map +0 -1
- package/lib/module/core/FiberTreeWalker.js.map +0 -1
- package/lib/module/core/IdleDetector.js.map +0 -1
- package/lib/module/core/MCPBridge.js.map +0 -1
- package/lib/module/core/ScreenDehydrator.js.map +0 -1
- package/lib/module/core/ZoneRegistry.js.map +0 -1
- package/lib/module/core/systemPrompt.js.map +0 -1
- package/lib/module/core/types.js.map +0 -1
- package/lib/module/hooks/useAction.js.map +0 -1
- package/lib/module/index.js.map +0 -1
- package/lib/module/plugin/withAppIntents.js.map +0 -1
- package/lib/module/providers/GeminiProvider.js.map +0 -1
- package/lib/module/providers/OpenAIProvider.js.map +0 -1
- package/lib/module/providers/ProviderFactory.js.map +0 -1
- package/lib/module/services/AudioInputService.js.map +0 -1
- package/lib/module/services/AudioOutputService.js.map +0 -1
- package/lib/module/services/KnowledgeBaseService.js.map +0 -1
- package/lib/module/services/VoiceService.js.map +0 -1
- package/lib/module/services/flags/FlagService.js.map +0 -1
- package/lib/module/services/telemetry/MobileAI.js.map +0 -1
- package/lib/module/services/telemetry/PiiScrubber.js.map +0 -1
- package/lib/module/services/telemetry/TelemetryService.js.map +0 -1
- package/lib/module/services/telemetry/TouchAutoCapture.js.map +0 -1
- package/lib/module/services/telemetry/device.js.map +0 -1
- package/lib/module/services/telemetry/deviceMetadata.js.map +0 -1
- package/lib/module/services/telemetry/index.js.map +0 -1
- package/lib/module/services/telemetry/types.js.map +0 -1
- package/lib/module/support/CSATSurvey.js.map +0 -1
- package/lib/module/support/EscalationEventSource.js.map +0 -1
- package/lib/module/support/EscalationSocket.js.map +0 -1
- package/lib/module/support/SupportChatModal.js.map +0 -1
- package/lib/module/support/SupportGreeting.js.map +0 -1
- package/lib/module/support/TicketStore.js.map +0 -1
- package/lib/module/support/escalateTool.js.map +0 -1
- package/lib/module/support/index.js.map +0 -1
- package/lib/module/support/supportPrompt.js.map +0 -1
- package/lib/module/support/types.js.map +0 -1
- package/lib/module/tools/datePickerTool.js.map +0 -1
- package/lib/module/tools/guideTool.js.map +0 -1
- package/lib/module/tools/index.js.map +0 -1
- package/lib/module/tools/keyboardTool.js.map +0 -1
- package/lib/module/tools/longPressTool.js.map +0 -1
- package/lib/module/tools/pickerTool.js.map +0 -1
- package/lib/module/tools/restoreTool.js.map +0 -1
- package/lib/module/tools/scrollTool.js.map +0 -1
- package/lib/module/tools/simplifyTool.js.map +0 -1
- package/lib/module/tools/sliderTool.js.map +0 -1
- package/lib/module/tools/tapTool.js.map +0 -1
- package/lib/module/tools/typeTool.js.map +0 -1
- package/lib/module/tools/types.js.map +0 -1
- package/lib/module/types/jsx.d.js.map +0 -1
- package/lib/module/utils/audioUtils.js.map +0 -1
- package/lib/module/utils/logger.js.map +0 -1
- package/lib/typescript/babel.config.d.ts.map +0 -1
- package/lib/typescript/bin/generate-map.d.cts.map +0 -1
- package/lib/typescript/eslint.config.d.mts.map +0 -1
- package/lib/typescript/generate-map.d.ts.map +0 -1
- package/lib/typescript/src/__cli_tmp__.d.ts.map +0 -1
- package/lib/typescript/src/components/AIAgent.d.ts.map +0 -1
- package/lib/typescript/src/components/AIZone.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentChatBar.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentErrorBoundary.d.ts.map +0 -1
- package/lib/typescript/src/components/AgentOverlay.d.ts.map +0 -1
- package/lib/typescript/src/components/DiscoveryTooltip.d.ts.map +0 -1
- package/lib/typescript/src/components/HighlightOverlay.d.ts.map +0 -1
- package/lib/typescript/src/components/Icons.d.ts.map +0 -1
- package/lib/typescript/src/components/ProactiveHint.d.ts.map +0 -1
- package/lib/typescript/src/components/cards/InfoCard.d.ts.map +0 -1
- package/lib/typescript/src/components/cards/ReviewSummary.d.ts.map +0 -1
- package/lib/typescript/src/config/endpoints.d.ts.map +0 -1
- package/lib/typescript/src/core/ActionRegistry.d.ts.map +0 -1
- package/lib/typescript/src/core/AgentRuntime.d.ts.map +0 -1
- package/lib/typescript/src/core/FiberTreeWalker.d.ts.map +0 -1
- package/lib/typescript/src/core/IdleDetector.d.ts.map +0 -1
- package/lib/typescript/src/core/MCPBridge.d.ts.map +0 -1
- package/lib/typescript/src/core/ScreenDehydrator.d.ts.map +0 -1
- package/lib/typescript/src/core/ZoneRegistry.d.ts.map +0 -1
- package/lib/typescript/src/core/systemPrompt.d.ts.map +0 -1
- package/lib/typescript/src/core/types.d.ts.map +0 -1
- package/lib/typescript/src/hooks/useAction.d.ts.map +0 -1
- package/lib/typescript/src/index.d.ts.map +0 -1
- package/lib/typescript/src/plugin/withAppIntents.d.ts.map +0 -1
- package/lib/typescript/src/providers/GeminiProvider.d.ts.map +0 -1
- package/lib/typescript/src/providers/OpenAIProvider.d.ts.map +0 -1
- package/lib/typescript/src/providers/ProviderFactory.d.ts.map +0 -1
- package/lib/typescript/src/services/AudioInputService.d.ts.map +0 -1
- package/lib/typescript/src/services/AudioOutputService.d.ts.map +0 -1
- package/lib/typescript/src/services/KnowledgeBaseService.d.ts.map +0 -1
- package/lib/typescript/src/services/VoiceService.d.ts.map +0 -1
- package/lib/typescript/src/services/flags/FlagService.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/MobileAI.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/PiiScrubber.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/TelemetryService.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/TouchAutoCapture.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/device.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/deviceMetadata.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/index.d.ts.map +0 -1
- package/lib/typescript/src/services/telemetry/types.d.ts.map +0 -1
- package/lib/typescript/src/support/CSATSurvey.d.ts.map +0 -1
- package/lib/typescript/src/support/EscalationEventSource.d.ts.map +0 -1
- package/lib/typescript/src/support/EscalationSocket.d.ts.map +0 -1
- package/lib/typescript/src/support/SupportChatModal.d.ts.map +0 -1
- package/lib/typescript/src/support/SupportGreeting.d.ts.map +0 -1
- package/lib/typescript/src/support/TicketStore.d.ts.map +0 -1
- package/lib/typescript/src/support/escalateTool.d.ts.map +0 -1
- package/lib/typescript/src/support/index.d.ts.map +0 -1
- package/lib/typescript/src/support/supportPrompt.d.ts.map +0 -1
- package/lib/typescript/src/support/types.d.ts.map +0 -1
- package/lib/typescript/src/tools/datePickerTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/guideTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/index.d.ts.map +0 -1
- package/lib/typescript/src/tools/keyboardTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/longPressTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/pickerTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/restoreTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/scrollTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/simplifyTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/sliderTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/tapTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/typeTool.d.ts.map +0 -1
- package/lib/typescript/src/tools/types.d.ts.map +0 -1
- package/lib/typescript/src/utils/audioUtils.d.ts.map +0 -1
- package/lib/typescript/src/utils/logger.d.ts.map +0 -1
- package/src/__cli_tmp__.tsx +0 -9
- package/src/cli/analyzers/chain-analyzer.ts +0 -183
- package/src/cli/extractors/ai-extractor.ts +0 -6
- package/src/cli/extractors/ast-extractor.ts +0 -551
- package/src/cli/generate-intents.ts +0 -140
- package/src/cli/generate-map.ts +0 -121
- package/src/cli/generate-swift.ts +0 -116
- package/src/cli/scanners/expo-scanner.ts +0 -203
- package/src/cli/scanners/rn-scanner.ts +0 -445
- package/src/components/AIAgent.tsx +0 -1716
- package/src/components/AIZone.tsx +0 -147
- package/src/components/AgentChatBar.tsx +0 -1143
- package/src/components/AgentErrorBoundary.tsx +0 -78
- package/src/components/AgentOverlay.tsx +0 -73
- package/src/components/DiscoveryTooltip.tsx +0 -148
- package/src/components/HighlightOverlay.tsx +0 -136
- package/src/components/Icons.tsx +0 -253
- package/src/components/ProactiveHint.tsx +0 -145
- package/src/components/cards/InfoCard.tsx +0 -58
- package/src/components/cards/ReviewSummary.tsx +0 -76
- package/src/config/endpoints.ts +0 -22
- package/src/core/ActionRegistry.ts +0 -105
- package/src/core/AgentRuntime.ts +0 -1471
- package/src/core/FiberTreeWalker.ts +0 -930
- package/src/core/IdleDetector.ts +0 -72
- package/src/core/MCPBridge.ts +0 -163
- package/src/core/ScreenDehydrator.ts +0 -53
- package/src/core/ZoneRegistry.ts +0 -44
- package/src/core/systemPrompt.ts +0 -431
- package/src/core/types.ts +0 -521
- package/src/hooks/useAction.ts +0 -182
- package/src/index.ts +0 -83
- package/src/plugin/withAppIntents.ts +0 -98
- package/src/providers/GeminiProvider.ts +0 -357
- package/src/providers/OpenAIProvider.ts +0 -379
- package/src/providers/ProviderFactory.ts +0 -36
- package/src/services/AudioInputService.ts +0 -226
- package/src/services/AudioOutputService.ts +0 -236
- package/src/services/KnowledgeBaseService.ts +0 -156
- package/src/services/VoiceService.ts +0 -451
- package/src/services/flags/FlagService.ts +0 -137
- package/src/services/telemetry/MobileAI.ts +0 -66
- package/src/services/telemetry/PiiScrubber.ts +0 -17
- package/src/services/telemetry/TelemetryService.ts +0 -323
- package/src/services/telemetry/TouchAutoCapture.ts +0 -165
- package/src/services/telemetry/device.ts +0 -93
- package/src/services/telemetry/deviceMetadata.ts +0 -13
- package/src/services/telemetry/index.ts +0 -13
- package/src/services/telemetry/types.ts +0 -75
- package/src/support/CSATSurvey.tsx +0 -304
- package/src/support/EscalationEventSource.ts +0 -190
- package/src/support/EscalationSocket.ts +0 -152
- package/src/support/SupportChatModal.tsx +0 -563
- package/src/support/SupportGreeting.tsx +0 -161
- package/src/support/TicketStore.ts +0 -100
- package/src/support/escalateTool.ts +0 -174
- package/src/support/index.ts +0 -29
- package/src/support/supportPrompt.ts +0 -55
- package/src/support/types.ts +0 -155
- package/src/tools/datePickerTool.ts +0 -60
- package/src/tools/guideTool.ts +0 -76
- package/src/tools/index.ts +0 -20
- package/src/tools/keyboardTool.ts +0 -30
- package/src/tools/longPressTool.ts +0 -61
- package/src/tools/pickerTool.ts +0 -115
- package/src/tools/restoreTool.ts +0 -33
- package/src/tools/scrollTool.ts +0 -156
- package/src/tools/simplifyTool.ts +0 -33
- package/src/tools/sliderTool.ts +0 -65
- package/src/tools/tapTool.ts +0 -93
- package/src/tools/typeTool.ts +0 -113
- package/src/tools/types.ts +0 -58
- package/src/types/jsx.d.ts +0 -20
- package/src/utils/audioUtils.ts +0 -54
- package/src/utils/logger.ts +0 -38
|
@@ -1,379 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenAIProvider — OpenAI Chat Completions API via raw fetch.
|
|
3
|
-
*
|
|
4
|
-
* Uses the same flat `agent_step` function pattern as GeminiProvider:
|
|
5
|
-
* - Reasoning fields (previous_goal_eval, memory, plan) + action in one tool call
|
|
6
|
-
* - `tool_choice: "required"` forces a tool call every step
|
|
7
|
-
* - `strict: true` guarantees schema adherence
|
|
8
|
-
*
|
|
9
|
-
* No SDK dependency — raw fetch for full React Native compatibility.
|
|
10
|
-
* Implements the AIProvider interface so it can be swapped with GeminiProvider.
|
|
11
|
-
*/
|
|
12
|
-
|
|
13
|
-
import { logger } from '../utils/logger';
|
|
14
|
-
import type {
|
|
15
|
-
AIProvider,
|
|
16
|
-
ToolDefinition,
|
|
17
|
-
AgentStep,
|
|
18
|
-
ProviderResult,
|
|
19
|
-
AgentReasoning,
|
|
20
|
-
TokenUsage,
|
|
21
|
-
} from '../core/types';
|
|
22
|
-
|
|
23
|
-
// ─── Constants ─────────────────────────────────────────────────
|
|
24
|
-
|
|
25
|
-
const AGENT_STEP_FN = 'agent_step';
|
|
26
|
-
const OPENAI_API_URL = 'https://api.openai.com/v1/chat/completions';
|
|
27
|
-
const REASONING_FIELDS = ['previous_goal_eval', 'memory', 'plan'] as const;
|
|
28
|
-
|
|
29
|
-
// ─── Provider ──────────────────────────────────────────────────
|
|
30
|
-
|
|
31
|
-
export class OpenAIProvider implements AIProvider {
|
|
32
|
-
private model: string;
|
|
33
|
-
private baseUrl: string;
|
|
34
|
-
private headers: Record<string, string>;
|
|
35
|
-
|
|
36
|
-
constructor(
|
|
37
|
-
apiKey?: string,
|
|
38
|
-
model: string = 'gpt-4.1-mini',
|
|
39
|
-
proxyUrl?: string,
|
|
40
|
-
proxyHeaders?: Record<string, string>,
|
|
41
|
-
) {
|
|
42
|
-
if (proxyUrl) {
|
|
43
|
-
this.baseUrl = proxyUrl.endsWith('/')
|
|
44
|
-
? `${proxyUrl}v1/chat/completions`
|
|
45
|
-
: proxyUrl;
|
|
46
|
-
this.headers = {
|
|
47
|
-
'Content-Type': 'application/json',
|
|
48
|
-
...(proxyHeaders || {}),
|
|
49
|
-
};
|
|
50
|
-
} else if (apiKey) {
|
|
51
|
-
this.baseUrl = OPENAI_API_URL;
|
|
52
|
-
this.headers = {
|
|
53
|
-
'Content-Type': 'application/json',
|
|
54
|
-
Authorization: `Bearer ${apiKey}`,
|
|
55
|
-
};
|
|
56
|
-
} else {
|
|
57
|
-
throw new Error(
|
|
58
|
-
'[mobileai] You must provide either "apiKey" or "proxyUrl" to use OpenAI provider.',
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
this.model = model;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
async generateContent(
|
|
66
|
-
systemPrompt: string,
|
|
67
|
-
userMessage: string,
|
|
68
|
-
tools: ToolDefinition[],
|
|
69
|
-
_history: AgentStep[],
|
|
70
|
-
screenshot?: string,
|
|
71
|
-
): Promise<ProviderResult> {
|
|
72
|
-
logger.info(
|
|
73
|
-
'OpenAIProvider',
|
|
74
|
-
`Sending request. Model: ${this.model}, Tools: ${tools.length}${screenshot ? ', with screenshot' : ''}`,
|
|
75
|
-
);
|
|
76
|
-
|
|
77
|
-
const agentStepTool = this.buildAgentStepTool(tools);
|
|
78
|
-
const messages = this.buildMessages(systemPrompt, userMessage, screenshot);
|
|
79
|
-
|
|
80
|
-
const startTime = Date.now();
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
const response = await fetch(this.baseUrl, {
|
|
84
|
-
method: 'POST',
|
|
85
|
-
headers: this.headers,
|
|
86
|
-
body: JSON.stringify({
|
|
87
|
-
model: this.model,
|
|
88
|
-
messages,
|
|
89
|
-
tools: [agentStepTool],
|
|
90
|
-
tool_choice: 'required',
|
|
91
|
-
temperature: 0.2,
|
|
92
|
-
max_tokens: 2048,
|
|
93
|
-
}),
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
if (!response.ok) {
|
|
97
|
-
const errorBody = await response.text();
|
|
98
|
-
throw new Error(`OpenAI API error ${response.status}: ${errorBody}`);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const data = await response.json();
|
|
102
|
-
const elapsed = Date.now() - startTime;
|
|
103
|
-
logger.info('OpenAIProvider', `Response received in ${elapsed}ms`);
|
|
104
|
-
|
|
105
|
-
const tokenUsage = this.extractTokenUsage(data);
|
|
106
|
-
if (tokenUsage) {
|
|
107
|
-
logger.info(
|
|
108
|
-
'OpenAIProvider',
|
|
109
|
-
`Tokens: ${tokenUsage.promptTokens} in / ${tokenUsage.completionTokens} out / $${tokenUsage.estimatedCostUSD.toFixed(6)}`,
|
|
110
|
-
);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const result = this.parseAgentStepResponse(data, tools);
|
|
114
|
-
result.tokenUsage = tokenUsage;
|
|
115
|
-
return result;
|
|
116
|
-
} catch (error: any) {
|
|
117
|
-
logger.error('OpenAIProvider', 'Request failed:', error.message);
|
|
118
|
-
throw error;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─── Build agent_step Tool ──────────────────────────────────
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Builds the OpenAI tool definition for `agent_step`.
|
|
126
|
-
* Same flat pattern as Gemini — reasoning fields + action in one function.
|
|
127
|
-
* Uses `strict: true` for guaranteed schema adherence.
|
|
128
|
-
*/
|
|
129
|
-
private buildAgentStepTool(tools: ToolDefinition[]): any {
|
|
130
|
-
const toolNames = tools.map((t) => t.name);
|
|
131
|
-
|
|
132
|
-
// Collect all unique parameter fields across all tools
|
|
133
|
-
const actionProperties: Record<string, any> = {};
|
|
134
|
-
for (const tool of tools) {
|
|
135
|
-
for (const [paramName, param] of Object.entries(tool.parameters)) {
|
|
136
|
-
if (actionProperties[paramName]) continue;
|
|
137
|
-
actionProperties[paramName] = {
|
|
138
|
-
type: param.type,
|
|
139
|
-
description: param.description,
|
|
140
|
-
...(param.enum ? { enum: param.enum } : {}),
|
|
141
|
-
};
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Build tool descriptions for enum
|
|
146
|
-
const toolDescriptions = tools
|
|
147
|
-
.map((t) => {
|
|
148
|
-
const params = Object.keys(t.parameters).join(', ');
|
|
149
|
-
return `- ${t.name}(${params}): ${t.description}`;
|
|
150
|
-
})
|
|
151
|
-
.join('\n');
|
|
152
|
-
|
|
153
|
-
// OpenAI strict mode requires additionalProperties: false
|
|
154
|
-
// and ALL properties in `required` array
|
|
155
|
-
const allProperties: Record<string, any> = {
|
|
156
|
-
previous_goal_eval: {
|
|
157
|
-
type: 'string',
|
|
158
|
-
description:
|
|
159
|
-
'One-sentence assessment of your last action. State success, failure, or uncertain. Skip on first step.',
|
|
160
|
-
},
|
|
161
|
-
memory: {
|
|
162
|
-
type: 'string',
|
|
163
|
-
description:
|
|
164
|
-
'Key facts to remember for future steps: progress made, items found, counters, field values already collected.',
|
|
165
|
-
},
|
|
166
|
-
plan: {
|
|
167
|
-
type: 'string',
|
|
168
|
-
description:
|
|
169
|
-
'Your immediate next goal — what action you will take and why.',
|
|
170
|
-
},
|
|
171
|
-
action_name: {
|
|
172
|
-
type: 'string',
|
|
173
|
-
description: 'Which action to execute.',
|
|
174
|
-
enum: toolNames,
|
|
175
|
-
},
|
|
176
|
-
...actionProperties,
|
|
177
|
-
};
|
|
178
|
-
|
|
179
|
-
return {
|
|
180
|
-
type: 'function',
|
|
181
|
-
function: {
|
|
182
|
-
name: AGENT_STEP_FN,
|
|
183
|
-
description: `Execute one agent step. Choose an action and provide reasoning.\n\nAvailable actions:\n${toolDescriptions}`,
|
|
184
|
-
parameters: {
|
|
185
|
-
type: 'object',
|
|
186
|
-
properties: allProperties,
|
|
187
|
-
required: Object.keys(allProperties),
|
|
188
|
-
additionalProperties: false,
|
|
189
|
-
},
|
|
190
|
-
strict: true,
|
|
191
|
-
},
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ─── Build Messages ────────────────────────────────────────
|
|
196
|
-
|
|
197
|
-
private buildMessages(
|
|
198
|
-
systemPrompt: string,
|
|
199
|
-
userMessage: string,
|
|
200
|
-
screenshot?: string,
|
|
201
|
-
): any[] {
|
|
202
|
-
const messages: any[] = [
|
|
203
|
-
{ role: 'system', content: systemPrompt },
|
|
204
|
-
];
|
|
205
|
-
|
|
206
|
-
// User message — text or multimodal with screenshot
|
|
207
|
-
if (screenshot) {
|
|
208
|
-
messages.push({
|
|
209
|
-
role: 'user',
|
|
210
|
-
content: [
|
|
211
|
-
{ type: 'text', text: userMessage },
|
|
212
|
-
{
|
|
213
|
-
type: 'image_url',
|
|
214
|
-
image_url: {
|
|
215
|
-
url: `data:image/jpeg;base64,${screenshot}`,
|
|
216
|
-
detail: 'low',
|
|
217
|
-
},
|
|
218
|
-
},
|
|
219
|
-
],
|
|
220
|
-
});
|
|
221
|
-
} else {
|
|
222
|
-
messages.push({ role: 'user', content: userMessage });
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return messages;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// ─── Parse Response ────────────────────────────────────────
|
|
229
|
-
|
|
230
|
-
private parseAgentStepResponse(
|
|
231
|
-
data: any,
|
|
232
|
-
tools: ToolDefinition[],
|
|
233
|
-
): ProviderResult {
|
|
234
|
-
const choice = data.choices?.[0];
|
|
235
|
-
|
|
236
|
-
if (!choice) {
|
|
237
|
-
logger.warn('OpenAIProvider', 'No choices in response');
|
|
238
|
-
return {
|
|
239
|
-
toolCalls: [
|
|
240
|
-
{
|
|
241
|
-
name: 'done',
|
|
242
|
-
args: { text: 'No response generated.', success: false },
|
|
243
|
-
},
|
|
244
|
-
],
|
|
245
|
-
reasoning: { previousGoalEval: '', memory: '', plan: '' },
|
|
246
|
-
text: 'No response generated.',
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
const message = choice.message;
|
|
251
|
-
const toolCall = message?.tool_calls?.[0];
|
|
252
|
-
|
|
253
|
-
if (!toolCall?.function) {
|
|
254
|
-
logger.warn(
|
|
255
|
-
'OpenAIProvider',
|
|
256
|
-
'No tool call in response. Text:',
|
|
257
|
-
message?.content,
|
|
258
|
-
);
|
|
259
|
-
return {
|
|
260
|
-
toolCalls: [
|
|
261
|
-
{
|
|
262
|
-
name: 'done',
|
|
263
|
-
args: {
|
|
264
|
-
text: message?.content || 'No action taken.',
|
|
265
|
-
success: false,
|
|
266
|
-
},
|
|
267
|
-
},
|
|
268
|
-
],
|
|
269
|
-
reasoning: { previousGoalEval: '', memory: '', plan: '' },
|
|
270
|
-
text: message?.content,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// OpenAI returns arguments as a JSON STRING — must parse
|
|
275
|
-
let args: Record<string, any>;
|
|
276
|
-
try {
|
|
277
|
-
args = JSON.parse(toolCall.function.arguments);
|
|
278
|
-
} catch (err) {
|
|
279
|
-
logger.error(
|
|
280
|
-
'OpenAIProvider',
|
|
281
|
-
'Failed to parse tool arguments:',
|
|
282
|
-
toolCall.function.arguments,
|
|
283
|
-
);
|
|
284
|
-
return {
|
|
285
|
-
toolCalls: [
|
|
286
|
-
{
|
|
287
|
-
name: 'done',
|
|
288
|
-
args: { text: 'Failed to parse AI response.', success: false },
|
|
289
|
-
},
|
|
290
|
-
],
|
|
291
|
-
reasoning: { previousGoalEval: '', memory: '', plan: '' },
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Extract reasoning fields
|
|
296
|
-
const reasoning: AgentReasoning = {
|
|
297
|
-
previousGoalEval: args.previous_goal_eval || '',
|
|
298
|
-
memory: args.memory || '',
|
|
299
|
-
plan: args.plan || '',
|
|
300
|
-
};
|
|
301
|
-
|
|
302
|
-
// Extract action
|
|
303
|
-
const actionName = args.action_name;
|
|
304
|
-
if (!actionName) {
|
|
305
|
-
logger.warn(
|
|
306
|
-
'OpenAIProvider',
|
|
307
|
-
'No action_name in agent_step. Falling back to done.',
|
|
308
|
-
);
|
|
309
|
-
return {
|
|
310
|
-
toolCalls: [
|
|
311
|
-
{
|
|
312
|
-
name: 'done',
|
|
313
|
-
args: { text: 'Agent did not choose an action.', success: false },
|
|
314
|
-
},
|
|
315
|
-
],
|
|
316
|
-
reasoning,
|
|
317
|
-
text: message?.content,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
// Build action args: extract only the params that belong to the matched tool
|
|
322
|
-
const actionArgs: Record<string, any> = {};
|
|
323
|
-
const reservedKeys = new Set([...REASONING_FIELDS, 'action_name']);
|
|
324
|
-
|
|
325
|
-
const matchedTool = tools.find((t) => t.name === actionName);
|
|
326
|
-
if (matchedTool) {
|
|
327
|
-
for (const paramName of Object.keys(matchedTool.parameters)) {
|
|
328
|
-
if (args[paramName] !== undefined) {
|
|
329
|
-
actionArgs[paramName] = args[paramName];
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
} else {
|
|
333
|
-
for (const [key, value] of Object.entries(args)) {
|
|
334
|
-
if (!reservedKeys.has(key)) {
|
|
335
|
-
actionArgs[key] = value;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
logger.info(
|
|
341
|
-
'OpenAIProvider',
|
|
342
|
-
`Parsed: action=${actionName}, plan="${reasoning.plan}"`,
|
|
343
|
-
);
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
toolCalls: [{ name: actionName, args: actionArgs }],
|
|
347
|
-
reasoning,
|
|
348
|
-
text: message?.content,
|
|
349
|
-
};
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
// ─── Token Usage Extraction ─────────────────────────────────
|
|
353
|
-
|
|
354
|
-
/**
|
|
355
|
-
* Extracts token usage from OpenAI response and calculates estimated cost.
|
|
356
|
-
*
|
|
357
|
-
* Pricing (GPT-4.1-mini):
|
|
358
|
-
* - Input: $0.40 / 1M tokens
|
|
359
|
-
* - Output: $1.60 / 1M tokens
|
|
360
|
-
*/
|
|
361
|
-
private extractTokenUsage(data: any): TokenUsage | undefined {
|
|
362
|
-
const usage = data?.usage;
|
|
363
|
-
if (!usage) return undefined;
|
|
364
|
-
|
|
365
|
-
const promptTokens = usage.prompt_tokens ?? 0;
|
|
366
|
-
const completionTokens = usage.completion_tokens ?? 0;
|
|
367
|
-
const totalTokens = usage.total_tokens ?? promptTokens + completionTokens;
|
|
368
|
-
|
|
369
|
-
// Cost estimation based on GPT-4.1-mini pricing
|
|
370
|
-
const INPUT_COST_PER_M = 0.4;
|
|
371
|
-
const OUTPUT_COST_PER_M = 1.6;
|
|
372
|
-
|
|
373
|
-
const estimatedCostUSD =
|
|
374
|
-
(promptTokens / 1_000_000) * INPUT_COST_PER_M +
|
|
375
|
-
(completionTokens / 1_000_000) * OUTPUT_COST_PER_M;
|
|
376
|
-
|
|
377
|
-
return { promptTokens, completionTokens, totalTokens, estimatedCostUSD };
|
|
378
|
-
}
|
|
379
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ProviderFactory — Creates the appropriate AI provider based on config.
|
|
3
|
-
*
|
|
4
|
-
* Centralizes provider instantiation so AIAgent.tsx doesn't need to
|
|
5
|
-
* know about individual provider implementations.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import type { AIProvider, AIProviderName } from '../core/types';
|
|
9
|
-
import { GeminiProvider } from './GeminiProvider';
|
|
10
|
-
import { OpenAIProvider } from './OpenAIProvider';
|
|
11
|
-
|
|
12
|
-
export function createProvider(
|
|
13
|
-
provider: AIProviderName = 'gemini',
|
|
14
|
-
apiKey?: string,
|
|
15
|
-
model?: string,
|
|
16
|
-
proxyUrl?: string,
|
|
17
|
-
proxyHeaders?: Record<string, string>,
|
|
18
|
-
): AIProvider {
|
|
19
|
-
switch (provider) {
|
|
20
|
-
case 'openai':
|
|
21
|
-
return new OpenAIProvider(
|
|
22
|
-
apiKey,
|
|
23
|
-
model || 'gpt-4.1-mini',
|
|
24
|
-
proxyUrl,
|
|
25
|
-
proxyHeaders,
|
|
26
|
-
);
|
|
27
|
-
case 'gemini':
|
|
28
|
-
default:
|
|
29
|
-
return new GeminiProvider(
|
|
30
|
-
apiKey,
|
|
31
|
-
model || 'gemini-2.5-flash',
|
|
32
|
-
proxyUrl,
|
|
33
|
-
proxyHeaders,
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,226 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AudioInputService — Real-time microphone capture for voice mode.
|
|
3
|
-
*
|
|
4
|
-
* Uses react-native-audio-api (Software Mansion) AudioRecorder for native
|
|
5
|
-
* PCM streaming from the microphone. Each chunk is converted from Float32
|
|
6
|
-
* to Int16 PCM and base64-encoded for the Gemini Live API.
|
|
7
|
-
*
|
|
8
|
-
* Echo cancellation is handled at the OS/hardware level via
|
|
9
|
-
* react-native-incall-manager (VOICE_COMMUNICATION mode) — not in JS.
|
|
10
|
-
*
|
|
11
|
-
* Requires: react-native-audio-api (development build only, not Expo Go)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { logger } from '../utils/logger';
|
|
15
|
-
import { float32ToInt16Base64 } from '../utils/audioUtils';
|
|
16
|
-
|
|
17
|
-
// ─── Types ─────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
export interface AudioInputConfig {
|
|
20
|
-
sampleRate?: number;
|
|
21
|
-
/** Number of samples per callback buffer (default: 4096) */
|
|
22
|
-
bufferLength?: number;
|
|
23
|
-
/** Callback with base64 PCM audio chunk */
|
|
24
|
-
onAudioChunk: (base64Audio: string) => void;
|
|
25
|
-
onError?: (error: string) => void;
|
|
26
|
-
onPermissionDenied?: () => void;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
type RecordingStatus = 'idle' | 'recording' | 'paused';
|
|
30
|
-
|
|
31
|
-
// ─── Service ───────────────────────────────────────────────────
|
|
32
|
-
|
|
33
|
-
export class AudioInputService {
|
|
34
|
-
private config: AudioInputConfig;
|
|
35
|
-
private status: RecordingStatus = 'idle';
|
|
36
|
-
private recorder: any = null;
|
|
37
|
-
|
|
38
|
-
// Auto-recovery: detect when mic session dies after audio playback.
|
|
39
|
-
// This is a react-native-audio-api bug where AudioRecorder loses mic access
|
|
40
|
-
// after AudioBufferQueueSourceNode plays audio (audio session conflict).
|
|
41
|
-
private consecutiveSilentFrames = 0;
|
|
42
|
-
private isRecovering = false;
|
|
43
|
-
private static readonly SILENT_THRESHOLD = 0.01;
|
|
44
|
-
private static readonly SILENT_FRAMES_BEFORE_RESTART = 15;
|
|
45
|
-
|
|
46
|
-
constructor(config: AudioInputConfig) {
|
|
47
|
-
this.config = config;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ─── Lifecycle ─────────────────────────────────────────────
|
|
51
|
-
|
|
52
|
-
async start(): Promise<boolean> {
|
|
53
|
-
try {
|
|
54
|
-
// Lazy-load react-native-audio-api (optional peer dependency)
|
|
55
|
-
let audioApi: any;
|
|
56
|
-
try {
|
|
57
|
-
const { NativeModules } = require('react-native');
|
|
58
|
-
if (!NativeModules.AudioApiModule) {
|
|
59
|
-
const msg =
|
|
60
|
-
'[mobileai] react-native-audio-api native module not found. '
|
|
61
|
-
+ 'Voice mode requires a development build (not Expo Go).';
|
|
62
|
-
logger.warn('AudioInput', msg);
|
|
63
|
-
this.config.onError?.(msg);
|
|
64
|
-
return false;
|
|
65
|
-
}
|
|
66
|
-
// Static require — Metro needs a literal string for bundling.
|
|
67
|
-
audioApi = require('react-native-audio-api');
|
|
68
|
-
} catch {
|
|
69
|
-
const msg =
|
|
70
|
-
'Voice mode requires react-native-audio-api. Install with: npm install react-native-audio-api';
|
|
71
|
-
logger.error('AudioInput', msg);
|
|
72
|
-
this.config.onError?.(msg);
|
|
73
|
-
return false;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
// Request mic permission (Android)
|
|
77
|
-
try {
|
|
78
|
-
const { PermissionsAndroid, Platform } = require('react-native');
|
|
79
|
-
if (Platform.OS === 'android') {
|
|
80
|
-
const result = await PermissionsAndroid.request(
|
|
81
|
-
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO
|
|
82
|
-
);
|
|
83
|
-
if (result !== PermissionsAndroid.RESULTS.GRANTED) {
|
|
84
|
-
logger.warn('AudioInput', 'Microphone permission denied');
|
|
85
|
-
this.config.onPermissionDenied?.();
|
|
86
|
-
return false;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
} catch {
|
|
90
|
-
// Permission check failed — continue and let native layer handle it
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// Create AudioRecorder
|
|
94
|
-
this.recorder = new audioApi.AudioRecorder();
|
|
95
|
-
this.consecutiveSilentFrames = 0;
|
|
96
|
-
|
|
97
|
-
const sampleRate = this.config.sampleRate || 16000;
|
|
98
|
-
const bufferLength = this.config.bufferLength || 4096;
|
|
99
|
-
|
|
100
|
-
// Register audio data callback
|
|
101
|
-
let frameCount = 0;
|
|
102
|
-
this.recorder.onAudioReady(
|
|
103
|
-
{ sampleRate, bufferLength, channelCount: 1 },
|
|
104
|
-
(event: any) => {
|
|
105
|
-
frameCount++;
|
|
106
|
-
try {
|
|
107
|
-
// event.buffer is an AudioBuffer — get Float32 channel data
|
|
108
|
-
const float32Data = event.buffer.getChannelData(0);
|
|
109
|
-
|
|
110
|
-
// Measure peak amplitude for diagnostics + silent detection
|
|
111
|
-
let maxAmp = 0;
|
|
112
|
-
for (let i = 0; i < float32Data.length; i++) {
|
|
113
|
-
const abs = Math.abs(float32Data[i] || 0);
|
|
114
|
-
if (abs > maxAmp) maxAmp = abs;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
// Diagnostic: log amplitude on first 5 frames, then every 10th
|
|
118
|
-
if (frameCount <= 5 || frameCount % 10 === 0) {
|
|
119
|
-
logger.info('AudioInput', `🔬 Frame #${frameCount}: maxAmp=${maxAmp.toFixed(6)}, samples=${float32Data.length}`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// ─── Auto-Recovery: Silent mic detection ─────────────
|
|
123
|
-
// After audio playback, react-native-audio-api's AudioRecorder
|
|
124
|
-
// can lose its mic session (all-zero frames). Detect this and
|
|
125
|
-
// restart the recorder to re-acquire the audio session.
|
|
126
|
-
if (maxAmp < AudioInputService.SILENT_THRESHOLD) {
|
|
127
|
-
this.consecutiveSilentFrames++;
|
|
128
|
-
if (
|
|
129
|
-
this.consecutiveSilentFrames >= AudioInputService.SILENT_FRAMES_BEFORE_RESTART &&
|
|
130
|
-
!this.isRecovering
|
|
131
|
-
) {
|
|
132
|
-
this.isRecovering = true;
|
|
133
|
-
logger.warn('AudioInput', `⚠️ ${this.consecutiveSilentFrames} silent frames — restarting recorder...`);
|
|
134
|
-
this.restartRecorder().then(() => {
|
|
135
|
-
this.isRecovering = false;
|
|
136
|
-
this.consecutiveSilentFrames = 0;
|
|
137
|
-
logger.info('AudioInput', '✅ Recorder restarted — mic session re-acquired');
|
|
138
|
-
}).catch((err: any) => {
|
|
139
|
-
this.isRecovering = false;
|
|
140
|
-
logger.error('AudioInput', `❌ Recorder restart failed: ${err?.message || err}`);
|
|
141
|
-
});
|
|
142
|
-
return; // Skip this frame
|
|
143
|
-
}
|
|
144
|
-
} else {
|
|
145
|
-
// Got real audio — reset counter
|
|
146
|
-
if (this.consecutiveSilentFrames > 5) {
|
|
147
|
-
logger.info('AudioInput', `🎤 Mic recovered after ${this.consecutiveSilentFrames} silent frames`);
|
|
148
|
-
}
|
|
149
|
-
this.consecutiveSilentFrames = 0;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const base64Chunk = float32ToInt16Base64(float32Data);
|
|
153
|
-
if (frameCount <= 5 || frameCount % 10 === 0) {
|
|
154
|
-
logger.info('AudioInput', `🎤 Frame #${frameCount}: chunk=${base64Chunk.length} chars, calling onAudioChunk...`);
|
|
155
|
-
}
|
|
156
|
-
this.config.onAudioChunk(base64Chunk);
|
|
157
|
-
} catch (err: any) {
|
|
158
|
-
logger.error('AudioInput', `Frame processing error: ${err.message}`);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
);
|
|
162
|
-
|
|
163
|
-
// Register error callback
|
|
164
|
-
this.recorder.onError((error: any) => {
|
|
165
|
-
logger.error('AudioInput', `Recorder error: ${error.message || error}`);
|
|
166
|
-
this.config.onError?.(error.message || String(error));
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Start recording
|
|
170
|
-
this.recorder.start();
|
|
171
|
-
this.status = 'recording';
|
|
172
|
-
logger.info('AudioInput', `Streaming started (${sampleRate}Hz, bufLen=${bufferLength})`);
|
|
173
|
-
return true;
|
|
174
|
-
} catch (error: any) {
|
|
175
|
-
logger.error('AudioInput', `Failed to start: ${error.message}`);
|
|
176
|
-
this.config.onError?.(error.message);
|
|
177
|
-
return false;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async stop(): Promise<void> {
|
|
182
|
-
try {
|
|
183
|
-
if (this.recorder && this.status !== 'idle') {
|
|
184
|
-
this.recorder.clearOnAudioReady();
|
|
185
|
-
this.recorder.clearOnError();
|
|
186
|
-
this.recorder.stop();
|
|
187
|
-
}
|
|
188
|
-
this.recorder = null;
|
|
189
|
-
this.status = 'idle';
|
|
190
|
-
this.consecutiveSilentFrames = 0;
|
|
191
|
-
logger.info('AudioInput', 'Streaming stopped');
|
|
192
|
-
} catch (error: any) {
|
|
193
|
-
logger.error('AudioInput', `Failed to stop: ${error.message}`);
|
|
194
|
-
this.recorder = null;
|
|
195
|
-
this.status = 'idle';
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
// ─── Auto-Recovery ─────────────────────────────────────────
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Restart the recorder to re-acquire the audio session.
|
|
203
|
-
* Fixes react-native-audio-api bug where AudioRecorder loses mic access
|
|
204
|
-
* after AudioBufferQueueSourceNode plays audio.
|
|
205
|
-
*/
|
|
206
|
-
private async restartRecorder(): Promise<void> {
|
|
207
|
-
logger.info('AudioInput', '🔄 Restarting recorder for mic recovery...');
|
|
208
|
-
await this.stop();
|
|
209
|
-
// Brief pause to let the audio system release resources
|
|
210
|
-
await new Promise(resolve => setTimeout(resolve, 300));
|
|
211
|
-
const ok = await this.start();
|
|
212
|
-
if (!ok) {
|
|
213
|
-
throw new Error('Recorder restart failed');
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// ─── Status ───────────────────────────────────────────────
|
|
218
|
-
|
|
219
|
-
get isRecording(): boolean {
|
|
220
|
-
return this.status === 'recording';
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
get currentStatus(): RecordingStatus {
|
|
224
|
-
return this.status;
|
|
225
|
-
}
|
|
226
|
-
}
|