mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-11-14 00:01:03 +00:00
c0ec8e567b
Asked on a journey page to write an entry, the assistant said it could not: the only journey chat was the Studio's, which keeps the journey's writes out. A journey session now holds what either page offers and shows the model only the asking page's tools. The journal page offers a viewer the reads, an editor the entry, photo, recorded day and day import writes, and the owner also the journey's own settings and trips; delete_journey stays out of the journey's own chat. A few reads that name no journey come along (place search, the photo libraries, Dawarich stays), the Dawarich journal entry is pinned through its journalId, and an entry an argument names must belong to the conversation's journey before the tool runs. The turn reads a journal card and instructions of its own; the Studio keeps exactly what it offered before.
163 lines
7.6 KiB
TypeScript
163 lines
7.6 KiB
TypeScript
import type { AssistantThreadRow, AssistantThreadsRepository } from '../../db/repositories/AssistantThreads.repository';
|
|
import type {
|
|
AssistantToolCallRow,
|
|
AssistantToolCallsRepository,
|
|
} from '../../db/repositories/AssistantToolCalls.repository';
|
|
import { withRequestContext } from '../database/request-context';
|
|
import { SettingsService } from '../settings/settings.service';
|
|
import { AssistantBookContext, bookTurnContext, emptyTripCard } from './assistant-book-context';
|
|
import { AssistantBookGate, openGate, type BookTurnGate } from './assistant-book-gate';
|
|
import { languageName, todayIn, turnContext, type TripCard } from './assistant-context';
|
|
import { homeCardFrom, homePrompt, homeTurnContext } from './assistant-home';
|
|
import { journalTurnContext } from './assistant-journal-context';
|
|
import { AssistantToolsService, type AssistantToolSession } from './assistant-tools.service';
|
|
import { tripCardFrom } from './assistant-trip-scope';
|
|
import { journalPrompt, studioPrompt, systemPrompt } from './assistant.prompt';
|
|
import { MikroORM } from '@mikro-orm/core';
|
|
import { Injectable } from '@nestjs/common';
|
|
import type { AssistantFocus, AssistantMention, AssistantScope } from '@trek/shared';
|
|
|
|
/**
|
|
* What a conversation is about, a trip, a journey or (home) neither
|
|
* (`AssistantScope`), and what follows from it: where its threads are kept,
|
|
* which tools a turn holds (the tools that take a `tripId` or a `journeyId`
|
|
* run on the conversation's own; at home on the one the model names, reads
|
|
* only), and what the model reads beside the history. The agent loop, the
|
|
* cards, the decisions and the undo are the same for all three.
|
|
*/
|
|
|
|
/** The scope of a trip conversation. */
|
|
export function tripScope(tripId: number): AssistantScope {
|
|
return { kind: 'trip', tripId };
|
|
}
|
|
|
|
/** The user's threads in one scope: every read and delete names the trip or journey next to the user. */
|
|
interface ScopedThreads {
|
|
create(userId: number, title: string, at: string): Promise<number>;
|
|
find(id: number, userId: number): Promise<AssistantThreadRow | null>;
|
|
list(userId: number, limit: number): Promise<AssistantThreadRow[]>;
|
|
remove(id: number, userId: number): Promise<boolean>;
|
|
}
|
|
|
|
export function scopedThreads(repo: AssistantThreadsRepository, scope: AssistantScope): ScopedThreads {
|
|
if (scope.kind === 'home') {
|
|
return {
|
|
create: (userId, title, at) => repo.createHomeThread(userId, title, at),
|
|
find: (id, userId) => repo.findOwnedAtHome(id, userId),
|
|
list: (userId, limit) => repo.listRecentAtHome(userId, limit),
|
|
remove: (id, userId) => repo.deleteOwnedAtHome(id, userId),
|
|
};
|
|
}
|
|
if (scope.kind === 'trip') {
|
|
const { tripId } = scope;
|
|
return {
|
|
create: (userId, title, at) => repo.createThread(userId, tripId, title, at),
|
|
find: (id, userId) => repo.findOwned(id, userId, tripId),
|
|
list: (userId, limit) => repo.listRecent(userId, tripId, limit),
|
|
remove: (id, userId) => repo.deleteOwned(id, userId, tripId),
|
|
};
|
|
}
|
|
const { journeyId } = scope;
|
|
return {
|
|
create: (userId, title, at) => repo.createJourneyThread(userId, journeyId, title, at),
|
|
find: (id, userId) => repo.findOwnedOnJourney(id, userId, journeyId),
|
|
list: (userId, limit) => repo.listRecentOnJourney(userId, journeyId, limit),
|
|
remove: (id, userId) => repo.deleteOwnedOnJourney(id, userId, journeyId),
|
|
};
|
|
}
|
|
|
|
/** A proposed change of one of the user's threads in this scope; null for anyone else's or another scope's. */
|
|
export function findScopedCall(
|
|
repo: AssistantToolCallsRepository,
|
|
scope: AssistantScope,
|
|
id: string,
|
|
userId: number,
|
|
): Promise<AssistantToolCallRow | null> {
|
|
if (scope.kind === 'home') return repo.findOwnedAtHome(id, userId);
|
|
return scope.kind === 'trip'
|
|
? repo.findOwned(id, userId, scope.tripId)
|
|
: repo.findOwnedOnJourney(id, userId, scope.journeyId);
|
|
}
|
|
|
|
/** What a turn knows about the user's request, whatever its scope. */
|
|
interface ScopeTurnInput {
|
|
userId: number;
|
|
locale: string;
|
|
timeZone: string;
|
|
focus: AssistantFocus | null;
|
|
mentions: readonly AssistantMention[];
|
|
now: Date;
|
|
}
|
|
|
|
/**
|
|
* What a turn hands the model and the agent loop: the trip card the cards
|
|
* read, the instructions, the context, and the gate its held calls pass
|
|
* (a journey's checks book writes; a trip's lets every call through).
|
|
*/
|
|
interface ScopeTurn {
|
|
card: TripCard;
|
|
system: string;
|
|
context: string;
|
|
gate: BookTurnGate;
|
|
}
|
|
|
|
/** Opens a scope's tool sessions and writes what its turns read. */
|
|
@Injectable()
|
|
export class AssistantScopes {
|
|
constructor(
|
|
private readonly tools: AssistantToolsService,
|
|
private readonly books: AssistantBookContext,
|
|
private readonly bookGate: AssistantBookGate,
|
|
private readonly settings: SettingsService,
|
|
private readonly orm: MikroORM,
|
|
) {}
|
|
|
|
/**
|
|
* The tool session of a turn (or, with targets, of an undo). A journey's
|
|
* offers what the page asking offers for the user's role in the journey
|
|
* (the Studio the book's writes only to someone who may change the book);
|
|
* the home chat's offers no trip's writes at all.
|
|
*/
|
|
async open(userId: number, scope: AssistantScope, undoTargets?: ReadonlySet<string>): Promise<AssistantToolSession> {
|
|
if (scope.kind === 'trip') return this.tools.open(userId, scope.tripId, undoTargets);
|
|
if (scope.kind === 'home') return this.tools.openHome(userId, undoTargets);
|
|
const role = await withRequestContext(this.orm, () => this.books.roleIn(userId, scope.journeyId));
|
|
const offer = { role, surface: scope.surface ?? 'studio' } as const;
|
|
return this.tools.openJourney(userId, scope.journeyId, offer, undoTargets);
|
|
}
|
|
|
|
/**
|
|
* The card, instructions and context of one turn: for a trip the trip card
|
|
* read through the turn's session and the user's units and trip notes; for a
|
|
* journey the book card, the Studio's instructions and the check of its
|
|
* book writes; at home the user's trips, read through the session, and the
|
|
* units, with every held call let through as on a trip.
|
|
*/
|
|
async turnOf(session: AssistantToolSession, scope: AssistantScope, turn: ScopeTurnInput): Promise<ScopeTurn> {
|
|
const language = languageName(turn.locale);
|
|
if (scope.kind === 'home') {
|
|
const home = await homeCardFrom(session, todayIn(turn.timeZone, turn.now));
|
|
const prefs = await withRequestContext(this.orm, () => this.settings.getUserSettings(turn.userId));
|
|
const context = homeTurnContext(home, turn.timeZone, turn.now, prefs);
|
|
return { card: emptyTripCard(turn.userId), system: homePrompt(language), context, gate: openGate(session) };
|
|
}
|
|
if (scope.kind === 'trip') {
|
|
const card = await tripCardFrom(session, scope.tripId, turn.userId, turn.focus, turn.mentions);
|
|
const prefs = await withRequestContext(this.orm, () => this.settings.getUserSettings(turn.userId));
|
|
const context = turnContext(card, turn.timeZone, turn.now, prefs);
|
|
return { card, system: systemPrompt(language), context, gate: openGate(session) };
|
|
}
|
|
const { journeyId } = scope;
|
|
if (scope.surface === 'journal') {
|
|
const journal = await withRequestContext(this.orm, () => this.books.journalCardOf(turn.userId, journeyId));
|
|
const context = journalTurnContext(journal, turn.timeZone, turn.now);
|
|
return { card: emptyTripCard(turn.userId), system: journalPrompt(language), context, gate: openGate(session) };
|
|
}
|
|
const book = await withRequestContext(this.orm, () => this.books.cardOf(turn.userId, journeyId, turn.focus));
|
|
const context = bookTurnContext(book, turn.timeZone, turn.now);
|
|
const gate = this.bookGate.forTurn(turn.userId, journeyId, session);
|
|
const system = studioPrompt(language, book.role !== 'viewer');
|
|
return { card: emptyTripCard(turn.userId), system, context, gate };
|
|
}
|
|
}
|