Files
Socrates/apps/web/prisma/schema.prisma
dtoro 78faca9968 MVP M6: live Socrates dock — LLM gateway + persisted thread + reply loop
The dock now talks to a real LLM. On first load it asks Socrates for an
opening turn that's grounded in the project's actual SysML model + active
validation issues. User replies and option-picks send turns through the
same channel. Thread + every message persist in SQLite so refresh keeps
the conversation.

apps/web/lib/llm
- gateway.ts: LLMGateway interface (chat + ChatOptions). Two adapters:
    - lmstudio: OpenAI SDK against LMSTUDIO_BASE_URL (default
      http://localhost:1234/v1)
    - anthropic: @anthropic-ai/sdk against claude-sonnet-4-6 (set
      ANTHROPIC_API_KEY when LLM_PROVIDER=anthropic)
  Provider chosen via LLM_PROVIDER env (default: lmstudio).
- chatJSON(): JSON-mode helper with parse-error repair-retry — the same
  defensive pattern proven against gemma-4-e4b in Phase 0.
- prompts.ts: server-only loader that caches .md prompts.
- prompts/socrates/character.md + review.md: ported verbatim from
  phase-0/src/prompts/ (Phase 0 corpus validated these 10/10).
- socrates.ts: sendUserTurn() — builds the system prompt (character +
  review + project context with trimmed model + active issues), runs
  chatJSON against the gateway, persists user + assistant turns,
  returns the structured turn. SocratesTurn schema is { text, options? }
  with up to 3 numbered options matching the prototype.

apps/web/prisma
- SocratesThread + SocratesMessage tables. Auto-create one open thread
  per project on first load.

apps/web/app/api/projects/[projectId]/socrates
- GET: returns active thread + parsed messages.
- POST: body { text }. Empty text triggers an opening turn. Persists user
  + assistant turns, returns assistant turn + provider metadata.

apps/web/components/socrates/SocratesDock.tsx
- Replaces the static thread prop with a projectId. Loads from API on
  mount, auto-triggers an opening turn if the thread is empty, sends
  user replies via POST. Numbered options click-to-pick or 1–3 keyboard
  shortcut (skipped when focus is in an input). Status line shows the
  active provider + model. Optimistic-local: user message appears
  instantly, "thinking…" placeholder shows while the LLM works, errors
  surface inline.

apps/web/.env.example + .env.local
- LLM_PROVIDER, LMSTUDIO_BASE_URL/MODEL/API_KEY, ANTHROPIC_API_KEY/MODEL.
- .env.local committed only with the local default (no real secrets);
  user supplies their own per-machine.

What's not yet here (next iterations):
- Streaming responses (currently waits for full response, ~5-15s)
- Multi-thread switcher (one auto-thread per project)
- Socrates-proposes-ops flow (M7)
2026-04-29 07:50:34 +02:00

90 lines
2.6 KiB
Plaintext

// SQLite-backed dev persistence (M5.9). The model is stored as a JSON snapshot
// per project + a changelog of applied ops. This is the smallest shape that
// gives us "refresh persists state" without committing to the full
// event-sourced architecture in docs/sync.md (that lands later).
//
// Switch `provider` to "postgresql" + set DATABASE_URL=postgres://… to move
// to a real DB later — schema is portable.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model Project {
id String @id
name String
scope String
tagline String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
snapshots ModelSnapshot[]
changes ChangelogEntry[]
threads SocratesThread[]
}
model ModelSnapshot {
id String @id @default(cuid())
projectId String
version Int
json String // serialized SysMLModel
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@unique([projectId, version])
@@index([projectId, version])
}
model ChangelogEntry {
id String @id @default(cuid())
projectId String
version Int // resulting model version after these ops landed
ops String // JSON-encoded ModelOp[]
reason String? // optional human/Socrates-supplied rationale
createdAt DateTime @default(now())
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
@@index([projectId, version])
}
model SocratesThread {
id String @id @default(cuid())
projectId String
/// Model element this thread is anchored to, if any (block id, req id, …).
anchorElementId String?
status String @default("open") // open | archived | resolved
title String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
messages SocratesMessage[]
@@index([projectId])
}
model SocratesMessage {
id String @id @default(cuid())
threadId String
role String // "user" | "assistant"
/// JSON: { text, options?: [{n,label,sub?}] }
content String
/// Provider + model that produced this turn (assistant turns only).
provider String?
model String?
inputTokens Int?
outputTokens Int?
createdAt DateTime @default(now())
thread SocratesThread @relation(fields: [threadId], references: [id], onDelete: Cascade)
@@index([threadId, createdAt])
}