Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions src/lib/anthropic.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ANTHROPIC_API_KEY, ANTHROPIC_MODEL, ANTHROPIC_TEMPERATURE } from '$env/static/private'
import { settings } from '$lib/config'
import { anthropicPrompt } from '$lib/prompts'
import type { Message, ToneType } from '$lib/types'
import { extractReplies, parseSummaryToHumanReadable } from '$lib/utils'
Expand All @@ -10,9 +10,9 @@ const DEFAULT_MODEL = 'claude-3-opus-20240229'
const DEFAULT_TEMPERATURE = 0.5

const getConfig = () => ({
model: ANTHROPIC_MODEL || DEFAULT_MODEL,
temperature: Number(ANTHROPIC_TEMPERATURE || DEFAULT_TEMPERATURE),
apiKey: ANTHROPIC_API_KEY,
model: settings.ANTHROPIC_MODEL || DEFAULT_MODEL,
temperature: Number(settings.ANTHROPIC_TEMPERATURE || DEFAULT_TEMPERATURE),
apiKey: settings.ANTHROPIC_API_KEY,
})

export const getAnthropicReply = async (
Expand Down
34 changes: 34 additions & 0 deletions src/lib/components/SettingsForm.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<script lang="ts">
import { enhance } from '$app/forms'
export let settings: { key: string; value: string; description: string }[]
export let action = '/settings?/save'
</script>

<form method="POST" use:enhance class="settings-form">
{#each settings as setting}
<div class="setting-row">
<label for={setting.key}>{setting.key}</label>
<input id={setting.key} name={setting.key} type="text" value={setting.value} />
<p class="description">{setting.description}</p>
</div>
{/each}
<button type="submit" formaction={action} class="save">Save</button>
</form>

<style>
.setting-row {
margin-bottom: 1rem;
}
label {
font-weight: bold;
display: block;
}
.description {
font-size: 0.8rem;
color: var(--gray);
margin-top: 0.25rem;
}
.save {
margin-top: 1rem;
}
</style>
71 changes: 71 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { env } from '$env/dynamic/private'
import { open } from 'sqlite'
import sqlite3 from 'sqlite3'
import os from 'node:os'
import path from 'node:path'

export interface Setting {
key: string
value: string
description: string
}

const DB_PATH = path.join(os.homedir(), '.wellsaid_settings.db')

const db = await open({ filename: DB_PATH, driver: sqlite3.Database })

await db.exec(`CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT,
description TEXT
)`)

const defaultSettings: Record<string, { value?: string; description: string }> = {
PARTNER_PHONE: { value: env.PARTNER_PHONE, description: "Your partner's phone number in the Messages app" },
HISTORY_LOOKBACK_HOURS: { value: env.HISTORY_LOOKBACK_HOURS, description: 'How many hours of prior conversation history to search for extra context' },
CUSTOM_CONTEXT: { value: env.CUSTOM_CONTEXT, description: 'To guide the AI\'s personality and behavior (Example: "Act as my therapist suggesting replies to my partner" or "You are a helpful assistant")' },
OPENAI_API_KEY: { value: env.OPENAI_API_KEY, description: 'Your OpenAI API key' },
OPENAI_MODEL: { value: env.OPENAI_MODEL, description: 'gpt-4 or any other OpenAI model' },
OPENAI_TEMPERATURE: { value: env.OPENAI_TEMPERATURE, description: 'Controls the randomness of the responses' },
OPENAI_TOP_P: { value: env.OPENAI_TOP_P, description: 'Lets the responses be a little more adventurous' },
OPENAI_FREQUENCY_PENALTY: { value: env.OPENAI_FREQUENCY_PENALTY, description: 'Keeps the suggestions from repeating themselves' },
OPENAI_PRESENCE_PENALTY: { value: env.OPENAI_PRESENCE_PENALTY, description: 'Nudges the AI to bring up fresh ideas' },
ANTHROPIC_API_KEY: { value: env.ANTHROPIC_API_KEY, description: 'Your Anthropic API key' },
ANTHROPIC_MODEL: { value: env.ANTHROPIC_MODEL, description: 'claude-3-opus-20240229 or another Anthropic model' },
ANTHROPIC_TEMPERATURE: { value: env.ANTHROPIC_TEMPERATURE, description: "Controls the randomness of Claude's responses" },
GROK_API_KEY: { value: env.GROK_API_KEY, description: 'Your Grok API key' },
GROK_MODEL: { value: env.GROK_MODEL, description: 'grok-1 or another Grok model' },
GROK_TEMPERATURE: { value: env.GROK_TEMPERATURE, description: 'Controls the randomness of Grok\'s responses' },
KHOJ_API_URL: { value: env.KHOJ_API_URL, description: 'Your Khoj server API URL if you have one, otherwise leave this out or leave it blank' },
KHOJ_AGENT: { value: env.KHOJ_AGENT, description: 'optional specific agent to use' },
}

for (const [key, { value = '', description }] of Object.entries(defaultSettings)) {
await db.run(
'INSERT OR IGNORE INTO settings (key, value, description) VALUES (?, ?, ?)',
key,
value,
description
)
}

const rows = await db.all<Setting[]>('SELECT key, value, description FROM settings')

export const settings: Record<string, string> = {}
for (const row of rows) {
settings[row.key] = row.value
}

export async function updateSetting(key: string, value: string): Promise<void> {
await db.run(
'INSERT INTO settings (key, value, description) VALUES (?, ?, (SELECT description FROM settings WHERE key = ?)) ON CONFLICT(key) DO UPDATE SET value=excluded.value',
key,
value,
key
)
settings[key] = value
}

export async function getAllSettings(): Promise<Setting[]> {
return db.all<Setting[]>('SELECT key, value, description FROM settings')
}
10 changes: 5 additions & 5 deletions src/lib/grok.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { GROK_API_KEY, GROK_MODEL, GROK_TEMPERATURE } from '$env/static/private'
import { settings } from '$lib/config'
import { fetchRelevantHistory } from './history'
import { logger } from './logger'
import { openAiPrompt, systemContext } from './prompts'
Expand All @@ -10,9 +10,9 @@ const DEFAULT_MODEL = 'grok-1'
const DEFAULT_TEMPERATURE = 0.5

const getConfig = () => ({
model: GROK_MODEL || DEFAULT_MODEL,
temperature: Number(GROK_TEMPERATURE || DEFAULT_TEMPERATURE),
apiKey: GROK_API_KEY,
model: settings.GROK_MODEL || DEFAULT_MODEL,
temperature: Number(settings.GROK_TEMPERATURE || DEFAULT_TEMPERATURE),
apiKey: settings.GROK_API_KEY,
})

export const getGrokReply = async (
Expand All @@ -34,7 +34,7 @@ export const getGrokReply = async (
const body = {
model: config.model,
messages: [
{ role: 'system', content: systemContext },
{ role: 'system', content: systemContext() },
{ role: 'user', content: formatMessagesAsText(messages) },
{ role: 'user', content: prompt },
],
Expand Down
4 changes: 2 additions & 2 deletions src/lib/history.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { HISTORY_LOOKBACK_HOURS } from '$env/static/private'
import { settings } from '$lib/config'
import type { Message } from '$lib/types'
import { queryMessagesDb } from './iMessages'
import { logger } from './logger'
import { formatMessagesAsText } from './utils'

const lookbackHours = Number.parseInt(HISTORY_LOOKBACK_HOURS || '0')

export const fetchRelevantHistory = async (messages: Message[]): Promise<string> => {
try {
const lookbackHours = Number.parseInt(settings.HISTORY_LOOKBACK_HOURS || '0')
if (!lookbackHours || messages.length === 0) {
logger.warn('No messages or invalid lookbackHours; skipping history fetch')
return ''
Expand Down
12 changes: 6 additions & 6 deletions src/lib/iMessages.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PARTNER_PHONE } from '$env/static/private'
import { settings } from '$lib/config'
import type { MessageRow } from '$lib/types'
import os from 'node:os'
import path from 'node:path'
Expand All @@ -11,7 +11,7 @@ const CHAT_DB_PATH = path.join(os.homedir(), 'Library', 'Messages', 'chat.db')

const buildQuery = (startDate: string, endDate: string) => {
logger.debug({ startDate, endDate }, 'Getting messages')
const params = [PARTNER_PHONE, isoToAppleNanoseconds(startDate), isoToAppleNanoseconds(endDate)]
const params = [settings.PARTNER_PHONE, isoToAppleNanoseconds(startDate), isoToAppleNanoseconds(endDate)]

const query = `
SELECT
Expand All @@ -35,7 +35,7 @@ const formatMessages = (rows: MessageRow[]) => {
.map((row) => ({
sender: row.is_from_me
? 'me'
: row.contact_id === PARTNER_PHONE
: row.contact_id === settings.PARTNER_PHONE
? 'partner'
: 'unknown',
text: row.text,
Expand All @@ -47,8 +47,8 @@ const formatMessages = (rows: MessageRow[]) => {
}

export const queryMessagesDb = async (startDate: string, endDate: string) => {
if (!PARTNER_PHONE) {
logger.warn('PARTNER_PHONE env var not set -- make sure it is set in your .env file')
if (!settings.PARTNER_PHONE) {
logger.warn('PARTNER_PHONE setting not configured')
return { messages: [] }
}

Expand All @@ -57,7 +57,7 @@ export const queryMessagesDb = async (startDate: string, endDate: string) => {

try {
const rows = (await db.all(query, params)) as MessageRow[]
logger.info({ count: rows.length, handleId: PARTNER_PHONE }, 'Fetched messages')
logger.info({ count: rows.length, handleId: settings.PARTNER_PHONE }, 'Fetched messages')

const formattedMessages = formatMessages(rows)

Expand Down
6 changes: 3 additions & 3 deletions src/lib/khoj.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { KHOJ_AGENT, KHOJ_API_URL } from '$env/static/private'
import { settings } from '$lib/config'
import { khojPrompt } from '$lib/prompts'
import type { Message, ToneType } from '$lib/types'
import { extractReplies, parseSummaryToHumanReadable } from '$lib/utils'
import { fetchRelevantHistory } from './history'
import { logger } from './logger'

const khojApiUrl = KHOJ_API_URL || 'http://localhost:42110/api/chat'
const khojApiUrl = settings.KHOJ_API_URL || 'http://localhost:42110/api/chat'

export const getKhojReply = async (
messages: Message[],
Expand All @@ -17,7 +17,7 @@ export const getKhojReply = async (
const prompt = khojPrompt(messages, tone, mergedContext)
const body = {
q: prompt,
...(KHOJ_AGENT ? { agent: KHOJ_AGENT } : {}),
...(settings.KHOJ_AGENT ? { agent: settings.KHOJ_AGENT } : {}),
}

logger.debug({ body }, 'Khoj body')
Expand Down
25 changes: 9 additions & 16 deletions src/lib/openAi.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,4 @@
import {
OPENAI_API_KEY,
OPENAI_FREQUENCY_PENALTY,
OPENAI_MODEL,
OPENAI_PRESENCE_PENALTY,
OPENAI_TEMPERATURE,
OPENAI_TOP_P,
} from '$env/static/private'
import { settings } from '$lib/config'
import { fetchRelevantHistory } from './history'
import { logger } from './logger'
import { openAiPrompt, systemContext } from './prompts'
Expand All @@ -17,16 +10,16 @@ const DEFAULT_MODEL = 'gpt-4'
const DEFAULT_TEMPERATURE = 0.5

const getConfig = (): OpenAIConfig => ({
model: OPENAI_MODEL || DEFAULT_MODEL,
temperature: Number(OPENAI_TEMPERATURE || DEFAULT_TEMPERATURE),
topP: OPENAI_TOP_P ? Number(OPENAI_TOP_P) : undefined,
frequencyPenalty: OPENAI_FREQUENCY_PENALTY ? Number(OPENAI_FREQUENCY_PENALTY) : undefined,
presencePenalty: OPENAI_PRESENCE_PENALTY ? Number(OPENAI_PRESENCE_PENALTY) : undefined,
model: settings.OPENAI_MODEL || DEFAULT_MODEL,
temperature: Number(settings.OPENAI_TEMPERATURE || DEFAULT_TEMPERATURE),
topP: settings.OPENAI_TOP_P ? Number(settings.OPENAI_TOP_P) : undefined,
frequencyPenalty: settings.OPENAI_FREQUENCY_PENALTY ? Number(settings.OPENAI_FREQUENCY_PENALTY) : undefined,
presencePenalty: settings.OPENAI_PRESENCE_PENALTY ? Number(settings.OPENAI_PRESENCE_PENALTY) : undefined,
apiUrl: API_URL,
apiKey: OPENAI_API_KEY,
apiKey: settings.OPENAI_API_KEY,
})

if (!OPENAI_API_KEY) {
if (!settings.OPENAI_API_KEY) {
logger.warn('⚠️ OPENAI_API_KEY is not set. OpenAI integration will not work.')
}

Expand Down Expand Up @@ -72,7 +65,7 @@ export const getOpenaiReply = async (
const body = {
model: config.model,
messages: [
{ role: 'system', content: systemContext },
{ role: 'system', content: systemContext() },
{ role: 'user', content: formatMessagesAsText(messages) },
{ role: 'user', content: prompt },
],
Expand Down
8 changes: 4 additions & 4 deletions src/lib/prompts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CUSTOM_CONTEXT } from '$env/static/private'
import { settings } from '$lib/config'
import type { Message, ToneType } from './types'
import { formatMessagesAsText } from './utils'

Expand All @@ -24,7 +24,7 @@ const responseFormat = [
'Reply 3: <long reply>',
].join('\n')

export const systemContext = [CUSTOM_CONTEXT, coreContext].join('\n\n')
export const systemContext = () => [settings.CUSTOM_CONTEXT || '', coreContext].join('\n\n')

const buildPrompt = (tone: string, context: string): string => {
const lines = [`${instructions} ${tone}`]
Expand All @@ -36,15 +36,15 @@ export const openAiPrompt = (tone: string, context: string): string => buildProm

export const khojPrompt = (messages: Message[], tone: ToneType, context: string): string =>
[
systemContext,
systemContext(),
'Here are some text messages between my partner and I:\n' + formatMessagesAsText(messages),
buildPrompt(tone, context),
responseFormat,
].join('\n')

export const anthropicPrompt = (messages: Message[], tone: ToneType, context: string): string =>
[
systemContext,
systemContext(),
'Here are some text messages between my partner and I:\n' + formatMessagesAsText(messages),
buildPrompt(tone, context),
responseFormat,
Expand Down
19 changes: 5 additions & 14 deletions src/lib/providers/registry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ANTHROPIC_API_KEY, KHOJ_API_URL, OPENAI_API_KEY, GROK_API_KEY } from '$env/static/private'
import { settings } from '$lib/config'

export interface ProviderConfig {
id: string
Expand Down Expand Up @@ -43,23 +43,14 @@ const PROVIDER_REGISTRY: Omit<ProviderConfig, 'isAvailable'>[] = [
// }
]

// Environment variable lookup
const ENV_VARS: Record<string, string | undefined> = {
OPENAI_API_KEY,
KHOJ_API_URL,
ANTHROPIC_API_KEY,
GROK_API_KEY,
// Add new env vars here as they become available
// GOOGLE_API_KEY
}

/**
* Get all available AI providers based on configured environment variables
* Get all available AI providers based on configured settings
*/
export function getAvailableProviders(): ProviderConfig[] {
return PROVIDER_REGISTRY.map((provider) => ({
...provider,
isAvailable: !!ENV_VARS[provider.envVar],
isAvailable: !!settings[provider.envVar],
})).filter((provider) => provider.isAvailable)
}

Expand All @@ -71,7 +62,7 @@ export function getDefaultProvider(): string {

if (available.length === 0) {
throw new Error(
'No AI providers are configured. Please set at least one provider environment variable.'
'No AI providers are configured. Please set at least one provider in settings.'
)
}

Expand All @@ -95,7 +86,7 @@ export function validateProviders(): void {

if (available.length === 0) {
console.error(
'Error: No AI providers are configured. Set at least one of the following environment variables:'
'Error: No AI providers are configured. Set at least one of the following settings:'
)
PROVIDER_REGISTRY.forEach((provider) => {
console.error(` - ${provider.envVar} (for ${provider.displayName})`)
Expand Down
3 changes: 3 additions & 0 deletions src/routes/+page.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { logger } from '$lib/logger'
import { getOpenaiReply } from '$lib/openAi'
import { DEFAULT_PROVIDER } from '$lib/provider'
import { getAvailableProviders, hasMultipleProviders } from '$lib/providers/registry'
import { getAllSettings } from '$lib/config'
import type { Message, ToneType } from '$lib/types'
import { fail } from '@sveltejs/kit'
import type { Actions, PageServerLoad } from './$types'
Expand All @@ -17,12 +18,14 @@ export const load: PageServerLoad = async ({ url }) => {
const end = new Date()
const start = new Date(end.getTime() - lookBack * ONE_HOUR)
const { messages } = await queryMessagesDb(start.toISOString(), end.toISOString())
const settings = await getAllSettings()

return {
messages,
multiProvider: hasMultipleProviders(),
defaultProvider: DEFAULT_PROVIDER,
availableProviders: getAvailableProviders(),
settings,
}
}

Expand Down
Loading