-
Notifications
You must be signed in to change notification settings - Fork 93
Expand file tree
/
Copy pathai-service.mdc
More file actions
186 lines (147 loc) Β· 6.01 KB
/
ai-service.mdc
File metadata and controls
186 lines (147 loc) Β· 6.01 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
---
description: Rules for working with the AI service layer (src/services/ai/ and src/hooks/). Apply when editing AI-related files.
globs: ["src/services/ai/**", "src/hooks/**", "src/screens/ai-chat/**"]
alwaysApply: false
---
# AI Service Layer
## Architecture Overview
```
src/services/ai/
βββ types.ts β All types, interfaces, and provider metadata constants
βββ AIService.ts β Public API: sendAIMessage(), streamAIMessage(), builders
βββ index.ts β Barrel β import from here, not from individual files
βββ providers/
βββ openai.ts β OpenAI Chat Completions API (SSE streaming)
βββ anthropic.ts β Anthropic Messages API (SSE streaming)
βββ gemini.ts β Google Gemini generateContent API (SSE streaming)
src/hooks/
βββ useAIChat.ts β Multi-turn conversation hook
βββ useAICompletion.tsβ Single-shot completion hook
βββ index.ts β Barrel
```
## The Golden Rule: No Hardcoded Model Names
`AIConfig.model` is a plain `string?`. The developer supplies whatever model their API key supports. The service layer never defaults to a specific model name β it passes `config.model` straight through to the HTTP request body.
```typescript
// WRONG β don't add defaults like this
const model = config.model ?? "gpt-4o-mini";
// CORRECT β pass it through as-is
body: JSON.stringify({ model: config.model, ... })
```
## AIConfig
```typescript
interface AIConfig {
provider: "openai" | "anthropic" | "gemini"; // which provider to use
apiKey: string; // caller supplies β never hardcode
model?: string; // caller supplies β no defaults in service
temperature?: number; // defaults to 0.7 inside providers
maxTokens?: number; // defaults to 1024 inside providers
systemPrompt?: string; // optional system instruction
baseURL?: string; // override for proxies or local LLMs (e.g. Ollama)
}
```
## Using the Hooks
### useAIChat β multi-turn conversation
```typescript
import { useAIChat } from "@hooks";
const { messages, isLoading, isStreaming, error, sendMessage, streamMessage, clearMessages, setSystemPrompt } =
useAIChat({
config: {
provider: "openai",
apiKey: userApiKey,
model: userModelName,
},
onError: (err) => console.error(err),
});
// Full response (await the complete reply before updating UI)
await sendMessage("Explain closures in JavaScript");
// Streaming (tokens update the last assistant message in real-time)
await streamMessage("Write a React Native component for a todo list");
// Inject/replace system instruction
setSystemPrompt("You are a concise React Native expert.");
```
### useAICompletion β single-shot
```typescript
import { useAICompletion } from "@hooks";
const { complete, result, isLoading, error, reset } = useAICompletion({
config: { provider: "anthropic", apiKey: key, model: model },
systemPrompt: "Classify the sentiment as positive, neutral, or negative.",
});
const sentiment = await complete(userReview);
```
## Calling the Service Directly (without hooks)
```typescript
import { sendAIMessage, streamAIMessage, buildUserMessage, buildSystemMessage } from "@services/ai";
const messages = [
buildSystemMessage("You are a helpful assistant."),
buildUserMessage("Hello!"),
];
// Full response
const response = await sendAIMessage(messages, config);
console.log(response.message.content);
// Streaming
await streamAIMessage(messages, config, {
onToken: (token) => process.stdout.write(token),
onComplete: (response) => saveToDatabase(response),
onError: (err) => showErrorBanner(err.message),
});
```
## Adding a New Provider
1. **Create the provider file** `src/services/ai/providers/<name>.ts`:
```typescript
import { IAIProvider, AIConfig, AIMessage, AIChatResponse, AIStreamCallbacks, AIError } from "../types";
export class MyProvider implements IAIProvider {
async sendMessage(messages: AIMessage[], config: AIConfig): Promise<AIChatResponse> {
// call the API, return AIChatResponse
}
async streamMessage(messages: AIMessage[], config: AIConfig, callbacks: AIStreamCallbacks): Promise<void> {
// call the streaming API, invoke callbacks
}
}
```
2. **Register in the factory** (`AIService.ts`):
```typescript
case "<name>":
return new MyProvider();
```
3. **Extend the type union** (`types.ts`):
```typescript
export type AIProvider = "openai" | "anthropic" | "gemini" | "<name>";
```
4. **Add metadata** (`types.ts`):
```typescript
export const AI_PROVIDER_LABELS: Record<AIProvider, string> = {
// ...existing,
"<name>": "My Provider",
};
export const AI_BASE_URLS: Record<AIProvider, string> = {
// ...existing,
"<name>": "https://api.myprovider.com/v1",
};
```
## Streaming Implementation Pattern
All three providers use the same SSE read loop. Follow this pattern:
```typescript
const reader = response.body?.getReader();
if (!reader) throw new AIError("No response body", config.provider);
const decoder = new TextDecoder();
let fullContent = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
const chunk = decoder.decode(value, { stream: true });
for (const line of chunk.split("\n").filter((l) => l.startsWith("data: "))) {
try {
const json = JSON.parse(line.slice(6));
const token = extractToken(json); // provider-specific path
if (token) { fullContent += token; callbacks.onToken?.(token); }
} catch { /* skip malformed chunks */ }
}
}
callbacks.onComplete?.({ message: { id: ..., role: "assistant", content: fullContent, timestamp: Date.now() } });
```
## What NOT to Do
- Never import from `src/services/ai/providers/*.ts` directly β use `@services/ai`
- Never hardcode model names in the service layer
- Never store API keys anywhere except component state
- Never skip the `onError` callback in streaming β unhandled rejections crash the app
- Never add `AI_MODELS` catalogs back β they go stale instantly as providers release new models