chore: sync remaining RealSkills updates and config changes

- Update skills route, activity feed, and stats components
- Update skills embedding and response generator modules
- Update Omega conversation messages route
- Update PRD documentation
- Refresh CLI oclif manifest
- Update gitignore and package configs
This commit is contained in:
Ajax Davis 2026-01-25 03:34:27 +10:00
parent a44f38eda9
commit 234548c6b2
18 changed files with 1397 additions and 679 deletions

3
.gitignore vendored
View file

@ -55,6 +55,9 @@ secrets.json
# turbo # turbo
.turbo .turbo
# ai sdk devtools
.devtools
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo

1
apps/web/.gitignore vendored
View file

@ -1,2 +1,3 @@
.vercel .vercel
.env*.local .env*.local
.devtools

View file

@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts"; import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View file

@ -21,6 +21,7 @@
}, },
"dependencies": { "dependencies": {
"@ai-sdk/anthropic": "^3.0.9", "@ai-sdk/anthropic": "^3.0.9",
"@ai-sdk/devtools": "^0.0.8",
"@ai-sdk/google": "^3.0.6", "@ai-sdk/google": "^3.0.6",
"@ai-sdk/groq": "^3.0.4", "@ai-sdk/groq": "^3.0.4",
"@ai-sdk/mistral": "^3.0.5", "@ai-sdk/mistral": "^3.0.5",
@ -61,8 +62,8 @@
"remark-gfm": "^4.0.1", "remark-gfm": "^4.0.1",
"resend": "^6.7.0", "resend": "^6.7.0",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"swr": "^2.2.5",
"streamdown": "^1.6.11", "streamdown": "^1.6.11",
"swr": "^2.2.5",
"three": "^0.182.0", "three": "^0.182.0",
"zod": "^4.3.5" "zod": "^4.3.5"
}, },

View file

@ -8,23 +8,23 @@
* POST - Ask a question (RAG + LLM response) * POST - Ask a question (RAG + LLM response)
*/ */
import { createHash } from 'crypto';
import { prisma } from '@tpmjs/db'; import { prisma } from '@tpmjs/db';
import { NextResponse, type NextRequest } from 'next/server'; import { createHash } from 'crypto';
import { type NextRequest, NextResponse } from 'next/server';
import { z } from 'zod'; import { z } from 'zod';
import { checkQuestionSimilarity } from '~/lib/ai/skills-embedding'; import { checkQuestionSimilarity } from '~/lib/ai/skills-embedding';
import { updateSkillGraph, getCollectionSkillsSummary } from '~/lib/ai/skills-graph-updater'; import { getCollectionSkillsSummary, updateSkillGraph } from '~/lib/ai/skills-graph-updater';
import { import {
generateSkillResponse,
generateFollowupSuggestions,
calculateConfidence,
type CollectionContext, type CollectionContext,
calculateConfidence,
generateFollowupSuggestions,
generateSkillResponse,
} from '~/lib/ai/skills-response-generator'; } from '~/lib/ai/skills-response-generator';
import { import {
seedCollectionSkills,
getSeedingStatus,
type CollectionWithTools, type CollectionWithTools,
getSeedingStatus,
seedCollectionSkills,
} from '~/lib/ai/skills-seeder'; } from '~/lib/ai/skills-seeder';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
@ -51,10 +51,7 @@ const SESSION_EXPIRY_MS = 24 * 60 * 60 * 1000;
* Hash agent identity for anonymization * Hash agent identity for anonymization
*/ */
function hashAgentIdentity(ip: string, userAgent: string): string { function hashAgentIdentity(ip: string, userAgent: string): string {
return createHash('sha256') return createHash('sha256').update(`${ip}:${userAgent}`).digest('hex').slice(0, 16);
.update(`${ip}:${userAgent}`)
.digest('hex')
.slice(0, 16);
} }
/** /**
@ -70,10 +67,7 @@ async function loadCollection(
}); });
if (!user || !user.username) { if (!user || !user.username) {
return NextResponse.json( return NextResponse.json({ success: false, error: 'User not found' }, { status: 404 });
{ success: false, error: 'User not found' },
{ status: 404 }
);
} }
const collection = await prisma.collection.findFirst({ const collection = await prisma.collection.findFirst({
@ -96,10 +90,7 @@ async function loadCollection(
}); });
if (!collection) { if (!collection) {
return NextResponse.json( return NextResponse.json({ success: false, error: 'Collection not found' }, { status: 404 });
{ success: false, error: 'Collection not found' },
{ status: 404 }
);
} }
if (!collection.isPublic) { if (!collection.isPublic) {
@ -160,9 +151,7 @@ export async function GET(_request: NextRequest, context: RouteContext) {
try { try {
const { username: rawUsername, slug } = await context.params; const { username: rawUsername, slug } = await context.params;
const username = rawUsername.startsWith('@') const username = rawUsername.startsWith('@') ? rawUsername.slice(1) : rawUsername;
? rawUsername.slice(1)
: rawUsername;
// Load collection // Load collection
const result = await loadCollection(username, slug); const result = await loadCollection(username, slug);
@ -225,19 +214,14 @@ export async function POST(request: NextRequest, context: RouteContext) {
try { try {
const { username: rawUsername, slug } = await context.params; const { username: rawUsername, slug } = await context.params;
const username = rawUsername.startsWith('@') const username = rawUsername.startsWith('@') ? rawUsername.slice(1) : rawUsername;
? rawUsername.slice(1)
: rawUsername;
// Parse and validate request body // Parse and validate request body
let body: unknown; let body: unknown;
try { try {
body = await request.json(); body = await request.json();
} catch { } catch {
return NextResponse.json( return NextResponse.json({ success: false, error: 'Invalid JSON body' }, { status: 400 });
{ success: false, error: 'Invalid JSON body' },
{ status: 400 }
);
} }
const parseResult = PostRequestSchema.safeParse(body); const parseResult = PostRequestSchema.safeParse(body);
@ -252,8 +236,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
); );
} }
const { question, sessionId, agentName, context: questionContext, tags } = const { question, sessionId, agentName, context: questionContext, tags } = parseResult.data;
parseResult.data;
// Load collection // Load collection
const result = await loadCollection(username, slug); const result = await loadCollection(username, slug);
@ -269,10 +252,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
const agentHash = hashAgentIdentity(ip, userAgent); const agentHash = hashAgentIdentity(ip, userAgent);
// Check for similarity / cache hit // Check for similarity / cache hit
const similarityResult = await checkQuestionSimilarity( const similarityResult = await checkQuestionSimilarity(question, collection.id);
question,
collection.id
);
// If very similar question exists (>95%), return cached answer // If very similar question exists (>95%), return cached answer
if (similarityResult.isCacheHit && similarityResult.cachedAnswer) { if (similarityResult.isCacheHit && similarityResult.cachedAnswer) {
@ -302,8 +282,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
}; };
// Get session history if session exists // Get session history if session exists
let sessionHistory: Array<{ role: 'user' | 'assistant'; content: string }> = let sessionHistory: Array<{ role: 'user' | 'assistant'; content: string }> = [];
[];
let activeSessionId = sessionId; let activeSessionId = sessionId;
if (sessionId) { if (sessionId) {
@ -319,9 +298,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
} }
// Generate response // Generate response
const fullQuestion = questionContext const fullQuestion = questionContext ? `${question}\n\nContext: ${questionContext}` : question;
? `${question}\n\nContext: ${questionContext}`
: question;
const { answer, tokensUsed } = await generateSkillResponse({ const { answer, tokensUsed } = await generateSkillResponse({
question: fullQuestion, question: fullQuestion,
@ -401,11 +378,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
// Generate follow-up suggestions (optional, don't block) // Generate follow-up suggestions (optional, don't block)
let suggestedFollowups: string[] = []; let suggestedFollowups: string[] = [];
try { try {
suggestedFollowups = await generateFollowupSuggestions( suggestedFollowups = await generateFollowupSuggestions(question, answer, collection.name);
question,
answer,
collection.name
);
} catch { } catch {
// Ignore errors for followups // Ignore errors for followups
} }

View file

@ -12,7 +12,8 @@
import { Prisma, prisma } from '@tpmjs/db'; import { Prisma, prisma } from '@tpmjs/db';
import { registryExecuteTool } from '@tpmjs/registry-execute'; import { registryExecuteTool } from '@tpmjs/registry-execute';
import { registrySearchTool } from '@tpmjs/registry-search'; import { registrySearchTool } from '@tpmjs/registry-search';
import { jsonSchema, type ModelMessage } from 'ai'; import { jsonSchema, wrapLanguageModel, type ModelMessage } from 'ai';
import { devToolsMiddleware } from '@ai-sdk/devtools';
import { type NextRequest, NextResponse } from 'next/server'; import { type NextRequest, NextResponse } from 'next/server';
import { z } from 'zod'; import { z } from 'zod';
import { authenticateRequest } from '~/lib/api-keys/middleware'; import { authenticateRequest } from '~/lib/api-keys/middleware';
@ -20,6 +21,12 @@ import { decryptApiKey } from '~/lib/crypto/api-keys';
import { buildSystemPrompt } from '~/lib/omega/system-prompt'; import { buildSystemPrompt } from '~/lib/omega/system-prompt';
import { checkRateLimit, type RateLimitConfig } from '~/lib/rate-limit'; import { checkRateLimit, type RateLimitConfig } from '~/lib/rate-limit';
// Initialize devtools middleware once at module level (only used in development)
const devtools = process.env.NODE_ENV === 'development' ? devToolsMiddleware() : null;
if (devtools) {
console.log('[Omega] AI SDK DevTools middleware initialized');
}
/** /**
* Warning about missing environment variables * Warning about missing environment variables
*/ */
@ -556,7 +563,16 @@ Remember: Your value is in EXECUTING tools to get real results, not just describ
} }
const openai = createOpenAI({ apiKey }); const openai = createOpenAI({ apiKey });
const model = openai('gpt-4.1-mini'); const baseModel = openai('gpt-4.1-mini');
// Wrap with devtools middleware in development
const model = devtools
? wrapLanguageModel({ model: baseModel, middleware: devtools })
: baseModel;
if (devtools) {
console.log('[Omega] Model wrapped with DevTools middleware');
}
// Create SSE stream // Create SSE stream
const stream = new ReadableStream({ const stream = new ReadableStream({

View file

@ -5,7 +5,7 @@
*/ */
import { prisma } from '@tpmjs/db'; import { prisma } from '@tpmjs/db';
import { NextResponse, type NextRequest } from 'next/server'; import { type NextRequest, NextResponse } from 'next/server';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@ -17,10 +17,7 @@ export async function GET(request: NextRequest) {
const limit = Math.min(50, Math.max(1, parseInt(limitParam || '10', 10))); const limit = Math.min(50, Math.max(1, parseInt(limitParam || '10', 10)));
if (!collectionId) { if (!collectionId) {
return NextResponse.json( return NextResponse.json({ error: 'collectionId is required' }, { status: 400 });
{ error: 'collectionId is required' },
{ status: 400 }
);
} }
try { try {
@ -31,17 +28,11 @@ export async function GET(request: NextRequest) {
}); });
if (!collection) { if (!collection) {
return NextResponse.json( return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
{ error: 'Collection not found' },
{ status: 404 }
);
} }
if (!collection.isPublic) { if (!collection.isPublic) {
return NextResponse.json( return NextResponse.json({ error: 'Collection is not public' }, { status: 403 });
{ error: 'Collection is not public' },
{ status: 403 }
);
} }
// Fetch recent questions with skill links (anonymized - no agent info) // Fetch recent questions with skill links (anonymized - no agent info)
@ -73,9 +64,6 @@ export async function GET(request: NextRequest) {
return NextResponse.json({ questions }); return NextResponse.json({ questions });
} catch (error) { } catch (error) {
console.error('[Skills Activity Error]:', error); console.error('[Skills Activity Error]:', error);
return NextResponse.json( return NextResponse.json({ error: 'Failed to fetch activity' }, { status: 500 });
{ error: 'Failed to fetch activity' },
{ status: 500 }
);
} }
} }

View file

@ -5,7 +5,7 @@
*/ */
import { prisma } from '@tpmjs/db'; import { prisma } from '@tpmjs/db';
import { NextResponse, type NextRequest } from 'next/server'; import { type NextRequest, NextResponse } from 'next/server';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
export const dynamic = 'force-dynamic'; export const dynamic = 'force-dynamic';
@ -15,10 +15,7 @@ export async function GET(request: NextRequest) {
const collectionId = searchParams.get('collectionId'); const collectionId = searchParams.get('collectionId');
if (!collectionId) { if (!collectionId) {
return NextResponse.json( return NextResponse.json({ error: 'collectionId is required' }, { status: 400 });
{ error: 'collectionId is required' },
{ status: 400 }
);
} }
try { try {
@ -29,17 +26,11 @@ export async function GET(request: NextRequest) {
}); });
if (!collection) { if (!collection) {
return NextResponse.json( return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
{ error: 'Collection not found' },
{ status: 404 }
);
} }
if (!collection.isPublic) { if (!collection.isPublic) {
return NextResponse.json( return NextResponse.json({ error: 'Collection is not public' }, { status: 403 });
{ error: 'Collection is not public' },
{ status: 403 }
);
} }
// Fetch stats in parallel // Fetch stats in parallel
@ -65,9 +56,6 @@ export async function GET(request: NextRequest) {
}); });
} catch (error) { } catch (error) {
console.error('[Skills Stats Error]:', error); console.error('[Skills Stats Error]:', error);
return NextResponse.json( return NextResponse.json({ error: 'Failed to fetch stats' }, { status: 500 });
{ error: 'Failed to fetch stats' },
{ status: 500 }
);
} }
} }

View file

@ -251,11 +251,7 @@ export default function SkillsPage(): React.ReactElement {
/> />
<h4 className="font-semibold mt-4">Example Response</h4> <h4 className="font-semibold mt-4">Example Response</h4>
<CodeBlock <CodeBlock code={exampleResponseJson} language="json" showCopy={true} />
code={exampleResponseJson}
language="json"
showCopy={true}
/>
</CardContent> </CardContent>
</Card> </Card>
@ -353,7 +349,9 @@ const followUp = await askSkills(
<h4 className="font-semibold text-sm mb-2">Tips for Better Responses</h4> <h4 className="font-semibold text-sm mb-2">Tips for Better Responses</h4>
<ul className="text-foreground-secondary text-sm space-y-1"> <ul className="text-foreground-secondary text-sm space-y-1">
<li> Be specific about what you&apos;re trying to accomplish</li> <li> Be specific about what you&apos;re trying to accomplish</li>
<li> Include relevant context in the <code>context</code> field</li> <li>
Include relevant context in the <code>context</code> field
</li>
<li> Use tags to hint at the problem domain</li> <li> Use tags to hint at the problem domain</li>
<li> Use sessions for related follow-up questions</li> <li> Use sessions for related follow-up questions</li>
</ul> </ul>
@ -413,8 +411,8 @@ const followUp = await askSkills(
</CardHeader> </CardHeader>
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<p className="text-foreground-secondary"> <p className="text-foreground-secondary">
When a collection&apos;s skills endpoint is accessed for the first time, it automatically When a collection&apos;s skills endpoint is accessed for the first time, it
seeds with synthetic questions generated from: automatically seeds with synthetic questions generated from:
</p> </p>
<ul className="list-disc list-inside space-y-2 text-foreground-secondary"> <ul className="list-disc list-inside space-y-2 text-foreground-secondary">
@ -424,8 +422,8 @@ const followUp = await askSkills(
</ul> </ul>
<p className="text-foreground-secondary mt-4"> <p className="text-foreground-secondary mt-4">
This ensures the endpoint is useful immediately, even before any real agent interactions. This ensures the endpoint is useful immediately, even before any real agent
Seeding typically adds 10-15 synthetic Q&amp;A pairs. interactions. Seeding typically adds 10-15 synthetic Q&amp;A pairs.
</p> </p>
<div className="bg-background-secondary p-4 rounded-lg mt-4"> <div className="bg-background-secondary p-4 rounded-lg mt-4">
@ -490,7 +488,10 @@ const followUp = await askSkills(
</p> </p>
</li> </li>
<li> <li>
<Link href="/docs/api/collections" className="text-primary hover:underline font-medium"> <Link
href="/docs/api/collections"
className="text-primary hover:underline font-medium"
>
Collections API Collections API
</Link> </Link>
<p className="text-foreground-secondary text-sm"> <p className="text-foreground-secondary text-sm">

View file

@ -1,15 +1,10 @@
'use client'; 'use client';
import { Badge } from '@tpmjs/ui/Badge/Badge'; import { Badge } from '@tpmjs/ui/Badge/Badge';
import { import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@tpmjs/ui/Card/Card';
import { Icon } from '@tpmjs/ui/Icon/Icon'; import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton'; import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton';
// Simple relative time formatter // Simple relative time formatter
function formatRelativeTime(date: Date): string { function formatRelativeTime(date: Date): string {
const now = Date.now(); const now = Date.now();
@ -24,6 +19,7 @@ function formatRelativeTime(date: Date): string {
if (minutes > 0) return `${minutes}m ago`; if (minutes > 0) return `${minutes}m ago`;
return 'just now'; return 'just now';
} }
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
interface SkillQuestion { interface SkillQuestion {
@ -104,11 +100,7 @@ export function SkillsActivityFeed({
return ( return (
<Card variant="default" className="border-dashed"> <Card variant="default" className="border-dashed">
<CardContent padding="lg" className="text-center"> <CardContent padding="lg" className="text-center">
<Icon <Icon icon="message" size="lg" className="mx-auto text-foreground-tertiary mb-2" />
icon="message"
size="lg"
className="mx-auto text-foreground-tertiary mb-2"
/>
<p className="text-foreground-secondary text-sm"> <p className="text-foreground-secondary text-sm">
No questions yet. Be the first to ask! No questions yet. Be the first to ask!
</p> </p>
@ -127,10 +119,7 @@ export function SkillsActivityFeed({
{q.question} {q.question}
</CardTitle> </CardTitle>
<div className="flex items-center gap-1 flex-shrink-0"> <div className="flex items-center gap-1 flex-shrink-0">
<Badge <Badge variant={q.confidence >= 0.7 ? 'success' : 'secondary'} size="sm">
variant={q.confidence >= 0.7 ? 'success' : 'secondary'}
size="sm"
>
{Math.round(q.confidence * 100)}% {Math.round(q.confidence * 100)}%
</Badge> </Badge>
</div> </div>

View file

@ -67,11 +67,7 @@ curl -X POST "${skillsUrl}" \\
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button <Button variant="ghost" size="sm" onClick={() => setShowApiDocs(!showApiDocs)}>
variant="ghost"
size="sm"
onClick={() => setShowApiDocs(!showApiDocs)}
>
<Icon icon="terminal" className="w-4 h-4 mr-1" /> <Icon icon="terminal" className="w-4 h-4 mr-1" />
API API
</Button> </Button>
@ -122,9 +118,7 @@ curl -X POST "${skillsUrl}" \\
{/* Activity Feed Column */} {/* Activity Feed Column */}
<div className="md:col-span-2"> <div className="md:col-span-2">
<div className="flex items-center justify-between mb-3"> <div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-medium text-foreground-secondary"> <h3 className="text-sm font-medium text-foreground-secondary">Recent Questions</h3>
Recent Questions
</h3>
</div> </div>
<SkillsActivityFeed collectionId={collectionId} limit={5} /> <SkillsActivityFeed collectionId={collectionId} limit={5} />
</div> </div>
@ -144,11 +138,7 @@ curl -X POST "${skillsUrl}" \\
Every question helps improve future responses for all agents. Every question helps improve future responses for all agents.
</p> </p>
</div> </div>
<Button <Button variant="default" size="sm" onClick={() => setShowApiDocs(!showApiDocs)}>
variant="default"
size="sm"
onClick={() => setShowApiDocs(!showApiDocs)}
>
Get Started Get Started
</Button> </Button>
</div> </div>

View file

@ -1,12 +1,7 @@
'use client'; 'use client';
import { Badge } from '@tpmjs/ui/Badge/Badge'; import { Badge } from '@tpmjs/ui/Badge/Badge';
import { import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
Card,
CardContent,
CardHeader,
CardTitle,
} from '@tpmjs/ui/Card/Card';
import { Icon } from '@tpmjs/ui/Icon/Icon'; import { Icon } from '@tpmjs/ui/Icon/Icon';
import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton'; import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -25,9 +20,7 @@ interface SkillsStatsProps {
collectionId: string; collectionId: string;
} }
export function SkillsStats({ export function SkillsStats({ collectionId }: SkillsStatsProps): React.ReactElement | null {
collectionId,
}: SkillsStatsProps): React.ReactElement | null {
const [stats, setStats] = useState<SkillStats | null>(null); const [stats, setStats] = useState<SkillStats | null>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
@ -35,9 +28,7 @@ export function SkillsStats({
useEffect(() => { useEffect(() => {
async function fetchStats() { async function fetchStats() {
try { try {
const response = await fetch( const response = await fetch(`/api/skills/stats?collectionId=${collectionId}`);
`/api/skills/stats?collectionId=${collectionId}`
);
if (!response.ok) { if (!response.ok) {
throw new Error('Failed to fetch stats'); throw new Error('Failed to fetch stats');
} }

View file

@ -30,9 +30,7 @@ export async function embedQuestion(text: string): Promise<number[]> {
*/ */
export function cosineSimilarity(a: number[], b: number[]): number { export function cosineSimilarity(a: number[], b: number[]): number {
if (a.length !== b.length) { if (a.length !== b.length) {
throw new Error( throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
`Vector dimension mismatch: ${a.length} vs ${b.length}`
);
} }
let dotProduct = 0; let dotProduct = 0;
@ -73,11 +71,7 @@ export async function findSimilarQuestions(
excludeId?: string; excludeId?: string;
} = {} } = {}
): Promise<SimilarQuestion[]> { ): Promise<SimilarQuestion[]> {
const { const { threshold = DEFAULT_SIMILARITY_THRESHOLD, limit = 5, excludeId } = options;
threshold = DEFAULT_SIMILARITY_THRESHOLD,
limit = 5,
excludeId,
} = options;
// Fetch all questions for this collection // Fetch all questions for this collection
const questions = await prisma.skillQuestion.findMany({ const questions = await prisma.skillQuestion.findMany({
@ -115,9 +109,7 @@ export async function findSimilarQuestions(
} }
// Sort by similarity descending and limit results // Sort by similarity descending and limit results
return similar return similar.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
.sort((a, b) => b.similarity - a.similarity)
.slice(0, limit);
} }
/** /**
@ -191,11 +183,10 @@ export async function checkQuestionSimilarity(
} }
// Find similar questions for RAG context // Find similar questions for RAG context
const similarQuestions = await findSimilarQuestions( const similarQuestions = await findSimilarQuestions(embedding, collectionId, {
embedding, threshold: DEFAULT_SIMILARITY_THRESHOLD,
collectionId, limit: 5,
{ threshold: DEFAULT_SIMILARITY_THRESHOLD, limit: 5 } });
);
return { return {
isCacheHit: false, isCacheHit: false,

View file

@ -13,10 +13,7 @@ import type { Skill, Tool } from '@prisma/client';
import { prisma } from '@tpmjs/db'; import { prisma } from '@tpmjs/db';
import { generateObject } from 'ai'; import { generateObject } from 'ai';
import { z } from 'zod'; import { z } from 'zod';
import { import { cosineSimilarity, embedQuestion } from './skills-embedding';
cosineSimilarity,
embedQuestion,
} from './skills-embedding';
const SKILL_MATCH_THRESHOLD = 0.75; const SKILL_MATCH_THRESHOLD = 0.75;
@ -50,12 +47,8 @@ export async function extractSkillsFromQuestion(
schema: z.object({ schema: z.object({
skills: z.array( skills: z.array(
z.object({ z.object({
name: z name: z.string().describe('Short skill name (2-5 words), e.g., "API error handling"'),
.string() description: z.string().describe('One sentence describing what this skill enables'),
.describe('Short skill name (2-5 words), e.g., "API error handling"'),
description: z
.string()
.describe('One sentence describing what this skill enables'),
}) })
), ),
}), }),
@ -228,11 +221,7 @@ export async function updateSkillGraph(params: {
// 2. Get or create skill nodes and link to question // 2. Get or create skill nodes and link to question
for (const extracted of extractedSkills) { for (const extracted of extractedSkills) {
try { try {
const skill = await getOrCreateSkill( const skill = await getOrCreateSkill(collectionId, extracted.name, extracted.description);
collectionId,
extracted.name,
extracted.description
);
// Link question to skill // Link question to skill
await prisma.skillQuestionSkill.upsert({ await prisma.skillQuestionSkill.upsert({
@ -314,9 +303,7 @@ export async function updateSkillGraph(params: {
/** /**
* Get skill summary for a collection * Get skill summary for a collection
*/ */
export async function getCollectionSkillsSummary( export async function getCollectionSkillsSummary(collectionId: string): Promise<{
collectionId: string
): Promise<{
totalQuestions: number; totalQuestions: number;
totalSkills: number; totalSkills: number;
topSkills: Array<{ topSkills: Array<{
@ -351,9 +338,7 @@ export async function getCollectionSkillsSummary(
* Recalculate confidence scores for all skills in a collection * Recalculate confidence scores for all skills in a collection
* (Useful for batch updates or maintenance) * (Useful for batch updates or maintenance)
*/ */
export async function recalculateSkillConfidence( export async function recalculateSkillConfidence(collectionId: string): Promise<void> {
collectionId: string
): Promise<void> {
const skills = await prisma.skill.findMany({ const skills = await prisma.skill.findMany({
where: { collectionId }, where: { collectionId },
include: { include: {

View file

@ -65,9 +65,7 @@ function buildSystemPrompt(params: GenerateResponseParams): string {
// Optional tag hints // Optional tag hints
const tagHints = const tagHints =
tags && tags.length > 0 tags && tags.length > 0 ? `\nThe user has tagged this question with: ${tags.join(', ')}` : '';
? `\nThe user has tagged this question with: ${tags.join(', ')}`
: '';
return `You are a helpful assistant that answers questions about using the tools in the "${collection.name}" collection. return `You are a helpful assistant that answers questions about using the tools in the "${collection.name}" collection.
@ -202,8 +200,7 @@ export function calculateConfidence(
// Boost for similar questions (RAG context) // Boost for similar questions (RAG context)
if (similarQuestions.length > 0) { if (similarQuestions.length > 0) {
const avgSimilarity = const avgSimilarity =
similarQuestions.reduce((sum, q) => sum + q.similarity, 0) / similarQuestions.reduce((sum, q) => sum + q.similarity, 0) / similarQuestions.length;
similarQuestions.length;
confidence += avgSimilarity * 0.4; // Up to 0.4 boost confidence += avgSimilarity * 0.4; // Up to 0.4 boost
} }

View file

@ -14,11 +14,8 @@ import { prisma } from '@tpmjs/db';
import { generateObject } from 'ai'; import { generateObject } from 'ai';
import { z } from 'zod'; import { z } from 'zod';
import { embedQuestion } from './skills-embedding'; import { embedQuestion } from './skills-embedding';
import {
generateSkillResponse,
type CollectionContext,
} from './skills-response-generator';
import { updateSkillGraph } from './skills-graph-updater'; import { updateSkillGraph } from './skills-graph-updater';
import { type CollectionContext, generateSkillResponse } from './skills-response-generator';
const SEED_BATCH_SIZE = 5; const SEED_BATCH_SIZE = 5;
const SEEDING_LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes const SEEDING_LOCK_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
@ -151,9 +148,7 @@ async function isSeeding(collectionId: string): Promise<boolean> {
* Seed a collection with synthetic questions * Seed a collection with synthetic questions
* Returns true if seeding was performed, false if skipped * Returns true if seeding was performed, false if skipped
*/ */
export async function seedCollectionSkills( export async function seedCollectionSkills(collection: CollectionWithTools): Promise<{
collection: CollectionWithTools
): Promise<{
seeded: boolean; seeded: boolean;
questionsCreated: number; questionsCreated: number;
reason?: string; reason?: string;
@ -189,10 +184,7 @@ export async function seedCollectionSkills(
// 2. Generate from tool descriptions // 2. Generate from tool descriptions
if (collection.tools.length > 0) { if (collection.tools.length > 0) {
const toolQuestions = await generateQuestionsFromTools( const toolQuestions = await generateQuestionsFromTools(collection.tools, 5);
collection.tools,
5
);
allQuestions.push(...toolQuestions); allQuestions.push(...toolQuestions);
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff