This commit is contained in:
@@ -58,6 +58,24 @@ export interface AIProviderConfig {
|
||||
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 {
|
||||
personality: "supportive_mentor" | "wise_teacher" | "quiet_observer" | "academic_tutor" | "friendly_coach";
|
||||
verbosity: "minimal" | "balanced" | "detailed";
|
||||
@@ -88,6 +106,64 @@ export const DEFAULT_AI_PROVIDER: AIProviderConfig = {
|
||||
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 {
|
||||
try {
|
||||
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";
|
||||
},
|
||||
|
||||
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 {
|
||||
return process.env.OPENAI_API_KEY;
|
||||
},
|
||||
|
||||
@@ -2,14 +2,21 @@ import { eq, and, desc } from "drizzle-orm";
|
||||
import { db, settings, aiHealthLog } from "../db";
|
||||
import {
|
||||
DEFAULT_AI_BEHAVIOR,
|
||||
DEFAULT_AI_PROVIDER,
|
||||
DEFAULT_AI_PROVIDER_SETTINGS,
|
||||
migrateProviderSettings,
|
||||
type AIBehaviorConfig,
|
||||
type AIProviderConfig,
|
||||
type AIProviderSettings,
|
||||
type AIHealthResult,
|
||||
type AIHealthCache,
|
||||
type AIMachineMode,
|
||||
type GenerateTextRequest,
|
||||
type GenerateTextResponse,
|
||||
} from "../ai/types";
|
||||
import { getProvider } from "../ai/provider-registry";
|
||||
import { isAiTimeoutError } from "../ai/ai-normalize";
|
||||
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> {
|
||||
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>) };
|
||||
}
|
||||
|
||||
export async function getAiProviderConfig(userId: string): Promise<AIProviderConfig> {
|
||||
export async function getAiProviderSettings(userId: string): Promise<AIProviderSettings> {
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(settings)
|
||||
.where(and(eq(settings.userId, userId), eq(settings.key, SETTINGS_KEYS.aiProvider)));
|
||||
if (!row?.value) return DEFAULT_AI_PROVIDER;
|
||||
return { ...DEFAULT_AI_PROVIDER, ...(row.value as Partial<AIProviderConfig>) };
|
||||
if (!row?.value) return DEFAULT_AI_PROVIDER_SETTINGS;
|
||||
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>) {
|
||||
@@ -43,9 +63,18 @@ export async function saveAiBehaviorConfig(userId: string, config: Partial<AIBeh
|
||||
return merged;
|
||||
}
|
||||
|
||||
export async function saveAiProviderConfig(userId: string, config: Partial<AIProviderConfig>) {
|
||||
const current = await getAiProviderConfig(userId);
|
||||
const merged = { ...current, ...config };
|
||||
export async function saveAiProviderSettings(
|
||||
userId: string,
|
||||
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");
|
||||
await db
|
||||
.insert(settings)
|
||||
@@ -57,32 +86,40 @@ export async function saveAiProviderConfig(userId: string, config: Partial<AIPro
|
||||
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
|
||||
.select()
|
||||
.from(settings)
|
||||
.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> {
|
||||
const config = await getAiProviderConfig(userId);
|
||||
const provider = getProvider(config.type);
|
||||
const result = await provider.healthCheck(config);
|
||||
export function isConnectionError(error: unknown): boolean {
|
||||
if (!(error instanceof Error)) return false;
|
||||
if (isAiTimeoutError(error)) return true;
|
||||
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");
|
||||
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
|
||||
.insert(settings)
|
||||
.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],
|
||||
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) {
|
||||
|
||||
@@ -4,6 +4,9 @@ import { teacherSchema } from "./ai";
|
||||
vi.mock("./ai-config", () => ({
|
||||
getAiBehaviorConfig: vi.fn(),
|
||||
getAiProviderConfig: vi.fn(),
|
||||
getAiProviderSettings: vi.fn(),
|
||||
getProseModel: vi.fn(async () => "test-prose"),
|
||||
generateTextWithFallback: vi.fn(),
|
||||
buildSystemPrompt: vi.fn(() => "system"),
|
||||
}));
|
||||
|
||||
@@ -19,7 +22,7 @@ vi.mock("./user", () => ({
|
||||
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";
|
||||
|
||||
const onlineAvailability = {
|
||||
@@ -40,6 +43,14 @@ const onlineProviderConfig = {
|
||||
timeoutMs: 30000,
|
||||
};
|
||||
|
||||
const providerSettings = {
|
||||
machineMode: "local" as const,
|
||||
profiles: {
|
||||
local: onlineProviderConfig,
|
||||
hyper: onlineProviderConfig,
|
||||
},
|
||||
};
|
||||
|
||||
describe("teacherSchema", () => {
|
||||
it("coerces string quiz answers to numbers", () => {
|
||||
const parsed = teacherSchema.parse({
|
||||
@@ -97,27 +108,31 @@ describe("generateTeacherContent", () => {
|
||||
it("returns AI content when provider succeeds", async () => {
|
||||
vi.mocked(getAiBehaviorConfig).mockResolvedValue(onlineAvailability);
|
||||
vi.mocked(getAiProviderConfig).mockResolvedValue(onlineProviderConfig);
|
||||
vi.mocked(getAiProviderSettings).mockResolvedValue(providerSettings);
|
||||
vi.mocked(generateTextWithFallback).mockResolvedValue({
|
||||
text: JSON.stringify({
|
||||
overview: "Intro",
|
||||
explanation: "Details here",
|
||||
flashcards: [{ front: "What is Rust?", back: "A systems language" }],
|
||||
quiz: [
|
||||
{
|
||||
question: "Rust is?",
|
||||
options: ["Fast", "Slow"],
|
||||
answer: 0,
|
||||
},
|
||||
],
|
||||
researchAssignment: { steps: ["Read chapter 1"], expectedOutcome: "Notes" },
|
||||
}),
|
||||
model: "test",
|
||||
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(async () => ({
|
||||
text: JSON.stringify({
|
||||
overview: "Intro",
|
||||
explanation: "Details here",
|
||||
flashcards: [{ front: "What is Rust?", back: "A systems language" }],
|
||||
quiz: [
|
||||
{
|
||||
question: "Rust is?",
|
||||
options: ["Fast", "Slow"],
|
||||
answer: 0,
|
||||
},
|
||||
],
|
||||
researchAssignment: { steps: ["Read chapter 1"], expectedOutcome: "Notes" },
|
||||
}),
|
||||
model: "test",
|
||||
latencyMs: 10,
|
||||
})),
|
||||
generateText: vi.fn(),
|
||||
});
|
||||
|
||||
const { generateTeacherContent } = await import("./ai");
|
||||
@@ -145,15 +160,19 @@ describe("generateTeacherContent", () => {
|
||||
it("returns fallback when AI JSON is invalid and strictMode is off", async () => {
|
||||
vi.mocked(getAiBehaviorConfig).mockResolvedValue(onlineAvailability);
|
||||
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({
|
||||
type: "ollama",
|
||||
healthCheck: vi.fn(async () => ({ status: "online" as const })),
|
||||
listModels: vi.fn(async () => []),
|
||||
generateText: vi.fn(async () => ({
|
||||
text: "not valid json",
|
||||
model: "test",
|
||||
latencyMs: 10,
|
||||
})),
|
||||
generateText: vi.fn(),
|
||||
});
|
||||
|
||||
const { generateTeacherContent } = await import("./ai");
|
||||
|
||||
@@ -17,12 +17,14 @@ import { parseAiJson } from "../ai/parse-json";
|
||||
import {
|
||||
getAiBehaviorConfig,
|
||||
getAiProviderConfig,
|
||||
getAiProviderSettings,
|
||||
getProseModel,
|
||||
generateTextWithFallback,
|
||||
buildSystemPrompt,
|
||||
} from "./ai-config";
|
||||
import { getTemplateBody } from "./ai-templates";
|
||||
import { renderTemplate } from "../ai/prompts/render";
|
||||
import { requireUser } from "./user";
|
||||
import { env } from "@/lib/config";
|
||||
|
||||
const questSchema = z.object({
|
||||
quests: z
|
||||
@@ -143,13 +145,23 @@ export async function getAiAvailability(userId: string) {
|
||||
const behavior = await getAiBehaviorConfig(userId);
|
||||
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 };
|
||||
|
||||
try {
|
||||
const provider = getProvider(providerConfig.type);
|
||||
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 {
|
||||
return { canUse: true, online: false };
|
||||
}
|
||||
@@ -183,22 +195,25 @@ async function aiGenerate(
|
||||
});
|
||||
|
||||
try {
|
||||
const provider = getProvider(providerConfig.type);
|
||||
const model =
|
||||
modelOverride ??
|
||||
(templateKey === "weekly_review"
|
||||
? env.ollamaModelProse
|
||||
? await getProseModel(userId)
|
||||
: providerConfig.model);
|
||||
|
||||
const res = await provider.generateText(providerConfig, {
|
||||
model,
|
||||
prompt,
|
||||
system,
|
||||
temperature: behavior.creativity,
|
||||
maxTokens: providerConfig.maxTokens,
|
||||
format,
|
||||
timeoutMs: providerConfig.timeoutMs,
|
||||
});
|
||||
const res = await generateTextWithFallback(
|
||||
userId,
|
||||
{
|
||||
model,
|
||||
prompt,
|
||||
system,
|
||||
temperature: behavior.creativity,
|
||||
maxTokens: providerConfig.maxTokens,
|
||||
format,
|
||||
timeoutMs: providerConfig.timeoutMs,
|
||||
},
|
||||
behavior
|
||||
);
|
||||
return res.text;
|
||||
} catch (e) {
|
||||
if (behavior.strictMode) throw e;
|
||||
@@ -381,7 +396,7 @@ export async function generateMentorReview(context: Record<string, unknown>, use
|
||||
"system_core",
|
||||
"system_weekly_review",
|
||||
{ context: JSON.stringify(context) },
|
||||
env.ollamaModelProse
|
||||
await getProseModel(uid)
|
||||
);
|
||||
if (raw) {
|
||||
try {
|
||||
@@ -514,16 +529,19 @@ export async function generateChatReply(
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
|
||||
const provider = getProvider(providerConfig.type);
|
||||
const res = await provider.generateText(providerConfig, {
|
||||
model: providerConfig.model,
|
||||
prompt,
|
||||
system,
|
||||
format: "text",
|
||||
temperature: behavior.creativity,
|
||||
maxTokens: Math.min(providerConfig.maxTokens, 800),
|
||||
timeoutMs: providerConfig.timeoutMs,
|
||||
});
|
||||
const res = await generateTextWithFallback(
|
||||
userId,
|
||||
{
|
||||
model: providerConfig.model,
|
||||
prompt,
|
||||
system,
|
||||
format: "text",
|
||||
temperature: behavior.creativity,
|
||||
maxTokens: Math.min(providerConfig.maxTokens, 800),
|
||||
timeoutMs: providerConfig.timeoutMs,
|
||||
},
|
||||
behavior
|
||||
);
|
||||
return res.text.trim() || null;
|
||||
} catch (e) {
|
||||
if (behavior.strictMode) throw e;
|
||||
|
||||
@@ -2,8 +2,7 @@ import { z } from "zod";
|
||||
import type { MemoryCategory, MemorySourceType } from "@adventureos/shared";
|
||||
import { SENSITIVE_MEMORY_CATEGORIES } from "@adventureos/shared";
|
||||
import { parseAiJson } from "@/lib/ai/parse-json";
|
||||
import { getProvider } from "@/lib/ai/provider-registry";
|
||||
import { getAiBehaviorConfig, getAiProviderConfig, buildSystemPrompt } from "./ai-config";
|
||||
import { getAiBehaviorConfig, getAiProviderConfig, generateTextWithFallback, buildSystemPrompt } from "./ai-config";
|
||||
import { createSuggestion, getMemoryLearningSettings } from "./ai-memory";
|
||||
|
||||
const candidateSchema = z.object({
|
||||
@@ -43,16 +42,19 @@ If nothing clear, return { "candidates": [] }.`;
|
||||
|
||||
try {
|
||||
const coreSystem = await getTemplateBody(userId);
|
||||
const provider = getProvider(providerConfig.type);
|
||||
const res = await provider.generateText(providerConfig, {
|
||||
model: providerConfig.model,
|
||||
prompt,
|
||||
system: buildSystemPrompt(behavior, coreSystem),
|
||||
format: "json",
|
||||
temperature: 0.3,
|
||||
maxTokens: 400,
|
||||
timeoutMs: providerConfig.timeoutMs,
|
||||
});
|
||||
const res = await generateTextWithFallback(
|
||||
userId,
|
||||
{
|
||||
model: providerConfig.model,
|
||||
prompt,
|
||||
system: buildSystemPrompt(behavior, coreSystem),
|
||||
format: "json",
|
||||
temperature: 0.3,
|
||||
maxTokens: 400,
|
||||
timeoutMs: providerConfig.timeoutMs,
|
||||
},
|
||||
behavior
|
||||
);
|
||||
|
||||
const parsed = candidateSchema.safeParse(parseAiJson(res.text));
|
||||
if (!parsed.success) return [];
|
||||
|
||||
Reference in New Issue
Block a user