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
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -55,6 +55,9 @@ secrets.json
|
||||||
# turbo
|
# turbo
|
||||||
.turbo
|
.turbo
|
||||||
|
|
||||||
|
# ai sdk devtools
|
||||||
|
.devtools
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
|
|
||||||
1
apps/web/.gitignore
vendored
1
apps/web/.gitignore
vendored
|
|
@ -1,2 +1,3 @@
|
||||||
.vercel
|
.vercel
|
||||||
.env*.local
|
.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" />
|
||||||
/// <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.
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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({
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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're trying to accomplish</li>
|
<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 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's skills endpoint is accessed for the first time, it automatically
|
When a collection'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&A pairs.
|
interactions. Seeding typically adds 10-15 synthetic Q&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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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');
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue