Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b4aa672c58 |
@@ -1,6 +1,6 @@
|
|||||||
import { Router, type Request, type Response } from "express";
|
import { Router, type Request, type Response } from "express";
|
||||||
import { randomBytes, randomUUID, createHash } from "crypto";
|
import { randomBytes, randomUUID, createHash } from "crypto";
|
||||||
import { db, sessions, sessionRequests, type Session } from "@workspace/db";
|
import { db, sessions, sessionRequests, sessionMessages, type Session } from "@workspace/db";
|
||||||
import { eq, and } from "drizzle-orm";
|
import { eq, and } from "drizzle-orm";
|
||||||
import { lnbitsService } from "../lib/lnbits.js";
|
import { lnbitsService } from "../lib/lnbits.js";
|
||||||
import { sessionsLimiter } from "../lib/rate-limiter.js";
|
import { sessionsLimiter } from "../lib/rate-limiter.js";
|
||||||
@@ -251,6 +251,42 @@ router.get("/sessions/:id", async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ── GET /sessions/:id/messages ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
router.get("/sessions/:id/messages", async (req: Request, res: Response) => {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
const macaroon = extractMacaroon(req);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await getSessionById(id);
|
||||||
|
if (!session) { res.status(404).json({ error: "Session not found" }); return; }
|
||||||
|
|
||||||
|
if (!macaroon || macaroon !== session.macaroon) {
|
||||||
|
res.status(401).json({ error: "Invalid or missing macaroon" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = await db
|
||||||
|
.select()
|
||||||
|
.from(sessionMessages)
|
||||||
|
.where(eq(sessionMessages.sessionId, id))
|
||||||
|
.orderBy(sessionMessages.id);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
sessionId: id,
|
||||||
|
messages: rows.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
role: m.role,
|
||||||
|
content: m.content,
|
||||||
|
sessionRequestId: m.sessionRequestId,
|
||||||
|
createdAt: m.createdAt.toISOString(),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ error: err instanceof Error ? err.message : "Failed to fetch messages" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// ── POST /sessions/:id/request ────────────────────────────────────────────────
|
// ── POST /sessions/:id/request ────────────────────────────────────────────────
|
||||||
|
|
||||||
router.post("/sessions/:id/request", async (req: Request, res: Response) => {
|
router.post("/sessions/:id/request", async (req: Request, res: Response) => {
|
||||||
@@ -424,7 +460,7 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => {
|
|||||||
const newSessionState = newBalance < MIN_BALANCE_SATS ? "paused" : "active";
|
const newSessionState = newBalance < MIN_BALANCE_SATS ? "paused" : "active";
|
||||||
const expiresAt = new Date(Date.now() + EXPIRY_MS);
|
const expiresAt = new Date(Date.now() + EXPIRY_MS);
|
||||||
|
|
||||||
// Persist session request + update session balance atomically
|
// Persist session request + messages + update session balance atomically
|
||||||
await db.transaction(async (tx) => {
|
await db.transaction(async (tx) => {
|
||||||
await tx.insert(sessionRequests).values({
|
await tx.insert(sessionRequests).values({
|
||||||
id: requestId,
|
id: requestId,
|
||||||
@@ -443,6 +479,24 @@ router.post("/sessions/:id/request", async (req: Request, res: Response) => {
|
|||||||
btcPriceUsd,
|
btcPriceUsd,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Store conversation messages
|
||||||
|
await tx.insert(sessionMessages).values({
|
||||||
|
sessionId: id,
|
||||||
|
role: "user",
|
||||||
|
content: requestText,
|
||||||
|
sessionRequestId: requestId,
|
||||||
|
});
|
||||||
|
|
||||||
|
const assistantContent = result ?? reason ?? errorMessage ?? "";
|
||||||
|
if (assistantContent) {
|
||||||
|
await tx.insert(sessionMessages).values({
|
||||||
|
sessionId: id,
|
||||||
|
role: "assistant",
|
||||||
|
content: assistantContent,
|
||||||
|
sessionRequestId: requestId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await tx
|
await tx
|
||||||
.update(sessions)
|
.update(sessions)
|
||||||
.set({
|
.set({
|
||||||
|
|||||||
13
lib/db/migrations/0008_session_messages.sql
Normal file
13
lib/db/migrations/0008_session_messages.sql
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
-- Migration: Session messages for conversation history
|
||||||
|
-- Stores user/assistant message pairs produced during session requests.
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS session_messages (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
session_id TEXT NOT NULL REFERENCES sessions(id),
|
||||||
|
role TEXT NOT NULL,
|
||||||
|
content TEXT NOT NULL,
|
||||||
|
session_request_id TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_session_messages_session_id ON session_messages(session_id);
|
||||||
@@ -5,6 +5,7 @@ export * from "./messages";
|
|||||||
export * from "./bootstrap-jobs";
|
export * from "./bootstrap-jobs";
|
||||||
export * from "./world-events";
|
export * from "./world-events";
|
||||||
export * from "./sessions";
|
export * from "./sessions";
|
||||||
|
export * from "./session-messages";
|
||||||
export * from "./nostr-identities";
|
export * from "./nostr-identities";
|
||||||
export * from "./timmy-config";
|
export * from "./timmy-config";
|
||||||
export * from "./free-tier-grants";
|
export * from "./free-tier-grants";
|
||||||
|
|||||||
35
lib/db/src/schema/session-messages.ts
Normal file
35
lib/db/src/schema/session-messages.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import { pgTable, text, timestamp, serial } from "drizzle-orm/pg-core";
|
||||||
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
|
import { z } from "zod/v4";
|
||||||
|
import { sessions } from "./sessions";
|
||||||
|
|
||||||
|
// ── session_messages ─────────────────────────────────────────────────────────
|
||||||
|
// Stores the full conversation history within a session.
|
||||||
|
// Each session request produces a "user" message (the request text)
|
||||||
|
// and an "assistant" message (the AI response), linked to the session
|
||||||
|
// and optionally to the session_request that produced them.
|
||||||
|
|
||||||
|
export const SESSION_MESSAGE_ROLES = ["user", "assistant"] as const;
|
||||||
|
export type SessionMessageRole = (typeof SESSION_MESSAGE_ROLES)[number];
|
||||||
|
|
||||||
|
export const sessionMessages = pgTable("session_messages", {
|
||||||
|
id: serial("id").primaryKey(),
|
||||||
|
sessionId: text("session_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => sessions.id),
|
||||||
|
role: text("role").$type<SessionMessageRole>().notNull(),
|
||||||
|
content: text("content").notNull(),
|
||||||
|
|
||||||
|
// Links back to the session_request that produced this message pair (nullable for flexibility)
|
||||||
|
sessionRequestId: text("session_request_id"),
|
||||||
|
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const insertSessionMessageSchema = createInsertSchema(sessionMessages).omit({
|
||||||
|
id: true,
|
||||||
|
createdAt: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type SessionMessage = typeof sessionMessages.$inferSelect;
|
||||||
|
export type InsertSessionMessage = z.infer<typeof insertSessionMessageSchema>;
|
||||||
Reference in New Issue
Block a user