mirror of
https://github.com/eliasstepanik/core.git
synced 2026-01-10 23:58:28 +00:00
feat: track token count in recall logs and improve search query documentation
This commit is contained in:
parent
5b31c8ed62
commit
e7ed6eb288
@ -13,6 +13,7 @@ import {
|
|||||||
import { getEmbedding, makeModelCall } from "~/lib/model.server";
|
import { getEmbedding, makeModelCall } from "~/lib/model.server";
|
||||||
import { prisma } from "~/db.server";
|
import { prisma } from "~/db.server";
|
||||||
import { runQuery } from "~/lib/neo4j.server";
|
import { runQuery } from "~/lib/neo4j.server";
|
||||||
|
import { encode } from "gpt-tokenizer/encoding/o200k_base";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* SearchService provides methods to search the reified + temporal knowledge graph
|
* SearchService provides methods to search the reified + temporal knowledge graph
|
||||||
@ -162,25 +163,6 @@ export class SearchService {
|
|||||||
`confidence: ${qualityFilter.confidence.toFixed(2)}`,
|
`confidence: ${qualityFilter.confidence.toFixed(2)}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Log recall asynchronously (don't await to avoid blocking response)
|
|
||||||
const responseTime = Date.now() - startTime;
|
|
||||||
this.logRecallAsync(
|
|
||||||
query,
|
|
||||||
userId,
|
|
||||||
filteredResults.map((item) => item.statement),
|
|
||||||
opts,
|
|
||||||
responseTime,
|
|
||||||
source,
|
|
||||||
).catch((error) => {
|
|
||||||
logger.error("Failed to log recall event:", error);
|
|
||||||
});
|
|
||||||
|
|
||||||
this.updateRecallCount(
|
|
||||||
userId,
|
|
||||||
episodes,
|
|
||||||
filteredResults.map((item) => item.statement),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Replace session episodes with compacts automatically
|
// Replace session episodes with compacts automatically
|
||||||
const unifiedEpisodes = await this.replaceWithCompacts(episodes, userId);
|
const unifiedEpisodes = await this.replaceWithCompacts(episodes, userId);
|
||||||
|
|
||||||
@ -191,6 +173,41 @@ export class SearchService {
|
|||||||
relevantScore: statement.score,
|
relevantScore: statement.score,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Calculate response content for token counting
|
||||||
|
let responseContent: string;
|
||||||
|
if (opts.structured) {
|
||||||
|
responseContent = JSON.stringify({
|
||||||
|
episodes: unifiedEpisodes,
|
||||||
|
facts: factsData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
responseContent = this.formatAsMarkdown(unifiedEpisodes, factsData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Estimate token count (rough approximation: 1 token ≈ 4 characters)
|
||||||
|
const tokenCount = encode(responseContent).length;
|
||||||
|
|
||||||
|
// Update the async log with token count
|
||||||
|
const responseTime = Date.now() - startTime;
|
||||||
|
|
||||||
|
this.updateRecallCount(
|
||||||
|
userId,
|
||||||
|
episodes,
|
||||||
|
filteredResults.map((item) => item.statement),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.logRecallAsync(
|
||||||
|
query,
|
||||||
|
userId,
|
||||||
|
episodes.length,
|
||||||
|
opts,
|
||||||
|
responseTime,
|
||||||
|
source,
|
||||||
|
tokenCount,
|
||||||
|
).catch((error) => {
|
||||||
|
logger.error("Failed to log recall event:", error);
|
||||||
|
});
|
||||||
|
|
||||||
// Return markdown by default, structured JSON if requested
|
// Return markdown by default, structured JSON if requested
|
||||||
if (opts.structured) {
|
if (opts.structured) {
|
||||||
return {
|
return {
|
||||||
@ -200,47 +217,27 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Return markdown formatted context
|
// Return markdown formatted context
|
||||||
return this.formatAsMarkdown(unifiedEpisodes, factsData);
|
return responseContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async logRecallAsync(
|
private async logRecallAsync(
|
||||||
query: string,
|
query: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
results: StatementNode[],
|
episodeCount: number,
|
||||||
options: Required<SearchOptions>,
|
options: Required<SearchOptions>,
|
||||||
responseTime: number,
|
responseTime: number,
|
||||||
source?: string,
|
source?: string,
|
||||||
|
tokenCount?: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Determine target type based on results
|
// Determine target type based on episode count
|
||||||
let targetType = "mixed_results";
|
let targetType = "mixed_results";
|
||||||
if (results.length === 1) {
|
if (episodeCount === 1) {
|
||||||
targetType = "statement";
|
targetType = "episodic";
|
||||||
} else if (results.length === 0) {
|
} else if (episodeCount === 0) {
|
||||||
targetType = "no_results";
|
targetType = "no_results";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate average similarity score if available
|
|
||||||
let averageSimilarityScore: number | null = null;
|
|
||||||
const scoresWithValues = results
|
|
||||||
.map((result) => {
|
|
||||||
// Try to extract score from various possible score fields
|
|
||||||
const score =
|
|
||||||
(result as any).rrfScore ||
|
|
||||||
(result as any).mmrScore ||
|
|
||||||
(result as any).crossEncoderScore ||
|
|
||||||
(result as any).finalScore ||
|
|
||||||
(result as any).score;
|
|
||||||
return score && typeof score === "number" ? score : null;
|
|
||||||
})
|
|
||||||
.filter((score): score is number => score !== null);
|
|
||||||
|
|
||||||
if (scoresWithValues.length > 0) {
|
|
||||||
averageSimilarityScore =
|
|
||||||
scoresWithValues.reduce((sum, score) => sum + score, 0) /
|
|
||||||
scoresWithValues.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.recallLog.create({
|
await prisma.recallLog.create({
|
||||||
data: {
|
data: {
|
||||||
accessType: "search",
|
accessType: "search",
|
||||||
@ -249,8 +246,8 @@ export class SearchService {
|
|||||||
searchMethod: "hybrid", // BM25 + Vector + BFS
|
searchMethod: "hybrid", // BM25 + Vector + BFS
|
||||||
minSimilarity: options.scoreThreshold,
|
minSimilarity: options.scoreThreshold,
|
||||||
maxResults: options.limit,
|
maxResults: options.limit,
|
||||||
resultCount: results.length,
|
resultCount: episodeCount,
|
||||||
similarityScore: averageSimilarityScore,
|
similarityScore: null,
|
||||||
context: JSON.stringify({
|
context: JSON.stringify({
|
||||||
entityTypes: options.entityTypes,
|
entityTypes: options.entityTypes,
|
||||||
predicateTypes: options.predicateTypes,
|
predicateTypes: options.predicateTypes,
|
||||||
@ -262,12 +259,15 @@ export class SearchService {
|
|||||||
}),
|
}),
|
||||||
source: source ?? "search_api",
|
source: source ?? "search_api",
|
||||||
responseTimeMs: responseTime,
|
responseTimeMs: responseTime,
|
||||||
|
metadata: {
|
||||||
|
tokenCount: tokenCount || 0,
|
||||||
|
},
|
||||||
userId,
|
userId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Logged recall event for user ${userId}: ${results.length} results in ${responseTime}ms`,
|
`Logged recall event for user ${userId}: ${episodeCount} episodes, ${tokenCount} tokens in ${responseTime}ms`,
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error creating recall log entry:", { error });
|
logger.error("Error creating recall log entry:", { error });
|
||||||
|
|||||||
@ -17,7 +17,28 @@ const SearchParamsSchema = {
|
|||||||
query: {
|
query: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description:
|
description:
|
||||||
"Search query as a simple statement or question. Write what you want to find, not a command. GOOD: 'user preferences for code style' or 'previous bugs in authentication' or 'GitHub integration setup'. BAD: 'search for' or 'find me' or 'get the'. Just state the topic directly.",
|
"Search query optimized for knowledge graph retrieval. Choose the right query structure based on your search intent:\n\n" +
|
||||||
|
"1. **Entity-Centric Queries** (Best for graph search):\n" +
|
||||||
|
" - ✅ GOOD: \"User's preferences for code style and formatting\"\n" +
|
||||||
|
" - ✅ GOOD: \"Project authentication implementation decisions\"\n" +
|
||||||
|
" - ❌ BAD: \"user code style\"\n" +
|
||||||
|
" - Format: [Person/Project] + [relationship/attribute] + [context]\n\n" +
|
||||||
|
"2. **Multi-Entity Relationship Queries** (Excellent for episode graph):\n" +
|
||||||
|
" - ✅ GOOD: \"User and team discussions about API design patterns\"\n" +
|
||||||
|
" - ✅ GOOD: \"relationship between database schema and performance optimization\"\n" +
|
||||||
|
" - ❌ BAD: \"user team api design\"\n" +
|
||||||
|
" - Format: [Entity1] + [relationship type] + [Entity2] + [context]\n\n" +
|
||||||
|
"3. **Semantic Question Queries** (Good for vector search):\n" +
|
||||||
|
" - ✅ GOOD: \"What causes authentication errors in production? What are the security requirements?\"\n" +
|
||||||
|
" - ✅ GOOD: \"How does caching improve API response times compared to direct database queries?\"\n" +
|
||||||
|
" - ❌ BAD: \"auth errors production\"\n" +
|
||||||
|
" - Format: Complete natural questions with full context\n\n" +
|
||||||
|
"4. **Concept Exploration Queries** (Good for BFS traversal):\n" +
|
||||||
|
" - ✅ GOOD: \"concepts and ideas related to database indexing and query optimization\"\n" +
|
||||||
|
" - ✅ GOOD: \"topics connected to user authentication and session management\"\n" +
|
||||||
|
" - ❌ BAD: \"database indexing concepts\"\n" +
|
||||||
|
" - Format: [concept] + related/connected + [domain/context]\n\n" +
|
||||||
|
"Avoid keyword soup queries - use complete phrases with proper context for best results.",
|
||||||
},
|
},
|
||||||
validAt: {
|
validAt: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user