This commit is contained in:
@@ -8,6 +8,11 @@ OLLAMA_URL=http://localhost:11434
|
|||||||
OLLAMA_MODEL_FAST=llama3.2:1b
|
OLLAMA_MODEL_FAST=llama3.2:1b
|
||||||
OLLAMA_MODEL_PROSE=llama3.2:1b
|
OLLAMA_MODEL_PROSE=llama3.2:1b
|
||||||
|
|
||||||
|
# Hyper machine (main PC) — seeds hyper profile defaults in AI settings
|
||||||
|
# HYPER_OLLAMA_URL=http://192.168.1.50:11434
|
||||||
|
# HYPER_OLLAMA_MODEL_FAST=llama3.1:8b
|
||||||
|
# HYPER_OLLAMA_MODEL_PROSE=llama3.1:8b
|
||||||
|
|
||||||
# Calibre library (live read-only metadata)
|
# Calibre library (live read-only metadata)
|
||||||
CALIBRE_LIBRARY_PATH=
|
CALIBRE_LIBRARY_PATH=
|
||||||
CALIBRE_METADATA_DB_PATH=
|
CALIBRE_METADATA_DB_PATH=
|
||||||
|
|||||||
@@ -2,17 +2,20 @@ import { handleApi } from "@/lib/api";
|
|||||||
import { requireUser } from "@/lib/services/user";
|
import { requireUser } from "@/lib/services/user";
|
||||||
import {
|
import {
|
||||||
getAiBehaviorConfig,
|
getAiBehaviorConfig,
|
||||||
getAiProviderConfig,
|
getAiProviderSettings,
|
||||||
saveAiBehaviorConfig,
|
saveAiBehaviorConfig,
|
||||||
|
saveAiProviderSettings,
|
||||||
saveAiProviderConfig,
|
saveAiProviderConfig,
|
||||||
} from "@/lib/services/ai-config";
|
} from "@/lib/services/ai-config";
|
||||||
|
import type { AIProviderSettings } from "@/lib/ai/types";
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
return handleApi(async () => {
|
return handleApi(async () => {
|
||||||
const { user } = await requireUser();
|
const { user } = await requireUser();
|
||||||
const behavior = await getAiBehaviorConfig(user.id);
|
const behavior = await getAiBehaviorConfig(user.id);
|
||||||
const provider = await getAiProviderConfig(user.id);
|
const providerSettings = await getAiProviderSettings(user.id);
|
||||||
return { behavior, provider };
|
const provider = providerSettings.profiles[providerSettings.machineMode];
|
||||||
|
return { behavior, provider, providerSettings };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,13 +24,23 @@ export async function PATCH(request: Request) {
|
|||||||
return handleApi(async () => {
|
return handleApi(async () => {
|
||||||
const { user } = await requireUser();
|
const { user } = await requireUser();
|
||||||
let behavior = await getAiBehaviorConfig(user.id);
|
let behavior = await getAiBehaviorConfig(user.id);
|
||||||
let provider = await getAiProviderConfig(user.id);
|
let providerSettings = await getAiProviderSettings(user.id);
|
||||||
|
|
||||||
if (body.behavior) {
|
if (body.behavior) {
|
||||||
behavior = await saveAiBehaviorConfig(user.id, body.behavior);
|
behavior = await saveAiBehaviorConfig(user.id, body.behavior);
|
||||||
}
|
}
|
||||||
if (body.provider) {
|
|
||||||
provider = await saveAiProviderConfig(user.id, body.provider);
|
if (body.providerSettings) {
|
||||||
|
providerSettings = await saveAiProviderSettings(
|
||||||
|
user.id,
|
||||||
|
body.providerSettings as Partial<AIProviderSettings>
|
||||||
|
);
|
||||||
|
} else if (body.provider) {
|
||||||
|
await saveAiProviderConfig(user.id, body.provider);
|
||||||
|
providerSettings = await getAiProviderSettings(user.id);
|
||||||
}
|
}
|
||||||
return { behavior, provider };
|
|
||||||
|
const provider = providerSettings.profiles[providerSettings.machineMode];
|
||||||
|
return { behavior, provider, providerSettings };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { handleApi } from "@/lib/api";
|
import { handleApi } from "@/lib/api";
|
||||||
import { requireUser } from "@/lib/services/user";
|
import { requireUser } from "@/lib/services/user";
|
||||||
import { listMemories, saveProfileSummary } from "@/lib/services/ai-memory";
|
import { listMemories, saveProfileSummary } from "@/lib/services/ai-memory";
|
||||||
import { getProvider } from "@/lib/ai/provider-registry";
|
import { getAiBehaviorConfig, getAiProviderConfig, generateTextWithFallback, buildSystemPrompt } from "@/lib/services/ai-config";
|
||||||
import { getAiBehaviorConfig, getAiProviderConfig, buildSystemPrompt } from "@/lib/services/ai-config";
|
|
||||||
import { getTemplateBody } from "@/lib/services/ai-templates";
|
import { getTemplateBody } from "@/lib/services/ai-templates";
|
||||||
import { renderTemplate } from "@/lib/ai/prompts/render";
|
import { renderTemplate } from "@/lib/ai/prompts/render";
|
||||||
|
|
||||||
@@ -32,8 +31,9 @@ export async function POST() {
|
|||||||
user_name: user.displayName,
|
user_name: user.displayName,
|
||||||
context: bulletList,
|
context: bulletList,
|
||||||
});
|
});
|
||||||
const provider = getProvider(providerConfig.type);
|
const res = await generateTextWithFallback(
|
||||||
const res = await provider.generateText(providerConfig, {
|
user.id,
|
||||||
|
{
|
||||||
model: providerConfig.model,
|
model: providerConfig.model,
|
||||||
prompt,
|
prompt,
|
||||||
system,
|
system,
|
||||||
@@ -41,7 +41,9 @@ export async function POST() {
|
|||||||
temperature: 0.5,
|
temperature: 0.5,
|
||||||
maxTokens: 400,
|
maxTokens: 400,
|
||||||
timeoutMs: providerConfig.timeoutMs,
|
timeoutMs: providerConfig.timeoutMs,
|
||||||
});
|
},
|
||||||
|
behavior
|
||||||
|
);
|
||||||
if (res.text.trim()) summaryText = res.text.trim();
|
if (res.text.trim()) summaryText = res.text.trim();
|
||||||
} catch {
|
} catch {
|
||||||
/* use bullet list fallback */
|
/* use bullet list fallback */
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
|
import type { AIProviderConfig, AIProviderSettings, AIMachineMode } from "@/lib/ai/types";
|
||||||
|
|
||||||
const PERSONALITIES = [
|
const PERSONALITIES = [
|
||||||
{ id: "supportive_mentor", label: "Supportive Mentor" },
|
{ id: "supportive_mentor", label: "Supportive Mentor" },
|
||||||
@@ -11,6 +12,34 @@ const PERSONALITIES = [
|
|||||||
{ id: "friendly_coach", label: "Friendly Coach" },
|
{ id: "friendly_coach", label: "Friendly Coach" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const DEFAULT_LOCAL_PROVIDER: AIProviderConfig = {
|
||||||
|
type: "ollama",
|
||||||
|
baseUrl: "http://localhost:11434",
|
||||||
|
model: "llama3.2:1b",
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2048,
|
||||||
|
timeoutMs: 60000,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_HYPER_PROVIDER: AIProviderConfig = {
|
||||||
|
type: "ollama",
|
||||||
|
baseUrl: "http://192.168.1.50:11434",
|
||||||
|
model: "llama3.1:8b",
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2048,
|
||||||
|
timeoutMs: 120000,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PROVIDER_SETTINGS: AIProviderSettings = {
|
||||||
|
machineMode: "local",
|
||||||
|
profiles: {
|
||||||
|
local: DEFAULT_LOCAL_PROVIDER,
|
||||||
|
hyper: DEFAULT_HYPER_PROVIDER,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export function AiConfigPanel() {
|
export function AiConfigPanel() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const { data } = useQuery({
|
const { data } = useQuery({
|
||||||
@@ -29,27 +58,46 @@ export function AiConfigPanel() {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
strictMode: false,
|
strictMode: false,
|
||||||
});
|
});
|
||||||
const [provider, setProvider] = useState({
|
const [providerSettings, setProviderSettings] =
|
||||||
type: "ollama",
|
useState<AIProviderSettings>(DEFAULT_PROVIDER_SETTINGS);
|
||||||
baseUrl: "http://localhost:11434",
|
|
||||||
model: "llama3.2:3b",
|
|
||||||
temperature: 0.7,
|
|
||||||
maxTokens: 2048,
|
|
||||||
timeoutMs: 60000,
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (data?.behavior) setBehavior(data.behavior);
|
if (data?.behavior) setBehavior(data.behavior);
|
||||||
if (data?.provider) setProvider(data.provider);
|
if (data?.providerSettings) setProviderSettings(data.providerSettings);
|
||||||
|
else if (data?.provider) {
|
||||||
|
setProviderSettings({
|
||||||
|
machineMode: "local",
|
||||||
|
profiles: {
|
||||||
|
local: data.provider,
|
||||||
|
hyper: DEFAULT_HYPER_PROVIDER,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const activeMode = providerSettings.machineMode;
|
||||||
|
const activeProfile = providerSettings.profiles[activeMode];
|
||||||
|
|
||||||
|
const updateActiveProfile = (updates: Partial<AIProviderConfig>) => {
|
||||||
|
setProviderSettings({
|
||||||
|
...providerSettings,
|
||||||
|
profiles: {
|
||||||
|
...providerSettings.profiles,
|
||||||
|
[activeMode]: { ...activeProfile, ...updates },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const setMachineMode = (mode: AIMachineMode) => {
|
||||||
|
setProviderSettings({ ...providerSettings, machineMode: mode });
|
||||||
|
};
|
||||||
|
|
||||||
const save = useMutation({
|
const save = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const res = await fetch("/api/ai/config", {
|
const res = await fetch("/api/ai/config", {
|
||||||
method: "PATCH",
|
method: "PATCH",
|
||||||
headers: { "Content-Type": "application/json" },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ behavior, provider }),
|
body: JSON.stringify({ behavior, providerSettings }),
|
||||||
});
|
});
|
||||||
return res.json();
|
return res.json();
|
||||||
},
|
},
|
||||||
@@ -126,11 +174,32 @@ export function AiConfigPanel() {
|
|||||||
|
|
||||||
<section className="space-y-2">
|
<section className="space-y-2">
|
||||||
<h3 className="text-sm font-bold">Provider Settings</h3>
|
<h3 className="text-sm font-bold">Provider Settings</h3>
|
||||||
|
|
||||||
|
<label className="flex items-center gap-2 text-sm">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={activeMode === "hyper"}
|
||||||
|
onChange={(e) => setMachineMode(e.target.checked ? "hyper" : "local")}
|
||||||
|
/>
|
||||||
|
Hyper-machine mode
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<p className="text-xs text-[var(--color-text-muted)]">
|
||||||
|
{activeMode === "hyper"
|
||||||
|
? `Hyper: ${providerSettings.profiles.hyper.model} @ ${providerSettings.profiles.hyper.baseUrl}`
|
||||||
|
: `Local: ${providerSettings.profiles.local.model} @ ${providerSettings.profiles.local.baseUrl}`}
|
||||||
|
{activeMode === "hyper" && (
|
||||||
|
<span className="block mt-1">
|
||||||
|
Falls back to local model automatically when the hyper machine is unreachable.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
|
||||||
<Field label="Provider Type">
|
<Field label="Provider Type">
|
||||||
<select
|
<select
|
||||||
className="retro-window-inset w-full p-2"
|
className="retro-window-inset w-full p-2"
|
||||||
value={provider.type}
|
value={activeProfile.type}
|
||||||
onChange={(e) => setProvider({ ...provider, type: e.target.value })}
|
onChange={(e) => updateActiveProfile({ type: e.target.value as AIProviderConfig["type"] })}
|
||||||
>
|
>
|
||||||
<option value="ollama">Ollama</option>
|
<option value="ollama">Ollama</option>
|
||||||
<option value="openai_compatible">OpenAI Compatible</option>
|
<option value="openai_compatible">OpenAI Compatible</option>
|
||||||
@@ -140,36 +209,36 @@ export function AiConfigPanel() {
|
|||||||
<Field label="Base URL">
|
<Field label="Base URL">
|
||||||
<input
|
<input
|
||||||
className="retro-window-inset w-full p-2"
|
className="retro-window-inset w-full p-2"
|
||||||
value={provider.baseUrl}
|
value={activeProfile.baseUrl}
|
||||||
onChange={(e) => setProvider({ ...provider, baseUrl: e.target.value })}
|
onChange={(e) => updateActiveProfile({ baseUrl: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Model">
|
<Field label="Model">
|
||||||
<input
|
<input
|
||||||
className="retro-window-inset w-full p-2"
|
className="retro-window-inset w-full p-2"
|
||||||
value={provider.model}
|
value={activeProfile.model}
|
||||||
onChange={(e) => setProvider({ ...provider, model: e.target.value })}
|
onChange={(e) => updateActiveProfile({ model: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Max Tokens">
|
<Field label="Max Tokens">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="retro-window-inset w-full p-2"
|
className="retro-window-inset w-full p-2"
|
||||||
value={provider.maxTokens}
|
value={activeProfile.maxTokens}
|
||||||
onChange={(e) => setProvider({ ...provider, maxTokens: parseInt(e.target.value) })}
|
onChange={(e) => updateActiveProfile({ maxTokens: parseInt(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<Field label="Timeout (ms)">
|
<Field label="Timeout (ms)">
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="retro-window-inset w-full p-2"
|
className="retro-window-inset w-full p-2"
|
||||||
value={provider.timeoutMs}
|
value={activeProfile.timeoutMs}
|
||||||
onChange={(e) => setProvider({ ...provider, timeoutMs: parseInt(e.target.value) })}
|
onChange={(e) => updateActiveProfile({ timeoutMs: parseInt(e.target.value) })}
|
||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">
|
<p className="text-xs text-[var(--color-text-muted)]">
|
||||||
API keys are configured via environment variables only (OPENAI_API_KEY, LLAMACPP_API_KEY).
|
API keys are configured via environment variables only (OPENAI_API_KEY, LLAMACPP_API_KEY).
|
||||||
Never stored in the database.
|
Never stored in the database. Hyper defaults can be seeded from HYPER_OLLAMA_* env vars on the server.
|
||||||
</p>
|
</p>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function AiHealthPanel() {
|
export function AiHealthPanel() {
|
||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
@@ -35,9 +34,20 @@ export function AiHealthPanel() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<h2 className="font-bold">AI Health</h2>
|
<h2 className="font-bold">AI Health</h2>
|
||||||
<p className="text-xs text-[var(--color-text-muted)]">
|
<p className="text-xs text-[var(--color-text-muted)]">
|
||||||
Check whether your local AI provider is reachable and responding.
|
Check whether your AI provider is reachable and responding.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
{health?.fallbackActive && (
|
||||||
|
<div className="text-sm text-[var(--color-gold)] retro-window-inset p-3">
|
||||||
|
Hyper machine unreachable — using local model.
|
||||||
|
{health.requestedMode && health.effectiveMode && (
|
||||||
|
<span className="block text-xs mt-1 text-[var(--color-text-muted)]">
|
||||||
|
Requested: {health.requestedMode} · Effective: {health.effectiveMode}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<p className="text-sm">Loading...</p>
|
<p className="text-sm">Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -58,6 +68,7 @@ export function AiHealthPanel() {
|
|||||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||||
<Stat label="Provider" value={health?.provider ?? "—"} />
|
<Stat label="Provider" value={health?.provider ?? "—"} />
|
||||||
<Stat label="Model" value={health?.model ?? "—"} />
|
<Stat label="Model" value={health?.model ?? "—"} />
|
||||||
|
<Stat label="Mode" value={health?.effectiveMode ?? health?.requestedMode ?? "—"} />
|
||||||
<Stat label="Latency" value={health?.latencyMs != null ? `${health.latencyMs}ms` : "—"} />
|
<Stat label="Latency" value={health?.latencyMs != null ? `${health.latencyMs}ms` : "—"} />
|
||||||
<Stat label="Base URL" value={health?.baseUrlSafe ?? "—"} />
|
<Stat label="Base URL" value={health?.baseUrlSafe ?? "—"} />
|
||||||
<Stat label="Context Length" value={health?.contextLength?.toString() ?? "—"} />
|
<Stat label="Context Length" value={health?.contextLength?.toString() ?? "—"} />
|
||||||
|
|||||||
@@ -58,6 +58,24 @@ export interface AIProviderConfig {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AIMachineMode = "local" | "hyper";
|
||||||
|
|
||||||
|
export interface AIProviderSettings {
|
||||||
|
machineMode: AIMachineMode;
|
||||||
|
profiles: {
|
||||||
|
local: AIProviderConfig;
|
||||||
|
hyper: AIProviderConfig;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AIHealthCacheMeta {
|
||||||
|
fallbackActive?: boolean;
|
||||||
|
requestedMode?: AIMachineMode;
|
||||||
|
effectiveMode?: AIMachineMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AIHealthCache = AIHealthResult & AIHealthCacheMeta;
|
||||||
|
|
||||||
export interface AIBehaviorConfig {
|
export interface AIBehaviorConfig {
|
||||||
personality: "supportive_mentor" | "wise_teacher" | "quiet_observer" | "academic_tutor" | "friendly_coach";
|
personality: "supportive_mentor" | "wise_teacher" | "quiet_observer" | "academic_tutor" | "friendly_coach";
|
||||||
verbosity: "minimal" | "balanced" | "detailed";
|
verbosity: "minimal" | "balanced" | "detailed";
|
||||||
@@ -88,6 +106,64 @@ export const DEFAULT_AI_PROVIDER: AIProviderConfig = {
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_HYPER_AI_PROVIDER: AIProviderConfig = {
|
||||||
|
type: "ollama",
|
||||||
|
baseUrl: env.hyperOllamaUrl,
|
||||||
|
model: env.hyperOllamaModelFast,
|
||||||
|
temperature: 0.7,
|
||||||
|
maxTokens: 2048,
|
||||||
|
timeoutMs: 120000,
|
||||||
|
enabled: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_AI_PROVIDER_SETTINGS: AIProviderSettings = {
|
||||||
|
machineMode: "local",
|
||||||
|
profiles: {
|
||||||
|
local: DEFAULT_AI_PROVIDER,
|
||||||
|
hyper: DEFAULT_HYPER_AI_PROVIDER,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function isLegacyProviderConfig(value: unknown): value is AIProviderConfig {
|
||||||
|
return (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"type" in value &&
|
||||||
|
"baseUrl" in value &&
|
||||||
|
!("machineMode" in value)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function migrateProviderSettings(value: unknown): AIProviderSettings {
|
||||||
|
if (
|
||||||
|
typeof value === "object" &&
|
||||||
|
value !== null &&
|
||||||
|
"machineMode" in value &&
|
||||||
|
"profiles" in value
|
||||||
|
) {
|
||||||
|
const settings = value as AIProviderSettings;
|
||||||
|
return {
|
||||||
|
machineMode: settings.machineMode ?? "local",
|
||||||
|
profiles: {
|
||||||
|
local: { ...DEFAULT_AI_PROVIDER, ...settings.profiles?.local },
|
||||||
|
hyper: { ...DEFAULT_HYPER_AI_PROVIDER, ...settings.profiles?.hyper },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const local = isLegacyProviderConfig(value)
|
||||||
|
? { ...DEFAULT_AI_PROVIDER, ...value }
|
||||||
|
: DEFAULT_AI_PROVIDER;
|
||||||
|
|
||||||
|
return {
|
||||||
|
machineMode: "local",
|
||||||
|
profiles: {
|
||||||
|
local,
|
||||||
|
hyper: DEFAULT_HYPER_AI_PROVIDER,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function redactUrl(url: string): string {
|
export function redactUrl(url: string): string {
|
||||||
try {
|
try {
|
||||||
const u = new URL(url);
|
const u = new URL(url);
|
||||||
|
|||||||
@@ -35,6 +35,22 @@ export const env = {
|
|||||||
return process.env.OLLAMA_MODEL_PROSE ?? process.env.OLLAMA_MODEL_FAST ?? "llama3.2:3b";
|
return process.env.OLLAMA_MODEL_PROSE ?? process.env.OLLAMA_MODEL_FAST ?? "llama3.2:3b";
|
||||||
},
|
},
|
||||||
|
|
||||||
|
get hyperOllamaUrl(): string {
|
||||||
|
return process.env.HYPER_OLLAMA_URL ?? "http://192.168.1.50:11434";
|
||||||
|
},
|
||||||
|
|
||||||
|
get hyperOllamaModelFast(): string {
|
||||||
|
return process.env.HYPER_OLLAMA_MODEL_FAST ?? "llama3.1:8b";
|
||||||
|
},
|
||||||
|
|
||||||
|
get hyperOllamaModelProse(): string {
|
||||||
|
return (
|
||||||
|
process.env.HYPER_OLLAMA_MODEL_PROSE ??
|
||||||
|
process.env.HYPER_OLLAMA_MODEL_FAST ??
|
||||||
|
"llama3.1:8b"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
get openAiApiKey(): string | undefined {
|
get openAiApiKey(): string | undefined {
|
||||||
return process.env.OPENAI_API_KEY;
|
return process.env.OPENAI_API_KEY;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -2,14 +2,21 @@ import { eq, and, desc } from "drizzle-orm";
|
|||||||
import { db, settings, aiHealthLog } from "../db";
|
import { db, settings, aiHealthLog } from "../db";
|
||||||
import {
|
import {
|
||||||
DEFAULT_AI_BEHAVIOR,
|
DEFAULT_AI_BEHAVIOR,
|
||||||
DEFAULT_AI_PROVIDER,
|
DEFAULT_AI_PROVIDER_SETTINGS,
|
||||||
|
migrateProviderSettings,
|
||||||
type AIBehaviorConfig,
|
type AIBehaviorConfig,
|
||||||
type AIProviderConfig,
|
type AIProviderConfig,
|
||||||
|
type AIProviderSettings,
|
||||||
type AIHealthResult,
|
type AIHealthResult,
|
||||||
|
type AIHealthCache,
|
||||||
|
type AIMachineMode,
|
||||||
|
type GenerateTextRequest,
|
||||||
|
type GenerateTextResponse,
|
||||||
} from "../ai/types";
|
} from "../ai/types";
|
||||||
import { getProvider } from "../ai/provider-registry";
|
import { getProvider } from "../ai/provider-registry";
|
||||||
|
import { isAiTimeoutError } from "../ai/ai-normalize";
|
||||||
import { PERSONALITY_MODIFIERS, VERBOSITY_MODIFIERS } from "../ai/prompts/defaults";
|
import { PERSONALITY_MODIFIERS, VERBOSITY_MODIFIERS } from "../ai/prompts/defaults";
|
||||||
import { SETTINGS_KEYS } from "@/lib/config";
|
import { SETTINGS_KEYS, env } from "@/lib/config";
|
||||||
|
|
||||||
export async function getAiBehaviorConfig(userId: string): Promise<AIBehaviorConfig> {
|
export async function getAiBehaviorConfig(userId: string): Promise<AIBehaviorConfig> {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
@@ -20,13 +27,26 @@ export async function getAiBehaviorConfig(userId: string): Promise<AIBehaviorCon
|
|||||||
return { ...DEFAULT_AI_BEHAVIOR, ...(row.value as Partial<AIBehaviorConfig>) };
|
return { ...DEFAULT_AI_BEHAVIOR, ...(row.value as Partial<AIBehaviorConfig>) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getAiProviderConfig(userId: string): Promise<AIProviderConfig> {
|
export async function getAiProviderSettings(userId: string): Promise<AIProviderSettings> {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(settings)
|
.from(settings)
|
||||||
.where(and(eq(settings.userId, userId), eq(settings.key, SETTINGS_KEYS.aiProvider)));
|
.where(and(eq(settings.userId, userId), eq(settings.key, SETTINGS_KEYS.aiProvider)));
|
||||||
if (!row?.value) return DEFAULT_AI_PROVIDER;
|
if (!row?.value) return DEFAULT_AI_PROVIDER_SETTINGS;
|
||||||
return { ...DEFAULT_AI_PROVIDER, ...(row.value as Partial<AIProviderConfig>) };
|
return migrateProviderSettings(row.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAiProviderConfig(userId: string): Promise<AIProviderConfig> {
|
||||||
|
const providerSettings = await getAiProviderSettings(userId);
|
||||||
|
return providerSettings.profiles[providerSettings.machineMode];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProseModel(userId: string): Promise<string> {
|
||||||
|
const providerSettings = await getAiProviderSettings(userId);
|
||||||
|
if (providerSettings.machineMode === "hyper") {
|
||||||
|
return env.hyperOllamaModelProse;
|
||||||
|
}
|
||||||
|
return env.ollamaModelProse;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveAiBehaviorConfig(userId: string, config: Partial<AIBehaviorConfig>) {
|
export async function saveAiBehaviorConfig(userId: string, config: Partial<AIBehaviorConfig>) {
|
||||||
@@ -43,9 +63,18 @@ export async function saveAiBehaviorConfig(userId: string, config: Partial<AIBeh
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveAiProviderConfig(userId: string, config: Partial<AIProviderConfig>) {
|
export async function saveAiProviderSettings(
|
||||||
const current = await getAiProviderConfig(userId);
|
userId: string,
|
||||||
const merged = { ...current, ...config };
|
config: Partial<AIProviderSettings>
|
||||||
|
): Promise<AIProviderSettings> {
|
||||||
|
const current = await getAiProviderSettings(userId);
|
||||||
|
const merged: AIProviderSettings = {
|
||||||
|
machineMode: config.machineMode ?? current.machineMode,
|
||||||
|
profiles: {
|
||||||
|
local: { ...current.profiles.local, ...config.profiles?.local },
|
||||||
|
hyper: { ...current.profiles.hyper, ...config.profiles?.hyper },
|
||||||
|
},
|
||||||
|
};
|
||||||
const { sql } = await import("drizzle-orm");
|
const { sql } = await import("drizzle-orm");
|
||||||
await db
|
await db
|
||||||
.insert(settings)
|
.insert(settings)
|
||||||
@@ -57,32 +86,40 @@ export async function saveAiProviderConfig(userId: string, config: Partial<AIPro
|
|||||||
return merged;
|
return merged;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getCachedHealth(userId: string): Promise<AIHealthResult | null> {
|
export async function saveAiProviderConfig(userId: string, config: Partial<AIProviderConfig>) {
|
||||||
|
const current = await getAiProviderSettings(userId);
|
||||||
|
const mode = current.machineMode;
|
||||||
|
return saveAiProviderSettings(userId, {
|
||||||
|
profiles: {
|
||||||
|
[mode]: { ...current.profiles[mode], ...config },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCachedHealth(userId: string): Promise<AIHealthCache | null> {
|
||||||
const [row] = await db
|
const [row] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(settings)
|
.from(settings)
|
||||||
.where(and(eq(settings.userId, userId), eq(settings.key, SETTINGS_KEYS.aiHealthCache)));
|
.where(and(eq(settings.userId, userId), eq(settings.key, SETTINGS_KEYS.aiHealthCache)));
|
||||||
return (row?.value as AIHealthResult) ?? null;
|
return (row?.value as AIHealthCache) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function runHealthCheck(userId: string): Promise<AIHealthResult> {
|
export function isConnectionError(error: unknown): boolean {
|
||||||
const config = await getAiProviderConfig(userId);
|
if (!(error instanceof Error)) return false;
|
||||||
const provider = getProvider(config.type);
|
if (isAiTimeoutError(error)) return true;
|
||||||
const result = await provider.healthCheck(config);
|
const msg = error.message.toLowerCase();
|
||||||
|
return (
|
||||||
|
msg.includes("fetch failed") ||
|
||||||
|
msg.includes("connection failed") ||
|
||||||
|
msg.includes("econnrefused") ||
|
||||||
|
msg.includes("enotfound") ||
|
||||||
|
msg.includes("network") ||
|
||||||
|
msg.includes("unable to connect")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function cacheHealthResult(userId: string, result: AIHealthCache) {
|
||||||
const { sql } = await import("drizzle-orm");
|
const { sql } = await import("drizzle-orm");
|
||||||
await db.insert(aiHealthLog).values({
|
|
||||||
userId,
|
|
||||||
providerType: config.type,
|
|
||||||
status: result.status,
|
|
||||||
latencyMs: result.latencyMs,
|
|
||||||
model: config.model,
|
|
||||||
contextLength: result.contextLength,
|
|
||||||
memoryUsageMb: result.memoryUsageMb,
|
|
||||||
baseUrlSafe: result.baseUrlSafe,
|
|
||||||
errorMessage: result.lastError,
|
|
||||||
});
|
|
||||||
|
|
||||||
await db
|
await db
|
||||||
.insert(settings)
|
.insert(settings)
|
||||||
.values({ userId, key: SETTINGS_KEYS.aiHealthCache, value: result })
|
.values({ userId, key: SETTINGS_KEYS.aiHealthCache, value: result })
|
||||||
@@ -90,8 +127,111 @@ export async function runHealthCheck(userId: string): Promise<AIHealthResult> {
|
|||||||
target: [settings.userId, settings.key],
|
target: [settings.userId, settings.key],
|
||||||
set: { value: sql`excluded.value` },
|
set: { value: sql`excluded.value` },
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return result;
|
export async function generateTextWithFallback(
|
||||||
|
userId: string,
|
||||||
|
req: GenerateTextRequest,
|
||||||
|
behavior: AIBehaviorConfig
|
||||||
|
): Promise<GenerateTextResponse & { fallbackActive: boolean; effectiveMode: AIMachineMode }> {
|
||||||
|
const providerSettings = await getAiProviderSettings(userId);
|
||||||
|
const requestedMode = providerSettings.machineMode;
|
||||||
|
const config = providerSettings.profiles[requestedMode];
|
||||||
|
const provider = getProvider(config.type);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await provider.generateText(config, req);
|
||||||
|
const cached = await getCachedHealth(userId);
|
||||||
|
if (cached?.fallbackActive) {
|
||||||
|
await cacheHealthResult(userId, {
|
||||||
|
...cached,
|
||||||
|
fallbackActive: false,
|
||||||
|
requestedMode,
|
||||||
|
effectiveMode: requestedMode,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { ...res, fallbackActive: false, effectiveMode: requestedMode };
|
||||||
|
} catch (error) {
|
||||||
|
if (
|
||||||
|
requestedMode === "hyper" &&
|
||||||
|
!behavior.strictMode &&
|
||||||
|
isConnectionError(error)
|
||||||
|
) {
|
||||||
|
const localConfig = providerSettings.profiles.local;
|
||||||
|
const localProvider = getProvider(localConfig.type);
|
||||||
|
const res = await localProvider.generateText(localConfig, req);
|
||||||
|
const cached = await getCachedHealth(userId);
|
||||||
|
await cacheHealthResult(userId, {
|
||||||
|
...(cached ?? {
|
||||||
|
status: "online",
|
||||||
|
provider: localConfig.type,
|
||||||
|
model: localConfig.model,
|
||||||
|
latencyMs: res.latencyMs,
|
||||||
|
lastSuccessAt: new Date().toISOString(),
|
||||||
|
lastFailureAt: null,
|
||||||
|
lastError: null,
|
||||||
|
baseUrlSafe: null,
|
||||||
|
contextLength: null,
|
||||||
|
memoryUsageMb: null,
|
||||||
|
modelsAvailable: [],
|
||||||
|
}),
|
||||||
|
fallbackActive: true,
|
||||||
|
requestedMode: "hyper",
|
||||||
|
effectiveMode: "local",
|
||||||
|
lastError:
|
||||||
|
error instanceof Error
|
||||||
|
? `Hyper machine unreachable: ${error.message}`
|
||||||
|
: "Hyper machine unreachable",
|
||||||
|
});
|
||||||
|
return { ...res, fallbackActive: true, effectiveMode: "local" };
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runHealthCheck(userId: string): Promise<AIHealthCache> {
|
||||||
|
const providerSettings = await getAiProviderSettings(userId);
|
||||||
|
const requestedMode = providerSettings.machineMode;
|
||||||
|
const config = providerSettings.profiles[requestedMode];
|
||||||
|
const provider = getProvider(config.type);
|
||||||
|
const result = await provider.healthCheck(config);
|
||||||
|
|
||||||
|
let cacheResult: AIHealthCache = {
|
||||||
|
...result,
|
||||||
|
fallbackActive: false,
|
||||||
|
requestedMode,
|
||||||
|
effectiveMode: requestedMode,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (requestedMode === "hyper" && result.status === "offline") {
|
||||||
|
const localConfig = providerSettings.profiles.local;
|
||||||
|
const localProvider = getProvider(localConfig.type);
|
||||||
|
const localResult = await localProvider.healthCheck(localConfig);
|
||||||
|
if (localResult.status !== "offline") {
|
||||||
|
cacheResult = {
|
||||||
|
...localResult,
|
||||||
|
fallbackActive: true,
|
||||||
|
requestedMode: "hyper",
|
||||||
|
effectiveMode: "local",
|
||||||
|
lastError: result.lastError ?? "Hyper machine unreachable",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.insert(aiHealthLog).values({
|
||||||
|
userId,
|
||||||
|
providerType: cacheResult.provider,
|
||||||
|
status: cacheResult.status,
|
||||||
|
latencyMs: cacheResult.latencyMs,
|
||||||
|
model: cacheResult.model,
|
||||||
|
contextLength: cacheResult.contextLength,
|
||||||
|
memoryUsageMb: cacheResult.memoryUsageMb,
|
||||||
|
baseUrlSafe: cacheResult.baseUrlSafe,
|
||||||
|
errorMessage: cacheResult.lastError,
|
||||||
|
});
|
||||||
|
|
||||||
|
await cacheHealthResult(userId, cacheResult);
|
||||||
|
return cacheResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getLastHealthLogs(userId: string, limit = 10) {
|
export async function getLastHealthLogs(userId: string, limit = 10) {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import { teacherSchema } from "./ai";
|
|||||||
vi.mock("./ai-config", () => ({
|
vi.mock("./ai-config", () => ({
|
||||||
getAiBehaviorConfig: vi.fn(),
|
getAiBehaviorConfig: vi.fn(),
|
||||||
getAiProviderConfig: vi.fn(),
|
getAiProviderConfig: vi.fn(),
|
||||||
|
getAiProviderSettings: vi.fn(),
|
||||||
|
getProseModel: vi.fn(async () => "test-prose"),
|
||||||
|
generateTextWithFallback: vi.fn(),
|
||||||
buildSystemPrompt: vi.fn(() => "system"),
|
buildSystemPrompt: vi.fn(() => "system"),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -19,7 +22,7 @@ vi.mock("./user", () => ({
|
|||||||
requireUser: vi.fn(async () => ({ user: { id: "user-1", displayName: "Traveler" } })),
|
requireUser: vi.fn(async () => ({ user: { id: "user-1", displayName: "Traveler" } })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { getAiBehaviorConfig, getAiProviderConfig } from "./ai-config";
|
import { getAiBehaviorConfig, getAiProviderConfig, getAiProviderSettings, generateTextWithFallback } from "./ai-config";
|
||||||
import { getProvider } from "../ai/provider-registry";
|
import { getProvider } from "../ai/provider-registry";
|
||||||
|
|
||||||
const onlineAvailability = {
|
const onlineAvailability = {
|
||||||
@@ -40,6 +43,14 @@ const onlineProviderConfig = {
|
|||||||
timeoutMs: 30000,
|
timeoutMs: 30000,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const providerSettings = {
|
||||||
|
machineMode: "local" as const,
|
||||||
|
profiles: {
|
||||||
|
local: onlineProviderConfig,
|
||||||
|
hyper: onlineProviderConfig,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe("teacherSchema", () => {
|
describe("teacherSchema", () => {
|
||||||
it("coerces string quiz answers to numbers", () => {
|
it("coerces string quiz answers to numbers", () => {
|
||||||
const parsed = teacherSchema.parse({
|
const parsed = teacherSchema.parse({
|
||||||
@@ -97,11 +108,8 @@ describe("generateTeacherContent", () => {
|
|||||||
it("returns AI content when provider succeeds", async () => {
|
it("returns AI content when provider succeeds", async () => {
|
||||||
vi.mocked(getAiBehaviorConfig).mockResolvedValue(onlineAvailability);
|
vi.mocked(getAiBehaviorConfig).mockResolvedValue(onlineAvailability);
|
||||||
vi.mocked(getAiProviderConfig).mockResolvedValue(onlineProviderConfig);
|
vi.mocked(getAiProviderConfig).mockResolvedValue(onlineProviderConfig);
|
||||||
vi.mocked(getProvider).mockReturnValue({
|
vi.mocked(getAiProviderSettings).mockResolvedValue(providerSettings);
|
||||||
type: "ollama",
|
vi.mocked(generateTextWithFallback).mockResolvedValue({
|
||||||
healthCheck: vi.fn(async () => ({ status: "online" as const })),
|
|
||||||
listModels: vi.fn(async () => []),
|
|
||||||
generateText: vi.fn(async () => ({
|
|
||||||
text: JSON.stringify({
|
text: JSON.stringify({
|
||||||
overview: "Intro",
|
overview: "Intro",
|
||||||
explanation: "Details here",
|
explanation: "Details here",
|
||||||
@@ -117,7 +125,14 @@ describe("generateTeacherContent", () => {
|
|||||||
}),
|
}),
|
||||||
model: "test",
|
model: "test",
|
||||||
latencyMs: 10,
|
latencyMs: 10,
|
||||||
})),
|
fallbackActive: false,
|
||||||
|
effectiveMode: "local",
|
||||||
|
});
|
||||||
|
vi.mocked(getProvider).mockReturnValue({
|
||||||
|
type: "ollama",
|
||||||
|
healthCheck: vi.fn(async () => ({ status: "online" as const })),
|
||||||
|
listModels: vi.fn(async () => []),
|
||||||
|
generateText: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { generateTeacherContent } = await import("./ai");
|
const { generateTeacherContent } = await import("./ai");
|
||||||
@@ -145,15 +160,19 @@ describe("generateTeacherContent", () => {
|
|||||||
it("returns fallback when AI JSON is invalid and strictMode is off", async () => {
|
it("returns fallback when AI JSON is invalid and strictMode is off", async () => {
|
||||||
vi.mocked(getAiBehaviorConfig).mockResolvedValue(onlineAvailability);
|
vi.mocked(getAiBehaviorConfig).mockResolvedValue(onlineAvailability);
|
||||||
vi.mocked(getAiProviderConfig).mockResolvedValue(onlineProviderConfig);
|
vi.mocked(getAiProviderConfig).mockResolvedValue(onlineProviderConfig);
|
||||||
|
vi.mocked(getAiProviderSettings).mockResolvedValue(providerSettings);
|
||||||
|
vi.mocked(generateTextWithFallback).mockResolvedValue({
|
||||||
|
text: "not valid json",
|
||||||
|
model: "test",
|
||||||
|
latencyMs: 10,
|
||||||
|
fallbackActive: false,
|
||||||
|
effectiveMode: "local",
|
||||||
|
});
|
||||||
vi.mocked(getProvider).mockReturnValue({
|
vi.mocked(getProvider).mockReturnValue({
|
||||||
type: "ollama",
|
type: "ollama",
|
||||||
healthCheck: vi.fn(async () => ({ status: "online" as const })),
|
healthCheck: vi.fn(async () => ({ status: "online" as const })),
|
||||||
listModels: vi.fn(async () => []),
|
listModels: vi.fn(async () => []),
|
||||||
generateText: vi.fn(async () => ({
|
generateText: vi.fn(),
|
||||||
text: "not valid json",
|
|
||||||
model: "test",
|
|
||||||
latencyMs: 10,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const { generateTeacherContent } = await import("./ai");
|
const { generateTeacherContent } = await import("./ai");
|
||||||
|
|||||||
@@ -17,12 +17,14 @@ import { parseAiJson } from "../ai/parse-json";
|
|||||||
import {
|
import {
|
||||||
getAiBehaviorConfig,
|
getAiBehaviorConfig,
|
||||||
getAiProviderConfig,
|
getAiProviderConfig,
|
||||||
|
getAiProviderSettings,
|
||||||
|
getProseModel,
|
||||||
|
generateTextWithFallback,
|
||||||
buildSystemPrompt,
|
buildSystemPrompt,
|
||||||
} from "./ai-config";
|
} from "./ai-config";
|
||||||
import { getTemplateBody } from "./ai-templates";
|
import { getTemplateBody } from "./ai-templates";
|
||||||
import { renderTemplate } from "../ai/prompts/render";
|
import { renderTemplate } from "../ai/prompts/render";
|
||||||
import { requireUser } from "./user";
|
import { requireUser } from "./user";
|
||||||
import { env } from "@/lib/config";
|
|
||||||
|
|
||||||
const questSchema = z.object({
|
const questSchema = z.object({
|
||||||
quests: z
|
quests: z
|
||||||
@@ -143,13 +145,23 @@ export async function getAiAvailability(userId: string) {
|
|||||||
const behavior = await getAiBehaviorConfig(userId);
|
const behavior = await getAiBehaviorConfig(userId);
|
||||||
if (!behavior.enabled) return { canUse: false, online: false };
|
if (!behavior.enabled) return { canUse: false, online: false };
|
||||||
|
|
||||||
const providerConfig = await getAiProviderConfig(userId);
|
const providerSettings = await getAiProviderSettings(userId);
|
||||||
|
const providerConfig = providerSettings.profiles[providerSettings.machineMode];
|
||||||
if (!providerConfig.enabled) return { canUse: false, online: false };
|
if (!providerConfig.enabled) return { canUse: false, online: false };
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(providerConfig.type);
|
const provider = getProvider(providerConfig.type);
|
||||||
const health = await provider.healthCheck(providerConfig);
|
const health = await provider.healthCheck(providerConfig);
|
||||||
return { canUse: true, online: health.status === "online" };
|
if (health.status === "online") {
|
||||||
|
return { canUse: true, online: true };
|
||||||
|
}
|
||||||
|
if (providerSettings.machineMode === "hyper") {
|
||||||
|
const localConfig = providerSettings.profiles.local;
|
||||||
|
const localProvider = getProvider(localConfig.type);
|
||||||
|
const localHealth = await localProvider.healthCheck(localConfig);
|
||||||
|
return { canUse: true, online: localHealth.status === "online" };
|
||||||
|
}
|
||||||
|
return { canUse: true, online: false };
|
||||||
} catch {
|
} catch {
|
||||||
return { canUse: true, online: false };
|
return { canUse: true, online: false };
|
||||||
}
|
}
|
||||||
@@ -183,14 +195,15 @@ async function aiGenerate(
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const provider = getProvider(providerConfig.type);
|
|
||||||
const model =
|
const model =
|
||||||
modelOverride ??
|
modelOverride ??
|
||||||
(templateKey === "weekly_review"
|
(templateKey === "weekly_review"
|
||||||
? env.ollamaModelProse
|
? await getProseModel(userId)
|
||||||
: providerConfig.model);
|
: providerConfig.model);
|
||||||
|
|
||||||
const res = await provider.generateText(providerConfig, {
|
const res = await generateTextWithFallback(
|
||||||
|
userId,
|
||||||
|
{
|
||||||
model,
|
model,
|
||||||
prompt,
|
prompt,
|
||||||
system,
|
system,
|
||||||
@@ -198,7 +211,9 @@ async function aiGenerate(
|
|||||||
maxTokens: providerConfig.maxTokens,
|
maxTokens: providerConfig.maxTokens,
|
||||||
format,
|
format,
|
||||||
timeoutMs: providerConfig.timeoutMs,
|
timeoutMs: providerConfig.timeoutMs,
|
||||||
});
|
},
|
||||||
|
behavior
|
||||||
|
);
|
||||||
return res.text;
|
return res.text;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (behavior.strictMode) throw e;
|
if (behavior.strictMode) throw e;
|
||||||
@@ -381,7 +396,7 @@ export async function generateMentorReview(context: Record<string, unknown>, use
|
|||||||
"system_core",
|
"system_core",
|
||||||
"system_weekly_review",
|
"system_weekly_review",
|
||||||
{ context: JSON.stringify(context) },
|
{ context: JSON.stringify(context) },
|
||||||
env.ollamaModelProse
|
await getProseModel(uid)
|
||||||
);
|
);
|
||||||
if (raw) {
|
if (raw) {
|
||||||
try {
|
try {
|
||||||
@@ -514,8 +529,9 @@ export async function generateChatReply(
|
|||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join("\n");
|
.join("\n");
|
||||||
|
|
||||||
const provider = getProvider(providerConfig.type);
|
const res = await generateTextWithFallback(
|
||||||
const res = await provider.generateText(providerConfig, {
|
userId,
|
||||||
|
{
|
||||||
model: providerConfig.model,
|
model: providerConfig.model,
|
||||||
prompt,
|
prompt,
|
||||||
system,
|
system,
|
||||||
@@ -523,7 +539,9 @@ export async function generateChatReply(
|
|||||||
temperature: behavior.creativity,
|
temperature: behavior.creativity,
|
||||||
maxTokens: Math.min(providerConfig.maxTokens, 800),
|
maxTokens: Math.min(providerConfig.maxTokens, 800),
|
||||||
timeoutMs: providerConfig.timeoutMs,
|
timeoutMs: providerConfig.timeoutMs,
|
||||||
});
|
},
|
||||||
|
behavior
|
||||||
|
);
|
||||||
return res.text.trim() || null;
|
return res.text.trim() || null;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (behavior.strictMode) throw e;
|
if (behavior.strictMode) throw e;
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { z } from "zod";
|
|||||||
import type { MemoryCategory, MemorySourceType } from "@adventureos/shared";
|
import type { MemoryCategory, MemorySourceType } from "@adventureos/shared";
|
||||||
import { SENSITIVE_MEMORY_CATEGORIES } from "@adventureos/shared";
|
import { SENSITIVE_MEMORY_CATEGORIES } from "@adventureos/shared";
|
||||||
import { parseAiJson } from "@/lib/ai/parse-json";
|
import { parseAiJson } from "@/lib/ai/parse-json";
|
||||||
import { getProvider } from "@/lib/ai/provider-registry";
|
import { getAiBehaviorConfig, getAiProviderConfig, generateTextWithFallback, buildSystemPrompt } from "./ai-config";
|
||||||
import { getAiBehaviorConfig, getAiProviderConfig, buildSystemPrompt } from "./ai-config";
|
|
||||||
import { createSuggestion, getMemoryLearningSettings } from "./ai-memory";
|
import { createSuggestion, getMemoryLearningSettings } from "./ai-memory";
|
||||||
|
|
||||||
const candidateSchema = z.object({
|
const candidateSchema = z.object({
|
||||||
@@ -43,8 +42,9 @@ If nothing clear, return { "candidates": [] }.`;
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const coreSystem = await getTemplateBody(userId);
|
const coreSystem = await getTemplateBody(userId);
|
||||||
const provider = getProvider(providerConfig.type);
|
const res = await generateTextWithFallback(
|
||||||
const res = await provider.generateText(providerConfig, {
|
userId,
|
||||||
|
{
|
||||||
model: providerConfig.model,
|
model: providerConfig.model,
|
||||||
prompt,
|
prompt,
|
||||||
system: buildSystemPrompt(behavior, coreSystem),
|
system: buildSystemPrompt(behavior, coreSystem),
|
||||||
@@ -52,7 +52,9 @@ If nothing clear, return { "candidates": [] }.`;
|
|||||||
temperature: 0.3,
|
temperature: 0.3,
|
||||||
maxTokens: 400,
|
maxTokens: 400,
|
||||||
timeoutMs: providerConfig.timeoutMs,
|
timeoutMs: providerConfig.timeoutMs,
|
||||||
});
|
},
|
||||||
|
behavior
|
||||||
|
);
|
||||||
|
|
||||||
const parsed = candidateSchema.safeParse(parseAiJson(res.text));
|
const parsed = candidateSchema.safeParse(parseAiJson(res.text));
|
||||||
if (!parsed.success) return [];
|
if (!parsed.success) return [];
|
||||||
|
|||||||
Reference in New Issue
Block a user