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:
parent
a44f38eda9
commit
234548c6b2
18 changed files with 1397 additions and 679 deletions
1
apps/web/.gitignore
vendored
1
apps/web/.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
|||
.vercel
|
||||
.env*.local
|
||||
.devtools
|
||||
|
|
|
|||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
|
|
@ -1,6 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <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
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@ai-sdk/anthropic": "^3.0.9",
|
||||
"@ai-sdk/devtools": "^0.0.8",
|
||||
"@ai-sdk/google": "^3.0.6",
|
||||
"@ai-sdk/groq": "^3.0.4",
|
||||
"@ai-sdk/mistral": "^3.0.5",
|
||||
|
|
@ -61,8 +62,8 @@
|
|||
"remark-gfm": "^4.0.1",
|
||||
"resend": "^6.7.0",
|
||||
"sonner": "^2.0.7",
|
||||
"swr": "^2.2.5",
|
||||
"streamdown": "^1.6.11",
|
||||
"swr": "^2.2.5",
|
||||
"three": "^0.182.0",
|
||||
"zod": "^4.3.5"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -8,23 +8,23 @@
|
|||
* POST - Ask a question (RAG + LLM response)
|
||||
*/
|
||||
|
||||
import { createHash } from 'crypto';
|
||||
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 { 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 {
|
||||
generateSkillResponse,
|
||||
generateFollowupSuggestions,
|
||||
calculateConfidence,
|
||||
type CollectionContext,
|
||||
calculateConfidence,
|
||||
generateFollowupSuggestions,
|
||||
generateSkillResponse,
|
||||
} from '~/lib/ai/skills-response-generator';
|
||||
import {
|
||||
seedCollectionSkills,
|
||||
getSeedingStatus,
|
||||
type CollectionWithTools,
|
||||
getSeedingStatus,
|
||||
seedCollectionSkills,
|
||||
} from '~/lib/ai/skills-seeder';
|
||||
|
||||
export const runtime = 'nodejs';
|
||||
|
|
@ -51,10 +51,7 @@ const SESSION_EXPIRY_MS = 24 * 60 * 60 * 1000;
|
|||
* Hash agent identity for anonymization
|
||||
*/
|
||||
function hashAgentIdentity(ip: string, userAgent: string): string {
|
||||
return createHash('sha256')
|
||||
.update(`${ip}:${userAgent}`)
|
||||
.digest('hex')
|
||||
.slice(0, 16);
|
||||
return createHash('sha256').update(`${ip}:${userAgent}`).digest('hex').slice(0, 16);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -70,10 +67,7 @@ async function loadCollection(
|
|||
});
|
||||
|
||||
if (!user || !user.username) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'User not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
return NextResponse.json({ success: false, error: 'User not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
const collection = await prisma.collection.findFirst({
|
||||
|
|
@ -96,10 +90,7 @@ async function loadCollection(
|
|||
});
|
||||
|
||||
if (!collection) {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Collection not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
return NextResponse.json({ success: false, error: 'Collection not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!collection.isPublic) {
|
||||
|
|
@ -160,9 +151,7 @@ export async function GET(_request: NextRequest, context: RouteContext) {
|
|||
|
||||
try {
|
||||
const { username: rawUsername, slug } = await context.params;
|
||||
const username = rawUsername.startsWith('@')
|
||||
? rawUsername.slice(1)
|
||||
: rawUsername;
|
||||
const username = rawUsername.startsWith('@') ? rawUsername.slice(1) : rawUsername;
|
||||
|
||||
// Load collection
|
||||
const result = await loadCollection(username, slug);
|
||||
|
|
@ -225,19 +214,14 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
|||
|
||||
try {
|
||||
const { username: rawUsername, slug } = await context.params;
|
||||
const username = rawUsername.startsWith('@')
|
||||
? rawUsername.slice(1)
|
||||
: rawUsername;
|
||||
const username = rawUsername.startsWith('@') ? rawUsername.slice(1) : rawUsername;
|
||||
|
||||
// Parse and validate request body
|
||||
let body: unknown;
|
||||
try {
|
||||
body = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json(
|
||||
{ success: false, error: 'Invalid JSON body' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ success: false, error: 'Invalid JSON body' }, { status: 400 });
|
||||
}
|
||||
|
||||
const parseResult = PostRequestSchema.safeParse(body);
|
||||
|
|
@ -252,8 +236,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
|||
);
|
||||
}
|
||||
|
||||
const { question, sessionId, agentName, context: questionContext, tags } =
|
||||
parseResult.data;
|
||||
const { question, sessionId, agentName, context: questionContext, tags } = parseResult.data;
|
||||
|
||||
// Load collection
|
||||
const result = await loadCollection(username, slug);
|
||||
|
|
@ -269,10 +252,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
|||
const agentHash = hashAgentIdentity(ip, userAgent);
|
||||
|
||||
// Check for similarity / cache hit
|
||||
const similarityResult = await checkQuestionSimilarity(
|
||||
question,
|
||||
collection.id
|
||||
);
|
||||
const similarityResult = await checkQuestionSimilarity(question, collection.id);
|
||||
|
||||
// If very similar question exists (>95%), return cached answer
|
||||
if (similarityResult.isCacheHit && similarityResult.cachedAnswer) {
|
||||
|
|
@ -302,8 +282,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
|||
};
|
||||
|
||||
// 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;
|
||||
|
||||
if (sessionId) {
|
||||
|
|
@ -319,9 +298,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
|||
}
|
||||
|
||||
// Generate response
|
||||
const fullQuestion = questionContext
|
||||
? `${question}\n\nContext: ${questionContext}`
|
||||
: question;
|
||||
const fullQuestion = questionContext ? `${question}\n\nContext: ${questionContext}` : question;
|
||||
|
||||
const { answer, tokensUsed } = await generateSkillResponse({
|
||||
question: fullQuestion,
|
||||
|
|
@ -401,11 +378,7 @@ export async function POST(request: NextRequest, context: RouteContext) {
|
|||
// Generate follow-up suggestions (optional, don't block)
|
||||
let suggestedFollowups: string[] = [];
|
||||
try {
|
||||
suggestedFollowups = await generateFollowupSuggestions(
|
||||
question,
|
||||
answer,
|
||||
collection.name
|
||||
);
|
||||
suggestedFollowups = await generateFollowupSuggestions(question, answer, collection.name);
|
||||
} catch {
|
||||
// Ignore errors for followups
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,8 @@
|
|||
import { Prisma, prisma } from '@tpmjs/db';
|
||||
import { registryExecuteTool } from '@tpmjs/registry-execute';
|
||||
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 { z } from 'zod';
|
||||
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 { 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
|
||||
*/
|
||||
|
|
@ -556,7 +563,16 @@ Remember: Your value is in EXECUTING tools to get real results, not just describ
|
|||
}
|
||||
|
||||
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
|
||||
const stream = new ReadableStream({
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
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 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)));
|
||||
|
||||
if (!collectionId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'collectionId is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ error: 'collectionId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -31,17 +28,11 @@ export async function GET(request: NextRequest) {
|
|||
});
|
||||
|
||||
if (!collection) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Collection not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!collection.isPublic) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Collection is not public' },
|
||||
{ status: 403 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Collection is not public' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Fetch recent questions with skill links (anonymized - no agent info)
|
||||
|
|
@ -73,9 +64,6 @@ export async function GET(request: NextRequest) {
|
|||
return NextResponse.json({ questions });
|
||||
} catch (error) {
|
||||
console.error('[Skills Activity Error]:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch activity' },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Failed to fetch activity' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
*/
|
||||
|
||||
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 dynamic = 'force-dynamic';
|
||||
|
|
@ -15,10 +15,7 @@ export async function GET(request: NextRequest) {
|
|||
const collectionId = searchParams.get('collectionId');
|
||||
|
||||
if (!collectionId) {
|
||||
return NextResponse.json(
|
||||
{ error: 'collectionId is required' },
|
||||
{ status: 400 }
|
||||
);
|
||||
return NextResponse.json({ error: 'collectionId is required' }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -29,17 +26,11 @@ export async function GET(request: NextRequest) {
|
|||
});
|
||||
|
||||
if (!collection) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Collection not found' },
|
||||
{ status: 404 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Collection not found' }, { status: 404 });
|
||||
}
|
||||
|
||||
if (!collection.isPublic) {
|
||||
return NextResponse.json(
|
||||
{ error: 'Collection is not public' },
|
||||
{ status: 403 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Collection is not public' }, { status: 403 });
|
||||
}
|
||||
|
||||
// Fetch stats in parallel
|
||||
|
|
@ -65,9 +56,6 @@ export async function GET(request: NextRequest) {
|
|||
});
|
||||
} catch (error) {
|
||||
console.error('[Skills Stats Error]:', error);
|
||||
return NextResponse.json(
|
||||
{ error: 'Failed to fetch stats' },
|
||||
{ status: 500 }
|
||||
);
|
||||
return NextResponse.json({ error: 'Failed to fetch stats' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -251,11 +251,7 @@ export default function SkillsPage(): React.ReactElement {
|
|||
/>
|
||||
|
||||
<h4 className="font-semibold mt-4">Example Response</h4>
|
||||
<CodeBlock
|
||||
code={exampleResponseJson}
|
||||
language="json"
|
||||
showCopy={true}
|
||||
/>
|
||||
<CodeBlock code={exampleResponseJson} language="json" showCopy={true} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
|
@ -353,7 +349,9 @@ const followUp = await askSkills(
|
|||
<h4 className="font-semibold text-sm mb-2">Tips for Better Responses</h4>
|
||||
<ul className="text-foreground-secondary text-sm space-y-1">
|
||||
<li>• Be specific about what you'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 sessions for related follow-up questions</li>
|
||||
</ul>
|
||||
|
|
@ -413,8 +411,8 @@ const followUp = await askSkills(
|
|||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-foreground-secondary">
|
||||
When a collection's skills endpoint is accessed for the first time, it automatically
|
||||
seeds with synthetic questions generated from:
|
||||
When a collection's skills endpoint is accessed for the first time, it
|
||||
automatically seeds with synthetic questions generated from:
|
||||
</p>
|
||||
|
||||
<ul className="list-disc list-inside space-y-2 text-foreground-secondary">
|
||||
|
|
@ -424,8 +422,8 @@ const followUp = await askSkills(
|
|||
</ul>
|
||||
|
||||
<p className="text-foreground-secondary mt-4">
|
||||
This ensures the endpoint is useful immediately, even before any real agent interactions.
|
||||
Seeding typically adds 10-15 synthetic Q&A pairs.
|
||||
This ensures the endpoint is useful immediately, even before any real agent
|
||||
interactions. Seeding typically adds 10-15 synthetic Q&A pairs.
|
||||
</p>
|
||||
|
||||
<div className="bg-background-secondary p-4 rounded-lg mt-4">
|
||||
|
|
@ -490,7 +488,10 @@ const followUp = await askSkills(
|
|||
</p>
|
||||
</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 →
|
||||
</Link>
|
||||
<p className="text-foreground-secondary text-sm">
|
||||
|
|
|
|||
|
|
@ -1,15 +1,10 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@tpmjs/ui/Card/Card';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton';
|
||||
|
||||
// Simple relative time formatter
|
||||
function formatRelativeTime(date: Date): string {
|
||||
const now = Date.now();
|
||||
|
|
@ -24,6 +19,7 @@ function formatRelativeTime(date: Date): string {
|
|||
if (minutes > 0) return `${minutes}m ago`;
|
||||
return 'just now';
|
||||
}
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
interface SkillQuestion {
|
||||
|
|
@ -104,11 +100,7 @@ export function SkillsActivityFeed({
|
|||
return (
|
||||
<Card variant="default" className="border-dashed">
|
||||
<CardContent padding="lg" className="text-center">
|
||||
<Icon
|
||||
icon="message"
|
||||
size="lg"
|
||||
className="mx-auto text-foreground-tertiary mb-2"
|
||||
/>
|
||||
<Icon icon="message" size="lg" className="mx-auto text-foreground-tertiary mb-2" />
|
||||
<p className="text-foreground-secondary text-sm">
|
||||
No questions yet. Be the first to ask!
|
||||
</p>
|
||||
|
|
@ -127,10 +119,7 @@ export function SkillsActivityFeed({
|
|||
{q.question}
|
||||
</CardTitle>
|
||||
<div className="flex items-center gap-1 flex-shrink-0">
|
||||
<Badge
|
||||
variant={q.confidence >= 0.7 ? 'success' : 'secondary'}
|
||||
size="sm"
|
||||
>
|
||||
<Badge variant={q.confidence >= 0.7 ? 'success' : 'secondary'} size="sm">
|
||||
{Math.round(q.confidence * 100)}%
|
||||
</Badge>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -67,11 +67,7 @@ curl -X POST "${skillsUrl}" \\
|
|||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => setShowApiDocs(!showApiDocs)}
|
||||
>
|
||||
<Button variant="ghost" size="sm" onClick={() => setShowApiDocs(!showApiDocs)}>
|
||||
<Icon icon="terminal" className="w-4 h-4 mr-1" />
|
||||
API
|
||||
</Button>
|
||||
|
|
@ -122,9 +118,7 @@ curl -X POST "${skillsUrl}" \\
|
|||
{/* Activity Feed Column */}
|
||||
<div className="md:col-span-2">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<h3 className="text-sm font-medium text-foreground-secondary">
|
||||
Recent Questions
|
||||
</h3>
|
||||
<h3 className="text-sm font-medium text-foreground-secondary">Recent Questions</h3>
|
||||
</div>
|
||||
<SkillsActivityFeed collectionId={collectionId} limit={5} />
|
||||
</div>
|
||||
|
|
@ -144,11 +138,7 @@ curl -X POST "${skillsUrl}" \\
|
|||
Every question helps improve future responses for all agents.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => setShowApiDocs(!showApiDocs)}
|
||||
>
|
||||
<Button variant="default" size="sm" onClick={() => setShowApiDocs(!showApiDocs)}>
|
||||
Get Started
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import { Badge } from '@tpmjs/ui/Badge/Badge';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@tpmjs/ui/Card/Card';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@tpmjs/ui/Card/Card';
|
||||
import { Icon } from '@tpmjs/ui/Icon/Icon';
|
||||
import { Skeleton } from '@tpmjs/ui/Skeleton/Skeleton';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
|
@ -25,9 +20,7 @@ interface SkillsStatsProps {
|
|||
collectionId: string;
|
||||
}
|
||||
|
||||
export function SkillsStats({
|
||||
collectionId,
|
||||
}: SkillsStatsProps): React.ReactElement | null {
|
||||
export function SkillsStats({ collectionId }: SkillsStatsProps): React.ReactElement | null {
|
||||
const [stats, setStats] = useState<SkillStats | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
|
@ -35,9 +28,7 @@ export function SkillsStats({
|
|||
useEffect(() => {
|
||||
async function fetchStats() {
|
||||
try {
|
||||
const response = await fetch(
|
||||
`/api/skills/stats?collectionId=${collectionId}`
|
||||
);
|
||||
const response = await fetch(`/api/skills/stats?collectionId=${collectionId}`);
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch stats');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,9 +30,7 @@ export async function embedQuestion(text: string): Promise<number[]> {
|
|||
*/
|
||||
export function cosineSimilarity(a: number[], b: number[]): number {
|
||||
if (a.length !== b.length) {
|
||||
throw new Error(
|
||||
`Vector dimension mismatch: ${a.length} vs ${b.length}`
|
||||
);
|
||||
throw new Error(`Vector dimension mismatch: ${a.length} vs ${b.length}`);
|
||||
}
|
||||
|
||||
let dotProduct = 0;
|
||||
|
|
@ -73,11 +71,7 @@ export async function findSimilarQuestions(
|
|||
excludeId?: string;
|
||||
} = {}
|
||||
): Promise<SimilarQuestion[]> {
|
||||
const {
|
||||
threshold = DEFAULT_SIMILARITY_THRESHOLD,
|
||||
limit = 5,
|
||||
excludeId,
|
||||
} = options;
|
||||
const { threshold = DEFAULT_SIMILARITY_THRESHOLD, limit = 5, excludeId } = options;
|
||||
|
||||
// Fetch all questions for this collection
|
||||
const questions = await prisma.skillQuestion.findMany({
|
||||
|
|
@ -115,9 +109,7 @@ export async function findSimilarQuestions(
|
|||
}
|
||||
|
||||
// Sort by similarity descending and limit results
|
||||
return similar
|
||||
.sort((a, b) => b.similarity - a.similarity)
|
||||
.slice(0, limit);
|
||||
return similar.sort((a, b) => b.similarity - a.similarity).slice(0, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -191,11 +183,10 @@ export async function checkQuestionSimilarity(
|
|||
}
|
||||
|
||||
// Find similar questions for RAG context
|
||||
const similarQuestions = await findSimilarQuestions(
|
||||
embedding,
|
||||
collectionId,
|
||||
{ threshold: DEFAULT_SIMILARITY_THRESHOLD, limit: 5 }
|
||||
);
|
||||
const similarQuestions = await findSimilarQuestions(embedding, collectionId, {
|
||||
threshold: DEFAULT_SIMILARITY_THRESHOLD,
|
||||
limit: 5,
|
||||
});
|
||||
|
||||
return {
|
||||
isCacheHit: false,
|
||||
|
|
|
|||
|
|
@ -13,10 +13,7 @@ import type { Skill, Tool } from '@prisma/client';
|
|||
import { prisma } from '@tpmjs/db';
|
||||
import { generateObject } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
cosineSimilarity,
|
||||
embedQuestion,
|
||||
} from './skills-embedding';
|
||||
import { cosineSimilarity, embedQuestion } from './skills-embedding';
|
||||
|
||||
const SKILL_MATCH_THRESHOLD = 0.75;
|
||||
|
||||
|
|
@ -50,12 +47,8 @@ export async function extractSkillsFromQuestion(
|
|||
schema: z.object({
|
||||
skills: z.array(
|
||||
z.object({
|
||||
name: z
|
||||
.string()
|
||||
.describe('Short skill name (2-5 words), e.g., "API error handling"'),
|
||||
description: z
|
||||
.string()
|
||||
.describe('One sentence describing what this skill enables'),
|
||||
name: z.string().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
|
||||
for (const extracted of extractedSkills) {
|
||||
try {
|
||||
const skill = await getOrCreateSkill(
|
||||
collectionId,
|
||||
extracted.name,
|
||||
extracted.description
|
||||
);
|
||||
const skill = await getOrCreateSkill(collectionId, extracted.name, extracted.description);
|
||||
|
||||
// Link question to skill
|
||||
await prisma.skillQuestionSkill.upsert({
|
||||
|
|
@ -314,9 +303,7 @@ export async function updateSkillGraph(params: {
|
|||
/**
|
||||
* Get skill summary for a collection
|
||||
*/
|
||||
export async function getCollectionSkillsSummary(
|
||||
collectionId: string
|
||||
): Promise<{
|
||||
export async function getCollectionSkillsSummary(collectionId: string): Promise<{
|
||||
totalQuestions: number;
|
||||
totalSkills: number;
|
||||
topSkills: Array<{
|
||||
|
|
@ -351,9 +338,7 @@ export async function getCollectionSkillsSummary(
|
|||
* Recalculate confidence scores for all skills in a collection
|
||||
* (Useful for batch updates or maintenance)
|
||||
*/
|
||||
export async function recalculateSkillConfidence(
|
||||
collectionId: string
|
||||
): Promise<void> {
|
||||
export async function recalculateSkillConfidence(collectionId: string): Promise<void> {
|
||||
const skills = await prisma.skill.findMany({
|
||||
where: { collectionId },
|
||||
include: {
|
||||
|
|
|
|||
|
|
@ -65,9 +65,7 @@ function buildSystemPrompt(params: GenerateResponseParams): string {
|
|||
|
||||
// Optional tag hints
|
||||
const tagHints =
|
||||
tags && tags.length > 0
|
||||
? `\nThe user has tagged this question with: ${tags.join(', ')}`
|
||||
: '';
|
||||
tags && tags.length > 0 ? `\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.
|
||||
|
||||
|
|
@ -202,8 +200,7 @@ export function calculateConfidence(
|
|||
// Boost for similar questions (RAG context)
|
||||
if (similarQuestions.length > 0) {
|
||||
const avgSimilarity =
|
||||
similarQuestions.reduce((sum, q) => sum + q.similarity, 0) /
|
||||
similarQuestions.length;
|
||||
similarQuestions.reduce((sum, q) => sum + q.similarity, 0) / similarQuestions.length;
|
||||
confidence += avgSimilarity * 0.4; // Up to 0.4 boost
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -14,11 +14,8 @@ import { prisma } from '@tpmjs/db';
|
|||
import { generateObject } from 'ai';
|
||||
import { z } from 'zod';
|
||||
import { embedQuestion } from './skills-embedding';
|
||||
import {
|
||||
generateSkillResponse,
|
||||
type CollectionContext,
|
||||
} from './skills-response-generator';
|
||||
import { updateSkillGraph } from './skills-graph-updater';
|
||||
import { type CollectionContext, generateSkillResponse } from './skills-response-generator';
|
||||
|
||||
const SEED_BATCH_SIZE = 5;
|
||||
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
|
||||
* Returns true if seeding was performed, false if skipped
|
||||
*/
|
||||
export async function seedCollectionSkills(
|
||||
collection: CollectionWithTools
|
||||
): Promise<{
|
||||
export async function seedCollectionSkills(collection: CollectionWithTools): Promise<{
|
||||
seeded: boolean;
|
||||
questionsCreated: number;
|
||||
reason?: string;
|
||||
|
|
@ -189,10 +184,7 @@ export async function seedCollectionSkills(
|
|||
|
||||
// 2. Generate from tool descriptions
|
||||
if (collection.tools.length > 0) {
|
||||
const toolQuestions = await generateQuestionsFromTools(
|
||||
collection.tools,
|
||||
5
|
||||
);
|
||||
const toolQuestions = await generateQuestionsFromTools(collection.tools, 5);
|
||||
allQuestions.push(...toolQuestions);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue