mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-20 00:41:26 +02:00
feat(writing): M3 — one-shot prose generation via mana-llm
Server:
- New llmText() helper in apps/api/src/lib/llm.ts for plain-text
(non-streaming) completions with token-usage reporting.
- POST /api/v1/writing/generations (Hono + requireTier('beta'))
accepts system+user prompts, forwards to mana-llm (default model
ollama/gemma3:4b), returns raw output + model + tokenUsage. The
endpoint is stateless — draft/version bookkeeping is entirely
client-side so the same route serves refinement calls later.
Client:
- writing/api.ts — Bearer-authed fetch client (follows the food/
news-research pattern).
- writing/utils/prompt-builder.ts — pure builder turning a briefing
(+ optional style preset / extracted principles) into a system+user
pair. Forbids preamble / sign-off / meta commentary so the output is
ready to paste into a version.
- writing/stores/generations.svelte.ts — orchestrates the full flow:
queued → running → call → new LocalDraftVersion → pointer flip →
succeeded. On failure leaves the current version untouched with the
error on the generation record. Emits WritingDraftGenerationStarted /
WritingDraftVersionCreated / WritingDraftGenerationFailed events.
UI:
- Generate button in DetailView.svelte (label flips "Generate" / "Neu
generieren" based on whether the draft already has content).
- GenerationStatus.svelte strip surfaces queued / running / failed with
model + duration badges; succeeded generations auto-disappear because
the new version is already live via the currentVersionId pointer.
M3 is synchronous and non-streaming by design. M7 adds mission-based
long-form with streaming + outline stage + reference injection. M6 will
reuse the same /generations endpoint for selection-refinement prompts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3c3b2ebbc7
commit
d725a8df8b
9 changed files with 814 additions and 11 deletions
|
|
@ -31,6 +31,15 @@ export interface LlmJsonOptions {
|
|||
maxTokens?: number;
|
||||
}
|
||||
|
||||
export interface LlmTextOptions {
|
||||
model: string;
|
||||
system?: string;
|
||||
user: string;
|
||||
temperature?: number;
|
||||
maxTokens?: number;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface LlmStreamOptions {
|
||||
model: string;
|
||||
system?: string;
|
||||
|
|
@ -101,6 +110,56 @@ export async function llmJson<T = unknown>(opts: LlmJsonOptions): Promise<T> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the LLM and return the raw text content — no JSON parsing, no
|
||||
* streaming. Used when you want a finished prose artifact (a generated
|
||||
* draft, a summary, a translation) as one string. Includes token usage
|
||||
* when the provider reports it so generation records can store it.
|
||||
*/
|
||||
export interface LlmTextResult {
|
||||
text: string;
|
||||
tokenUsage?: { input: number; output: number };
|
||||
model: string;
|
||||
}
|
||||
|
||||
export async function llmText(opts: LlmTextOptions): Promise<LlmTextResult> {
|
||||
const res = await fetch(`${LLM_URL}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
model: opts.model,
|
||||
messages: buildMessages(opts.system, opts.user),
|
||||
temperature: opts.temperature ?? 0.7,
|
||||
max_tokens: opts.maxTokens ?? 2000,
|
||||
}),
|
||||
signal: opts.signal,
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const body = await res.text().catch(() => '');
|
||||
throw new LlmError(`mana-llm returned ${res.status}`, res.status, body);
|
||||
}
|
||||
|
||||
const data = (await res.json()) as {
|
||||
choices?: Array<{ message?: { content?: string } }>;
|
||||
usage?: { prompt_tokens?: number; completion_tokens?: number };
|
||||
model?: string;
|
||||
};
|
||||
const text = data.choices?.[0]?.message?.content;
|
||||
if (!text) throw new LlmError('mana-llm response missing content');
|
||||
return {
|
||||
text: text.trim(),
|
||||
tokenUsage:
|
||||
data.usage && typeof data.usage.prompt_tokens === 'number'
|
||||
? {
|
||||
input: data.usage.prompt_tokens ?? 0,
|
||||
output: data.usage.completion_tokens ?? 0,
|
||||
}
|
||||
: undefined,
|
||||
model: data.model ?? opts.model,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Call the LLM in streaming mode. Invokes onToken() for each delta and
|
||||
* returns the full concatenated text once the stream completes.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue