diff --git a/apps/gui/src/app/config/page.tsx b/apps/gui/src/app/config/page.tsx index bc9e1a4..6c743f3 100644 --- a/apps/gui/src/app/config/page.tsx +++ b/apps/gui/src/app/config/page.tsx @@ -1,7 +1,16 @@ "use client"; -import { useEffect, useState } from "react"; -import { getConfigStatus } from "@/app/play/actions"; +import { useEffect, useState, useCallback } from "react"; +import { + getConfigStatus, + listProviderInstances, + createProviderInstance, + deleteProviderInstance, + setActiveProviderInstance, + getProviderMappings, + setProviderMapping, +} from "@/app/play/actions"; +import type { LLMProviderInstance } from "@/lib/provider-manager"; interface ConfigStatus { apiKeySet: boolean; @@ -12,37 +21,106 @@ interface ConfigStatus { export default function ConfigPage() { const [config, setConfig] = useState(null); + const [instances, setInstances] = useState([]); + const [mappings, setMappings] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); - useEffect(() => { - let cancelled = false; + const [newName, setNewName] = useState(""); + const [newProvider, setNewProvider] = useState("google-genai"); + const [newKey, setNewKey] = useState(""); - async function load() { - setLoading(true); - setError(""); - try { - const result = await getConfigStatus(); - if (!cancelled) { - setConfig(result); - } - } catch (err) { - if (!cancelled) { - setError(err instanceof Error ? err.message : String(err)); - } - } finally { - if (!cancelled) { - setLoading(false); - } - } + const loadInstances = useCallback(async () => { + try { + const list = await listProviderInstances(); + setInstances(list); + } catch { + // ignore } - - load(); - return () => { - cancelled = true; - }; }, []); + const loadMappings = useCallback(async () => { + try { + const maps = await getProviderMappings(); + setMappings(maps); + } catch { + // ignore + } + }, []); + + const loadAll = useCallback(async () => { + setLoading(true); + setError(""); + try { + const result = await getConfigStatus(); + setConfig(result); + await loadInstances(); + await loadMappings(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }, [loadInstances, loadMappings]); + + useEffect(() => { + loadAll(); + }, [loadAll]); + + const handleAddInstance = async (e: React.FormEvent) => { + e.preventDefault(); + if (!newName.trim() || !newKey.trim()) return; + try { + setLoading(true); + await createProviderInstance(newName, newProvider, newKey); + setNewName(""); + setNewKey(""); + await loadInstances(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }; + + const handleDeleteInstance = async (id: string) => { + if (!confirm("Are you sure you want to delete this provider instance?")) return; + try { + setLoading(true); + await deleteProviderInstance(id); + await loadInstances(); + await loadMappings(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }; + + const handleSetActiveInstance = async (id: string) => { + try { + setLoading(true); + await setActiveProviderInstance(id); + await loadInstances(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }; + + const handleUpdateMapping = async (task: string, providerInstanceId: string) => { + try { + setLoading(true); + await setProviderMapping(task, providerInstanceId); + await loadMappings(); + } catch (err) { + setError(err instanceof Error ? err.message : String(err)); + } finally { + setLoading(false); + } + }; + return (

Configuration

@@ -53,19 +131,145 @@ export default function ConfigPage() { {config && !loading && ( <>
-

LLM Provider

-
- Provider - Google Gemini +

LLM Provider Instances

+
+ {instances.length === 0 ? ( +
+ No custom LLM provider instances configured. Defaulting to the environment GOOGLE_API_KEY if present. +
+ ) : ( + + + + + + + + + + + + {instances.map((inst) => ( + + + + + + + + ))} + +
Friendly NameProvider TypeAPI Key PreviewStatusActions
{inst.name}{inst.providerName} + + {inst.apiKey.substring(0, 10)}...{inst.apiKey.substring(inst.apiKey.length - 4)} + + + {inst.isActive ? ( + Active + ) : ( + Inactive + )} + +
+ {!inst.isActive && ( + + )} + +
+
+ )}
+ +
+

Add LLM Provider Instance

+
+
+ + setNewName(e.target.value)} + placeholder="e.g. Gemini - Production" + required + /> +
+
+ + +
+
+ + setNewKey(e.target.value)} + placeholder="AIzaSy..." + required + /> +
+ +
+
+
+ +
+

Task Provider Routing

+

+ Configure which LLM Provider Key Instance should handle each specific simulation task. Mappings default to the currently Active instance if not specified. +

+
+ {[ + { key: "actor-prose", label: "Actor Prose Generation", desc: "Generates roleplay/narrative prose for Non-Player Characters." }, + { key: "llm-validator", label: "LLM Validator", desc: "Arbitrates and validates proposed actions against the world state rules." }, + { key: "intent-decoder", label: "Intent Decoder", desc: "Splits raw prose actions into structured intents (Player and NPC)." }, + { key: "timedelta", label: "TimeDelta Generator", desc: "Calculates the duration of character actions to advance the game clock." }, + ].map((task) => ( +
+
+ {task.label} + {task.desc} +
+ +
+ ))} +
+
+ +
+

Environment Variables Default

- Model + Default Model {config.model}
- API Key + Default API Key (.env)
- {!config.apiKeySet && ( -
- Add GOOGLE_API_KEY=your_key to the{" "} - .env file in the project root, then restart the - server. -
- )}

Available Scenarios

{config.availableScenarios.length === 0 ? (

- No scenarios found in{" "} - content/demo/scenarios/. + No scenarios found in content/demo/scenarios/.

) : ( @@ -215,6 +411,142 @@ export default function ConfigPage() { font-family: monospace; font-size: 0.875rem; } + + /* Pill and List Styles */ + .status-pill { + display: inline-block; + padding: 0.125rem 0.5rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 600; + } + .status-pill.active { + background: #dcfce7; + color: #15803d; + } + .status-pill.inactive { + background: #f3f4f6; + color: #4b5563; + } + .action-buttons { + display: flex; + gap: 0.5rem; + align-items: center; + } + .btn-sm { + padding: 0.25rem 0.5rem; + font-size: 0.75rem; + background: #3b82f6; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + } + .btn-sm:hover { + background: #2563eb; + } + .btn-sm.delete-btn { + background: #ef4444; + } + .btn-sm.delete-btn:hover { + background: #dc2626; + } + .add-instance-form { + margin-top: 1.5rem; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 1.25rem; + } + .add-instance-form h3 { + font-size: 0.95rem; + margin-bottom: 0.75rem; + color: #111; + } + .form-fields { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + align-items: flex-end; + } + @media (min-width: 768px) { + .form-fields { + grid-template-columns: 1fr 1fr 1.5fr auto; + } + } + .form-fields .field { + display: flex; + flex-direction: column; + gap: 0.25rem; + } + .form-fields .field label { + font-size: 0.75rem; + font-weight: 500; + } + .form-fields .field input, + .form-fields .field select { + padding: 0.375rem 0.5rem; + font-size: 0.8125rem; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + } + .btn-add { + padding: 0.375rem 1rem; + font-size: 0.8125rem; + background: #10b981; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + } + .btn-add:hover { + background: #059669; + } + + /* Task Provider Routing Styles */ + .mappings-grid { + display: grid; + grid-template-columns: 1fr; + gap: 1rem; + margin-top: 1rem; + } + @media (min-width: 768px) { + .mappings-grid { + grid-template-columns: 1fr 1fr; + } + } + .mapping-card { + border: 1px solid #e5e7eb; + background: #f9fafb; + border-radius: 8px; + padding: 1rem; + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 0.75rem; + } + .mapping-info { + display: flex; + flex-direction: column; + gap: 0.25rem; + font-size: 0.8125rem; + } + .mapping-info strong { + font-size: 0.875rem; + color: #111; + } + .mapping-card select { + padding: 0.375rem 0.5rem; + font-size: 0.8125rem; + border: 1px solid #ccc; + border-radius: 4px; + background: #fff; + width: 100%; + } + .text-gray { + color: #6b7280; + } `} ); diff --git a/apps/gui/src/app/play/actions.ts b/apps/gui/src/app/play/actions.ts index c0a0a7a..b200722 100644 --- a/apps/gui/src/app/play/actions.ts +++ b/apps/gui/src/app/play/actions.ts @@ -4,6 +4,8 @@ import path from "path"; import fs from "fs"; import { simulationManager } from "@/lib/simulation"; import type { SimSnapshot } from "@/lib/simulation"; +import { ProviderManager } from "@/lib/provider-manager"; +import type { LLMProviderInstance } from "@/lib/provider-manager"; function resolveScenarioPath(relative: string): string { const cwd = process.cwd(); @@ -30,6 +32,7 @@ type ActionResult = export async function startSimulation(input: { scenario?: string; playEntity?: string; + providerInstanceId?: string; }): Promise { try { const scenarioFile = @@ -43,6 +46,7 @@ export async function startSimulation(input: { const snapshot = await simulationManager.create( resolved, input.playEntity || undefined, + input.providerInstanceId, ); if (snapshot.status === "error") { @@ -229,3 +233,34 @@ export async function deleteSimulation(simId: string): Promise< }; } } + +export async function listProviderInstances(): Promise { + return ProviderManager.list(); +} + +export async function createProviderInstance( + name: string, + providerName: string, + apiKey: string, +): Promise { + return ProviderManager.create(name, providerName, apiKey); +} + +export async function deleteProviderInstance(id: string): Promise { + ProviderManager.delete(id); +} + +export async function setActiveProviderInstance(id: string): Promise { + ProviderManager.setActive(id); +} + +export async function getProviderMappings(): Promise> { + return ProviderManager.getMappings(); +} + +export async function setProviderMapping( + task: string, + providerInstanceId: string, +): Promise { + ProviderManager.setMapping(task, providerInstanceId); +} diff --git a/apps/gui/src/components/play/PlayView.tsx b/apps/gui/src/components/play/PlayView.tsx index 6563892..55380ce 100644 --- a/apps/gui/src/components/play/PlayView.tsx +++ b/apps/gui/src/components/play/PlayView.tsx @@ -10,8 +10,10 @@ import { getConfigStatus, getScenarioEntities, deleteSimulation, + listProviderInstances, } from "@/app/play/actions"; import type { SimSnapshot } from "@/lib/simulation-types"; +import type { LLMProviderInstance } from "@/lib/provider-manager"; function IntentTag({ intent, @@ -304,9 +306,12 @@ export function PlayView() { const [availableEntities, setAvailableEntities] = useState<{ id: string; name: string }[]>([]); const [selectedEntity, setSelectedEntity] = useState(""); - // Load scenarios on mount + const [providerInstances, setProviderInstances] = useState([]); + const [selectedProviderInstance, setSelectedProviderInstance] = useState(""); + + // Load scenarios and provider instances on mount useEffect(() => { - async function loadScenarios() { + async function loadScenariosAndProviders() { try { const configStatus = await getConfigStatus(); setScenarios(configStatus.availableScenarios); @@ -316,9 +321,21 @@ export function PlayView() { } catch { // ignore } + try { + const providersList = await listProviderInstances(); + setProviderInstances(providersList); + const active = providersList.find(p => p.isActive); + if (active) { + setSelectedProviderInstance(active.id); + } else if (providersList.length > 0) { + setSelectedProviderInstance(providersList[0].id); + } + } catch { + // ignore + } } - loadScenarios(); - }, []); + loadScenariosAndProviders(); + }, [snapshot]); // Fetch entities when selectedScenario changes useEffect(() => { @@ -355,6 +372,7 @@ export function PlayView() { const result = await startSimulation({ scenario: (form.get("scenario") as string) || undefined, playEntity: (form.get("playEntity") as string) || undefined, + providerInstanceId: selectedProviderInstance || undefined, }); if (!result.ok) { @@ -473,6 +491,25 @@ export function PlayView() { ))} +
+ + +
diff --git a/apps/gui/src/lib/provider-manager.ts b/apps/gui/src/lib/provider-manager.ts new file mode 100644 index 0000000..b81b482 --- /dev/null +++ b/apps/gui/src/lib/provider-manager.ts @@ -0,0 +1,165 @@ +import Database from "better-sqlite3"; +import path from "path"; +import fs from "fs"; +import type { LLMProviderInstance } from "@omnia/llm"; + +export type { LLMProviderInstance }; + +function getSettingsDb() { + const dbDir = path.resolve(process.cwd(), "data"); + if (!fs.existsSync(dbDir)) { + fs.mkdirSync(dbDir, { recursive: true }); + } + const dbPath = path.join(dbDir, "settings.db"); + const db = new Database(dbPath); + + db.prepare(` + CREATE TABLE IF NOT EXISTS provider_instances ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + providerName TEXT NOT NULL, + apiKey TEXT NOT NULL, + isActive INTEGER NOT NULL DEFAULT 0 + ) + `).run(); + + return db; +} + +export class ProviderManager { + static list(): LLMProviderInstance[] { + const db = getSettingsDb(); + try { + const rows = db.prepare(`SELECT * FROM provider_instances`).all() as { + id: string; + name: string; + providerName: string; + apiKey: string; + isActive: number; + }[]; + return rows.map((r) => ({ + id: r.id, + name: r.name, + providerName: r.providerName, + apiKey: r.apiKey, + isActive: r.isActive === 1, + })); + } finally { + db.close(); + } + } + + static create(name: string, providerName: string, apiKey: string): LLMProviderInstance { + const db = getSettingsDb(); + try { + const id = "provider-" + Date.now(); + const activeCount = db.prepare(`SELECT COUNT(*) as count FROM provider_instances WHERE isActive = 1`).get() as { count: number }; + const isActive = activeCount.count === 0 ? 1 : 0; + + db.prepare(` + INSERT INTO provider_instances (id, name, providerName, apiKey, isActive) + VALUES (?, ?, ?, ?, ?) + `).run(id, name, providerName, apiKey, isActive); + + return { id, name, providerName, apiKey, isActive: isActive === 1 }; + } finally { + db.close(); + } + } + + static delete(id: string): void { + const db = getSettingsDb(); + try { + const provider = db.prepare(`SELECT isActive FROM provider_instances WHERE id = ?`).get(id) as { isActive: number } | undefined; + db.prepare(`DELETE FROM provider_instances WHERE id = ?`).run(id); + + if (provider && provider.isActive === 1) { + const next = db.prepare(`SELECT id FROM provider_instances LIMIT 1`).get() as { id: string } | undefined; + if (next) { + db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(next.id); + } + } + } finally { + db.close(); + } + } + + static setActive(id: string): void { + const db = getSettingsDb(); + try { + db.prepare(`UPDATE provider_instances SET isActive = 0`).run(); + db.prepare(`UPDATE provider_instances SET isActive = 1 WHERE id = ?`).run(id); + } finally { + db.close(); + } + } + + static getActive(): LLMProviderInstance | null { + const db = getSettingsDb(); + try { + const row = db.prepare(`SELECT * FROM provider_instances WHERE isActive = 1`).get() as { + id: string; + name: string; + providerName: string; + apiKey: string; + isActive: number; + } | undefined; + if (!row) return null; + return { + id: row.id, + name: row.name, + providerName: row.providerName, + apiKey: row.apiKey, + isActive: true, + }; + } finally { + db.close(); + } + } + + static getMappings(): Record { + const db = getSettingsDb(); + try { + db.prepare(` + CREATE TABLE IF NOT EXISTS provider_mappings ( + task TEXT PRIMARY KEY, + providerInstanceId TEXT NOT NULL + ) + `).run(); + const rows = db.prepare(`SELECT * FROM provider_mappings`).all() as { + task: string; + providerInstanceId: string; + }[]; + const mappings: Record = {}; + for (const row of rows) { + mappings[row.task] = row.providerInstanceId; + } + return mappings; + } finally { + db.close(); + } + } + + static setMapping(task: string, providerInstanceId: string): void { + const db = getSettingsDb(); + try { + db.prepare(` + CREATE TABLE IF NOT EXISTS provider_mappings ( + task TEXT PRIMARY KEY, + providerInstanceId TEXT NOT NULL + ) + `).run(); + if (!providerInstanceId) { + db.prepare(`DELETE FROM provider_mappings WHERE task = ?`).run(task); + } else { + db.prepare(` + INSERT INTO provider_mappings (task, providerInstanceId) + VALUES (?, ?) + ON CONFLICT(task) DO UPDATE SET providerInstanceId = excluded.providerInstanceId + `).run(task, providerInstanceId); + } + } finally { + db.close(); + } + } +} diff --git a/apps/gui/src/lib/simulation.ts b/apps/gui/src/lib/simulation.ts index 0218dd8..98ae128 100644 --- a/apps/gui/src/lib/simulation.ts +++ b/apps/gui/src/lib/simulation.ts @@ -25,8 +25,9 @@ import { IActorProseGenerator, buildBufferEntryForIntent, } from "@omnia/actor"; -import { GeminiProvider } from "@omnia/llm"; +import { GeminiProvider, ILLMProvider, MockLLMProvider } from "@omnia/llm"; import { ScenarioLoader } from "@omnia/scenario"; +import { ProviderManager } from "./provider-manager"; import type { IntentInfo, @@ -66,6 +67,7 @@ interface SavedState { waitingEntity?: WaitingContext; aliasDoneForTurn: boolean; log: LogEntry[]; + providerMappings: Record; } function loadSessionState(db: Database.Database, id: string): SavedState | null { @@ -96,7 +98,10 @@ interface SimSession { entities: EntityInfo[]; playerEntityId: string | undefined; entityIndex: number; - llmProvider: GeminiProvider; + actorProvider: ILLMProvider; + validatorProvider: ILLMProvider; + decoderProvider: ILLMProvider; + timedeltaProvider: ILLMProvider; architect: Architect; aliasGenerator: AliasDeltaGenerator; log: LogEntry[]; @@ -104,6 +109,7 @@ interface SimSession { error?: string; waitingEntity?: WaitingContext; aliasDoneForTurn: boolean; + providerMappings: Record; } class SimulationManager { @@ -112,9 +118,20 @@ class SimulationManager { async create( scenarioPath: string, playEntityName?: string, + providerInstanceId?: string, ): Promise { - const apiKey = process.env.GOOGLE_API_KEY; - if (!apiKey) { + let activeInstance = providerInstanceId + ? ProviderManager.list().find((p) => p.id === providerInstanceId) + : ProviderManager.getActive(); + + if (!activeInstance) { + const envKey = process.env.GOOGLE_API_KEY; + if (envKey) { + activeInstance = ProviderManager.create("Default (Env)", "google-genai", envKey); + } + } + + if (!activeInstance) { return { id: "", status: "error", @@ -125,7 +142,7 @@ class SimulationManager { entities: [], log: [], entityIndex: 0, - error: "GOOGLE_API_KEY is not set. Add it to your .env file.", + error: "No active LLM Provider Instance found. Please configure a key in Settings first.", }; } @@ -202,9 +219,37 @@ class SimulationManager { } } - const llmProvider = new GeminiProvider(apiKey); - const architect = new Architect(llmProvider, coreRepo); - const aliasGenerator = new AliasDeltaGenerator(llmProvider); + const list = ProviderManager.list(); + const active = ProviderManager.getActive() || activeInstance; + const mappings = ProviderManager.getMappings(); + + const resolveProviderForTask = (task: string): ILLMProvider => { + const mappedId = mappings[task]; + let inst = mappedId ? list.find((p) => p.id === mappedId) : null; + if (!inst) { + inst = active; + } + + const key = inst ? inst.apiKey : (process.env.GOOGLE_API_KEY || ""); + const providerName = inst ? inst.providerName : "google-genai"; + + if (providerName === "google-genai") { + return new GeminiProvider(key); + } else { + return new MockLLMProvider([]); + } + }; + + const actorProvider = resolveProviderForTask("actor-prose"); + const validatorProvider = resolveProviderForTask("llm-validator"); + const decoderProvider = resolveProviderForTask("intent-decoder"); + const timedeltaProvider = resolveProviderForTask("timedelta"); + + const architect = new Architect( + { validator: validatorProvider, timedelta: timedeltaProvider }, + coreRepo, + ); + const aliasGenerator = new AliasDeltaGenerator(actorProvider); const session: SimSession = { db, @@ -219,12 +264,16 @@ class SimulationManager { entities: entityInfos, playerEntityId, entityIndex: 0, - llmProvider, + actorProvider, + validatorProvider, + decoderProvider, + timedeltaProvider, architect, aliasGenerator, log: [], status: "running", aliasDoneForTurn: false, + providerMappings: mappings, }; this.sessions.set(id, session); @@ -300,15 +349,13 @@ class SimulationManager { if (!entity) throw new Error(`Player entity "${ctx.entityId}" not found`); const playerActor = new ActorAgent( - session.llmProvider, + { actor: session.actorProvider, decoder: session.decoderProvider }, session.bufferRepo, 20, new FixedProseGenerator(prose), ); - const startCallIdx = session.llmProvider.lastCalls.length; const result = await playerActor.act(worldState, entity); - const endCallIdx = session.llmProvider.lastCalls.length; const entry: LogEntry = { turn: session.turn, @@ -323,8 +370,8 @@ class SimulationManager { }, }; - if (endCallIdx > startCallIdx) { - const call = session.llmProvider.lastCalls[startCallIdx]; + if (session.decoderProvider.lastCalls && session.decoderProvider.lastCalls.length > 0) { + const call = session.decoderProvider.lastCalls[session.decoderProvider.lastCalls.length - 1]; entry.decoderPrompt = { systemPrompt: call.systemPrompt, userContext: call.userContext, @@ -441,10 +488,12 @@ class SimulationManager { const entity = worldState.getEntity(info.id); if (!entity) throw new Error(`Entity "${info.id}" not found`); - const actor = new ActorAgent(session.llmProvider, session.bufferRepo, 20); - const startCallIdx = session.llmProvider.lastCalls.length; + const actor = new ActorAgent( + { actor: session.actorProvider, decoder: session.decoderProvider }, + session.bufferRepo, + 20, + ); const result = await actor.act(worldState, entity); - const endCallIdx = session.llmProvider.lastCalls.length; const entry: LogEntry = { turn: session.turn, @@ -455,28 +504,22 @@ class SimulationManager { timestamp: worldState.clock.get().toISOString(), }; - if (endCallIdx - startCallIdx >= 2) { - const actorCall = session.llmProvider.lastCalls[startCallIdx]; - const decoderCall = session.llmProvider.lastCalls[startCallIdx + 1]; - + if (session.actorProvider.lastCalls && session.actorProvider.lastCalls.length > 0) { + const actorCall = session.actorProvider.lastCalls[session.actorProvider.lastCalls.length - 1]; entry.rawPrompt = { systemPrompt: actorCall.systemPrompt, userContext: actorCall.userContext, }; entry.usage = actorCall.usage; + } + if (session.decoderProvider.lastCalls && session.decoderProvider.lastCalls.length > 0) { + const decoderCall = session.decoderProvider.lastCalls[session.decoderProvider.lastCalls.length - 1]; entry.decoderPrompt = { systemPrompt: decoderCall.systemPrompt, userContext: decoderCall.userContext, }; entry.decoderUsage = decoderCall.usage; - } else if (endCallIdx - startCallIdx === 1) { - const call = session.llmProvider.lastCalls[startCallIdx]; - entry.rawPrompt = { - systemPrompt: call.systemPrompt, - userContext: call.userContext, - }; - entry.usage = call.usage; } for (const intent of result.intents.intents) { @@ -604,17 +647,47 @@ class SimulationManager { return null; } - const apiKey = process.env.GOOGLE_API_KEY; - if (!apiKey) { - db.close(); - throw new Error("GOOGLE_API_KEY is not set."); - } + const list = ProviderManager.list(); + const active = ProviderManager.getActive(); + const mappings = state.providerMappings || {}; + + const resolveProviderForTask = (task: string): ILLMProvider => { + const mappedId = mappings[task]; + let inst = mappedId ? list.find((p) => p.id === mappedId) : null; + if (!inst) { + inst = active; + } + if (!inst) { + const envKey = process.env.GOOGLE_API_KEY; + if (envKey) { + inst = ProviderManager.create("Default (Env)", "google-genai", envKey); + } + } + + if (!inst) { + throw new Error(`No active LLM Provider Instance found for task "${task}". Please configure a key in Settings first.`); + } + + if (inst.providerName === "google-genai") { + return new GeminiProvider(inst.apiKey); + } else { + return new MockLLMProvider([]); + } + }; const coreRepo = new SQLiteRepository(db); const bufferRepo = new BufferRepository(db); - const llmProvider = new GeminiProvider(apiKey); - const architect = new Architect(llmProvider, coreRepo); - const aliasGenerator = new AliasDeltaGenerator(llmProvider); + + const actorProvider = resolveProviderForTask("actor-prose"); + const validatorProvider = resolveProviderForTask("llm-validator"); + const decoderProvider = resolveProviderForTask("intent-decoder"); + const timedeltaProvider = resolveProviderForTask("timedelta"); + + const architect = new Architect( + { validator: validatorProvider, timedelta: timedeltaProvider }, + coreRepo, + ); + const aliasGenerator = new AliasDeltaGenerator(actorProvider); const session: SimSession = { db, @@ -629,7 +702,10 @@ class SimulationManager { entities: state.entities || [], playerEntityId: state.playerEntityId, entityIndex: state.entityIndex, - llmProvider, + actorProvider, + validatorProvider, + decoderProvider, + timedeltaProvider, architect, aliasGenerator, log: state.log || [], @@ -637,6 +713,7 @@ class SimulationManager { error: state.error, waitingEntity: state.waitingEntity, aliasDoneForTurn: state.aliasDoneForTurn || false, + providerMappings: mappings, }; this.sessions.set(id, session); @@ -710,6 +787,7 @@ class SimulationManager { waitingEntity: session.waitingEntity, aliasDoneForTurn: session.aliasDoneForTurn, log: session.log, + providerMappings: session.providerMappings, }; session.db.prepare(` diff --git a/packages/actor/src/actor.ts b/packages/actor/src/actor.ts index 6cdd7bf..a8f9110 100644 --- a/packages/actor/src/actor.ts +++ b/packages/actor/src/actor.ts @@ -68,15 +68,29 @@ export class ActorAgent { private decoder: IntentDecoder; private generator: IActorProseGenerator; + private llmProvider: ILLMProvider; + constructor( - private llmProvider: ILLMProvider, + llmProvider: ILLMProvider | { actor: ILLMProvider; decoder: ILLMProvider }, bufferRepo?: BufferRepository, memoryLimit?: number, generator?: IActorProseGenerator, ) { + let actorProv: ILLMProvider; + let decoderProv: ILLMProvider; + + if ("actor" in llmProvider && "decoder" in llmProvider) { + actorProv = llmProvider.actor; + decoderProv = llmProvider.decoder; + } else { + actorProv = llmProvider; + decoderProv = llmProvider; + } + this.promptBuilder = new ActorPromptBuilder(bufferRepo, memoryLimit); - this.decoder = new IntentDecoder(llmProvider); - this.generator = generator ?? new LLMActorProseGenerator(llmProvider); + this.decoder = new IntentDecoder(decoderProv); + this.generator = generator ?? new LLMActorProseGenerator(actorProv); + this.llmProvider = actorProv; } /** diff --git a/packages/architect/src/architect.ts b/packages/architect/src/architect.ts index 6959aa4..17c40b9 100644 --- a/packages/architect/src/architect.ts +++ b/packages/architect/src/architect.ts @@ -12,9 +12,23 @@ export class Architect { private validator: LLMValidator; private timeDeltaGenerator: TimeDeltaGenerator; - constructor(llmProvider: ILLMProvider, private repo?: SQLiteRepository) { - this.validator = new LLMValidator(llmProvider); - this.timeDeltaGenerator = new TimeDeltaGenerator(llmProvider); + constructor( + llmProvider: ILLMProvider | { validator: ILLMProvider; timedelta: ILLMProvider }, + private repo?: SQLiteRepository, + ) { + let valProv: ILLMProvider; + let timeProv: ILLMProvider; + + if ("validator" in llmProvider && "timedelta" in llmProvider) { + valProv = llmProvider.validator; + timeProv = llmProvider.timedelta; + } else { + valProv = llmProvider; + timeProv = llmProvider; + } + + this.validator = new LLMValidator(valProv); + this.timeDeltaGenerator = new TimeDeltaGenerator(timeProv); } /** diff --git a/packages/llm/src/llm.ts b/packages/llm/src/llm.ts index 9b3d93f..7c2550a 100644 --- a/packages/llm/src/llm.ts +++ b/packages/llm/src/llm.ts @@ -36,3 +36,11 @@ export interface ILLMProvider { ): Promise>>; lastCalls?: LLMCallRecord[]; } + +export interface LLMProviderInstance { + id: string; + name: string; + providerName: string; + apiKey: string; + isActive: boolean; +}