Skip to content

Commit ac6804e

Browse files
Add Grok provider support (#40)
* feat: add Grok provider * chore: remove Grok trending context
1 parent 3725ec8 commit ac6804e

File tree

15 files changed

+254
-19
lines changed

15 files changed

+254
-19
lines changed

.env.example

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ ANTHROPIC_API_KEY=your-anthropic-api-key
1717
ANTHROPIC_MODEL=claude-3-opus-20240229
1818
ANTHROPIC_TEMPERATURE=0.5
1919

20+
# Grok (leave values blank if not using)
21+
GROK_API_KEY=your-grok-api-key
22+
GROK_MODEL=grok-1
23+
GROK_TEMPERATURE=0.5
24+
2025
# Khoj (leave values blank if not using)
2126
KHOJ_API_URL=http://127.0.0.1:42110/api/chat
2227
KHOJ_AGENT=agent-name

README.md

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
### Requirements
4747

4848
- iMessages database access -- designed to run from a Mac logged into your iCloud
49-
- API key from OpenAI or Anthropic, or a local [Khoj](https://khoj.dev/) instance
49+
- API key from OpenAI, Anthropic, or Grok, or a local [Khoj](https://khoj.dev/) instance
5050

5151
### Obtaining an OpenAI API Key
5252

@@ -118,6 +118,12 @@ Update the values in the `.env` file. The following variables are needed:
118118
- `ANTHROPIC_MODEL`: claude-3-opus-20240229 or another Anthropic model
119119
- `ANTHROPIC_TEMPERATURE`: Controls the randomness of Claude's responses
120120

121+
- **Grok**
122+
123+
- `GROK_API_KEY`: Your Grok API key
124+
- `GROK_MODEL`: grok-1 or another Grok model
125+
- `GROK_TEMPERATURE`: Controls the randomness of Grok's responses
126+
121127
- **Khoj**
122128

123129
- `KHOJ_API_URL`: Your [Khoj](https://khoj.dev/) server API URL if you have one, otherwise leave this out or leave it blank
@@ -160,7 +166,7 @@ below), it will automatically use those files and start with HTTPS.
160166
161167
## How It Works
162168
163-
WellSaid connects to your macOS Messages database to fetch your conversations with a specific contact (set via the `PARTNER_PHONE` environment variable). It then uses an AI provider (any combination of OpenAI, Anthropic, and/or Khoj) to analyze the conversation and generate:
169+
WellSaid connects to your macOS Messages database to fetch your conversations with a specific contact (set via the `PARTNER_PHONE` environment variable). It then uses an AI provider (any combination of OpenAI, Anthropic, Grok, and/or Khoj) to analyze the conversation and generate:
164170
165171
1. A summary of the conversation, including emotional tone and key topics
166172
1. Three suggested replies (short, medium, and long) in your chosen tone
@@ -171,7 +177,7 @@ WellSaid connects to your macOS Messages database to fetch your conversations wi
171177
- **State Management**: Svelte's built-in $state system
172178
- **Styling**: Custom CSS with variables for theming
173179
- **Database**: SQLite (connecting to macOS Messages database)
174-
- **AI Integration**: OpenAI API (GPT-4 or other models), Anthropic Claude models, and/or local [Khoj](https://khoj.dev/) server
180+
- **AI Integration**: OpenAI API (GPT-4 or other models), Anthropic Claude models, Grok, and/or local [Khoj](https://khoj.dev/) server
175181
- **Logging**: Pino for structured logging
176182

177183
## Development and Local Usage
@@ -252,7 +258,7 @@ Now when you visit your app over HTTPS (via Safari), iOS will trust the cert, an
252258

253259
## Privacy and Security Considerations
254260

255-
- All conversation analysis happens through the selected AI provider (OpenAI, Anthropic, or Khoj), so your data is subject to that provider's privacy policy.
261+
- All conversation analysis happens through the selected AI provider (OpenAI, Anthropic, Grok, or Khoj), so your data is subject to that provider's privacy policy.
256262
257263
## Troubleshooting
258264
@@ -268,6 +274,7 @@ Now when you visit your app over HTTPS (via Safari), iOS will trust the cert, an
268274
- [Svelte](https://svelte.dev/) - The web framework used
269275
- [OpenAI](https://openai.com/) - AI model provider
270276
- [Anthropic](https://www.anthropic.com/) - Claude model provider
277+
- [Grok](https://x.ai/) - Additional AI provider option
271278
- [Khoj](https://khoj.dev/) - Alternative local AI model provider and search
272279
- [SQLite](https://sqlite.org/) - Database engine
273280
- [Tailscale](https://tailscale.com/) - For making secure remote access easy

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "wellsaid",
33
"private": true,
4-
"version": "1.2.0",
4+
"version": "1.3.0",
55
"packageManager": "yarn@1.22.22",
66
"description": "Empathy. Upgraded.",
77
"author": "splinesreticulating",

src/ambient.d.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,10 @@ declare namespace App {
1313
ANTHROPIC_API_KEY?: string
1414
ANTHROPIC_MODEL?: string
1515
ANTHROPIC_TEMPERATURE?: string
16+
GROK_API_KEY?: string
17+
GROK_MODEL?: string
18+
GROK_TEMPERATURE?: string
1619
CUSTOM_CONTEXT?: string
17-
DEFAULT_PROVIDER?: 'khoj' | 'openai' | 'anthropic'
20+
DEFAULT_PROVIDER?: 'khoj' | 'openai' | 'anthropic' | 'grok'
1821
}
1922
}

src/lib/anthropic.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export const getAnthropicReply = async (
5959
throw new Error(`Anthropic API error code ${response.status}: ${response.statusText}`)
6060
}
6161

62-
const data = await response.json() as { content?: Array<{ text?: string }> }
62+
const data = (await response.json()) as { content?: Array<{ text?: string }> }
6363
const rawOutput = data.content?.[0]?.text || ''
6464

6565
logger.debug({ rawOutput }, 'Anthropic raw response')

src/lib/grok.ts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { GROK_API_KEY, GROK_MODEL, GROK_TEMPERATURE } from '$env/static/private'
2+
import { fetchRelevantHistory } from './history'
3+
import { logger } from './logger'
4+
import { openAiPrompt, systemContext } from './prompts'
5+
import type { Message, ToneType } from './types'
6+
import { formatMessagesAsText } from './utils'
7+
8+
const API_URL = 'https://grok.x.ai/api/chat/completions'
9+
const DEFAULT_MODEL = 'grok-1'
10+
const DEFAULT_TEMPERATURE = 0.5
11+
12+
const getConfig = () => ({
13+
model: GROK_MODEL || DEFAULT_MODEL,
14+
temperature: Number(GROK_TEMPERATURE || DEFAULT_TEMPERATURE),
15+
apiKey: GROK_API_KEY,
16+
})
17+
18+
export const getGrokReply = async (
19+
messages: Message[],
20+
tone: ToneType,
21+
context: string
22+
): Promise<{ summary: string; replies: string[] }> => {
23+
const config = getConfig()
24+
if (!config.apiKey) {
25+
return {
26+
summary: 'Grok API key is not configured.',
27+
replies: ['Please set up your Grok API key in the .env file.'],
28+
}
29+
}
30+
31+
const historyContext = await fetchRelevantHistory(messages)
32+
const prompt = openAiPrompt(tone, historyContext) + '\n\n' + context
33+
34+
const body = {
35+
model: config.model,
36+
messages: [
37+
{ role: 'system', content: systemContext },
38+
{ role: 'user', content: formatMessagesAsText(messages) },
39+
{ role: 'user', content: prompt },
40+
],
41+
temperature: config.temperature,
42+
tools: [
43+
{
44+
type: 'function',
45+
function: {
46+
name: 'draft_replies',
47+
description: 'Generate a short summary and three suggested replies',
48+
parameters: {
49+
type: 'object',
50+
properties: {
51+
summary: { type: 'string' },
52+
replies: { type: 'array', items: { type: 'string' } },
53+
},
54+
required: ['summary', 'replies'],
55+
},
56+
},
57+
},
58+
],
59+
tool_choice: { type: 'function', function: { name: 'draft_replies' } },
60+
}
61+
62+
logger.debug({ body }, 'Sending request to Grok')
63+
logger.info('Sending request to Grok')
64+
65+
try {
66+
const response = await fetch(API_URL, {
67+
method: 'POST',
68+
headers: {
69+
'Content-Type': 'application/json',
70+
Authorization: `Bearer ${config.apiKey}`,
71+
},
72+
body: JSON.stringify(body),
73+
})
74+
75+
if (!response.ok) {
76+
logger.error({ status: response.status }, 'Grok API error')
77+
throw new Error(`Grok API error code ${response.status}: ${response.statusText}`)
78+
}
79+
80+
const { choices } = await response.json()
81+
const functionCall = choices[0]?.message?.tool_calls?.[0]?.function
82+
if (!functionCall) throw new Error('No function call in response')
83+
const { summary, replies } = JSON.parse(functionCall.arguments)
84+
85+
if (!summary || !Array.isArray(replies)) {
86+
logger.error({ summary, replies }, 'Invalid response format from Grok')
87+
throw new Error('Invalid response format from Grok')
88+
}
89+
90+
return { summary, replies }
91+
} catch (error) {
92+
logger.error({ error }, 'Failed to get Grok reply')
93+
return {
94+
summary: '',
95+
replies: ['(AI API error. Check your key and usage.)'],
96+
}
97+
}
98+
}

src/lib/prompts.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,10 @@ export const khojPrompt = (messages: Message[], tone: ToneType, context: string)
4242
responseFormat,
4343
].join('\n')
4444

45-
export const anthropicPrompt = (
46-
messages: Message[],
47-
tone: ToneType,
48-
context: string
49-
): string =>
45+
export const anthropicPrompt = (messages: Message[], tone: ToneType, context: string): string =>
5046
[
5147
systemContext,
52-
'Here are some text messages between my partner and I:\n' +
53-
formatMessagesAsText(messages),
48+
'Here are some text messages between my partner and I:\n' + formatMessagesAsText(messages),
5449
buildPrompt(tone, context),
5550
responseFormat,
5651
].join('\n')

src/lib/providers/registry.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ANTHROPIC_API_KEY, KHOJ_API_URL, OPENAI_API_KEY } from '$env/static/private'
1+
import { ANTHROPIC_API_KEY, KHOJ_API_URL, OPENAI_API_KEY, GROK_API_KEY } from '$env/static/private'
22

33
export interface ProviderConfig {
44
id: string
@@ -28,6 +28,12 @@ const PROVIDER_REGISTRY: Omit<ProviderConfig, 'isAvailable'>[] = [
2828
displayName: 'Anthropic (Claude)',
2929
envVar: 'ANTHROPIC_API_KEY',
3030
},
31+
{
32+
id: 'grok',
33+
name: 'Grok',
34+
displayName: 'Grok (X.ai)',
35+
envVar: 'GROK_API_KEY',
36+
},
3137
// Future providers can be added here with their corresponding env vars
3238
// {
3339
// id: 'gemini',
@@ -42,6 +48,7 @@ const ENV_VARS: Record<string, string | undefined> = {
4248
OPENAI_API_KEY,
4349
KHOJ_API_URL,
4450
ANTHROPIC_API_KEY,
51+
GROK_API_KEY,
4552
// Add new env vars here as they become available
4653
// GOOGLE_API_KEY
4754
}

src/lib/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export interface MessageRow {
1515
export interface PageData {
1616
messages?: Message[]
1717
multiProvider: boolean
18-
defaultProvider: 'khoj' | 'openai' | 'anthropic'
18+
defaultProvider: 'khoj' | 'openai' | 'anthropic' | 'grok'
1919
}
2020

2121
export const TONES = ['gentle', 'funny', 'reassuring', 'concise'] as const

src/routes/+page.server.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { queryMessagesDb } from '$lib/iMessages'
22
import { getKhojReply } from '$lib/khoj'
33
import { getAnthropicReply } from '$lib/anthropic'
4+
import { getGrokReply } from '$lib/grok'
45
import { logger } from '$lib/logger'
56
import { getOpenaiReply } from '$lib/openAi'
67
import { DEFAULT_PROVIDER } from '$lib/provider'
@@ -42,8 +43,10 @@ export const actions: Actions = {
4243
provider === 'khoj'
4344
? getKhojReply
4445
: provider === 'anthropic'
45-
? getAnthropicReply
46-
: getOpenaiReply
46+
? getAnthropicReply
47+
: provider === 'grok'
48+
? getGrokReply
49+
: getOpenaiReply
4750

4851
let messages: Message[]
4952
try {

0 commit comments

Comments
 (0)