2026-05-02 03:18:47 +02:00
/**
* Knowledge Base API Client
*
* Semantic search and RAG queries against indexed knowledge.
* Matches backend: roboco/api/routes/optimal.py
*/
import api from "./client" ;
import {
KBIndexType ,
type KBSearchRequest ,
type KBSearchResponse ,
type KBSearchResult ,
type RAGQueryRequest ,
type RAGQueryResponse ,
type KBStats ,
type KBIndexStats ,
type RAGHealthResponse ,
type MentorAskRequest ,
type MentorAskResponse ,
type ErrorSearchRequest ,
type ErrorSearchResponse ,
type ErrorRecordRequest ,
type ErrorRecordResponse ,
type DecisionCheckRequest ,
type DecisionCheckResponse ,
type DecisionRecordRequest ,
type DecisionRecordResponse ,
type StandardsGetRequest ,
type StandardsGetResponse ,
type ValidateActionRequest ,
type ValidateActionResponse ,
type CodeReviewRequest ,
type CodeReviewResponse ,
type LearningRecordRequest ,
type LearningRecordResponse ,
type LearningSearchRequest ,
type ProactiveContextRequest ,
type ProactiveContextResponse ,
type TokenEstimateRequest ,
type TokenEstimateResponse ,
type RefreshIndexRequest ,
type RefreshIndexResponse ,
type ClearIndexResponse ,
type ReindexResponse ,
type ReindexRequest ,
type IndexStalenessResponse ,
} from "@/types" ;
import { isMockMode } from "@/lib/mock-data" ;
// =============================================================================
// MOCK DATA
// =============================================================================
const mockSearchResults : KBSearchResult [] = [
{
content : "## Task Lifecycle\n\nTasks follow a strict state machine from PENDING through IN_PROGRESS to COMPLETED. Each transition requires specific conditions..." ,
source : "docs/architecture/task-lifecycle.md" ,
score : 0.88 ,
index_type : KBIndexType.DOCUMENTATION ,
metadata : { section : "Architecture" },
},
{
content : "BE-DEV-1: I've completed the task validation logic. The acceptance criteria now require at least one item before a task can be created." ,
source : "channel:backend/session:abc123" ,
score : 0.75 ,
index_type : KBIndexType.CONVERSATIONS ,
metadata : { agent : "be-dev-1" , timestamp : "2024-01-15T10:30:00Z" },
},
{
content : "Learned that proper error boundaries in the task form prevent cascading failures. Applied this pattern to all form components." ,
source : "journal:be-dev-1/entry:xyz789" ,
score : 0.71 ,
index_type : KBIndexType.JOURNALS ,
metadata : { agent : "be-dev-1" , type : "learning" },
},
];
const mockStats : KBStats = {
indexes : [
{ index_type : KBIndexType.DOCUMENTATION , document_count : 45 , chunk_count : 320 , last_updated : "2024-01-15T11:30:00Z" },
{ index_type : KBIndexType.CONVERSATIONS , document_count : 890 , chunk_count : 4200 , last_updated : "2024-01-15T12:15:00Z" },
{ index_type : KBIndexType.JOURNALS , document_count : 156 , chunk_count : 780 , last_updated : "2024-01-15T10:00:00Z" },
{ index_type : KBIndexType.ERRORS , document_count : 45 , chunk_count : 180 , last_updated : "2024-01-15T10:00:00Z" },
{ index_type : KBIndexType.STANDARDS , document_count : 12 , chunk_count : 60 , last_updated : "2024-01-15T10:00:00Z" },
{ index_type : KBIndexType.DECISIONS , document_count : 78 , chunk_count : 390 , last_updated : "2024-01-15T10:00:00Z" },
{ index_type : KBIndexType.REVIEWS , document_count : 234 , chunk_count : 1170 , last_updated : "2024-01-15T10:00:00Z" },
{ index_type : KBIndexType.LEARNINGS , document_count : 89 , chunk_count : 445 , last_updated : "2024-01-15T10:00:00Z" },
],
2026-06-15 04:55:16 +02:00
total_documents : 1549 ,
total_chunks : 6745 ,
2026-05-02 03:18:47 +02:00
};
// Backend returns stats as dict, we need to transform to array
interface BackendIndexStats {
initialized : boolean ;
indexes : Record < string , { document_count : number ; chunk_count : number ; last_updated : string | null }>;
}
function transformStatsResponse ( backendStats : BackendIndexStats ) : KBStats {
const indexes : KBIndexStats [] = Object . entries ( backendStats . indexes || {}). map (([ indexType , stats ]) => ({
index_type : indexType as KBIndexType ,
document_count : stats.document_count ?? 0 ,
chunk_count : stats.chunk_count ?? 0 ,
last_updated : stats.last_updated ?? null ,
}));
const total_documents = indexes . reduce (( sum , idx ) => sum + idx . document_count , 0 );
const total_chunks = indexes . reduce (( sum , idx ) => sum + idx . chunk_count , 0 );
return { indexes , total_documents , total_chunks };
}
// =============================================================================
// SEARCH ENDPOINTS
// =============================================================================
/**
* Semantic search across indexed content
*/
async function search ( params : KBSearchRequest ) : Promise < KBSearchResponse > {
if ( isMockMode ()) {
// Filter by index types if specified
let results = [... mockSearchResults ];
if ( params . index_types && params . index_types . length > 0 ) {
results = results . filter (( r ) => params . index_types ! . includes ( r . index_type ));
}
// Filter by min score
if ( params . min_score ) {
results = results . filter (( r ) => r . score >= params . min_score ! );
}
// Limit results
const topK = params . top_k ?? 10 ;
results = results . slice ( 0 , topK );
return {
results ,
total : results.length ,
query : params.query ,
};
}
const response = await api . post < KBSearchResponse >( "/optimal/kb/search" , params );
return response . data ;
}
// =============================================================================
// RAG ENDPOINTS
// =============================================================================
/**
* RAG query - ask a question, get AI answer with citations
*/
async function ragQuery ( params : RAGQueryRequest ) : Promise < RAGQueryResponse > {
if ( isMockMode ()) {
// Simulate RAG response with mock data
return {
answer : `Based on the indexed knowledge, here's what I found about " ${ params . question } ": \ n \ nThe system uses a structured approach where tasks follow a defined lifecycle. Each task starts in PENDING state and can transition through various states like IN_PROGRESS, BLOCKED, and eventually COMPLETED or CANCELLED. \ n \ nKey points: \ n- Tasks require acceptance criteria before creation \ n- State transitions are validated by the TaskService \ n- Agents can claim and work on tasks based on their role and team` ,
citations : mockSearchResults.slice ( 0 , 3 ). map (( r ) => ({
content : r.content ,
source : r.source ,
score : r.score ,
index_type : r.index_type ,
metadata : r.metadata ,
})),
query : params.question ,
context_used : 3 ,
};
}
// Transform frontend params to backend format
const backendParams = {
query : params.question ,
index_types : params.index_types ,
top_k : params.max_context_chunks ?? 5 ,
};
const response = await api . post < RAGQueryResponse >( "/optimal/rag/query" , backendParams );
return response . data ;
}
/**
* Get context for a query without generating an answer
*/
async function getContext ( params : RAGQueryRequest ) : Promise < KBSearchResult [] > {
if ( isMockMode ()) {
return mockSearchResults . slice ( 0 , params . max_context_chunks ?? 5 );
}
// Transform frontend params to backend format
const backendParams = {
query : params.question ,
index_types : params.index_types ,
top_k : params.max_context_chunks ?? 5 ,
};
2026-06-15 04:55:16 +02:00
const response = await api . post < KBSearchResponse >( "/optimal/rag/context" , backendParams );
return response . data . results ;
2026-05-02 03:18:47 +02:00
}
// =============================================================================
// STATS ENDPOINTS
// =============================================================================
/**
* Get index statistics
*/
async function getStats () : Promise < KBStats > {
if ( isMockMode ()) {
return mockStats ;
}
const response = await api . get < BackendIndexStats >( "/optimal/stats" );
return transformStatsResponse ( response . data );
}
/**
* Get stats for a specific index type
*/
async function getIndexStats ( indexType : KBIndexType ) : Promise < KBIndexStats > {
if ( isMockMode ()) {
const stats = mockStats . indexes . find (( i ) => i . index_type === indexType );
if ( stats ) return stats ;
return {
index_type : indexType ,
document_count : 0 ,
chunk_count : 0 ,
last_updated : null ,
};
}
const response = await api . get < KBIndexStats >( `/optimal/stats/ ${ indexType } ` );
return response . data ;
}
// =============================================================================
// BROWSE ENDPOINTS
// =============================================================================
/**
* List documents in a specific index (for browsing)
*/
async function listDocuments (
indexType : KBIndexType ,
params ?: { limit? : number ; offset? : number }
) : Promise < { documents : Array < { id : string ; source : string ; indexed_at : string ; metadata? : Record < string , unknown > } > ; total : number } > {
if ( isMockMode ()) {
// Generate mock document list
const docs = mockSearchResults
. filter (( r ) => r . index_type === indexType )
. map (( r , idx ) => ({
id : `doc- ${ indexType } - ${ idx } ` ,
source : r.source ,
indexed_at : "2024-01-15T12:00:00Z" ,
}));
return { documents : docs , total : docs.length };
}
const response = await api . get < { documents : Array < { id : string ; source : string ; indexed_at : string ; metadata? : Record < string , unknown > } > ; total : number ; index_type : string } > (
`/optimal/kb/ ${ indexType } /documents` ,
{ params }
);
return { documents : response.data.documents , total : response.data.total };
}
// =============================================================================
// HEALTH ENDPOINT
// =============================================================================
/**
* Get RAG system health status
*/
async function getHealth () : Promise < RAGHealthResponse > {
if ( isMockMode ()) {
return {
healthy : true ,
embedding_status : "healthy" ,
llm_status : "healthy" ,
vector_store_status : "healthy" ,
details : {},
};
}
const response = await api . get < RAGHealthResponse >( "/optimal/health" );
return response . data ;
}
// =============================================================================
// INDEX MANAGEMENT
// =============================================================================
/**
* Delete/clear an index
*/
async function deleteIndex ( indexType : KBIndexType ) : Promise < ClearIndexResponse > {
if ( isMockMode ()) {
return { status : "cleared" , index_type : indexType };
}
const response = await api . delete < ClearIndexResponse >( `/optimal/kb/ ${ indexType } ` );
return response . data ;
}
/**
* Refresh an index with updated sources
*/
async function refreshIndex ( request : RefreshIndexRequest ) : Promise < RefreshIndexResponse > {
if ( isMockMode ()) {
return { status : "refreshed" , index_type : request.index_type , sources : request.sources };
}
const response = await api . post < RefreshIndexResponse >( "/optimal/kb/refresh" , request );
return response . data ;
}
/**
* Trigger full reindex with detailed reporting
*
* @param request - Optional parameters for reindexing
* @param request.force - Force reindex even if indexes have content
* @param request.timeout_seconds - Max time to wait (default: 300s)
* @returns Detailed report of what was indexed, failed, and skipped
*/
async function reindexAll ( request? : ReindexRequest ) : Promise < ReindexResponse > {
if ( isMockMode ()) {
return {
status : "reindexed" ,
code : {
index_type : "code" ,
total_attempted : 100 ,
successful : 98 ,
failed : 2 ,
skipped : 0 ,
success_rate : 98.0 ,
has_failures : true ,
failed_sources : [
[ "/path/to/file1.py" , "UTF-8 decode error" ],
[ "/path/to/file2.py" , "File too large" ],
],
duration_seconds : 45.2 ,
},
documentation : {
index_type : "documentation" ,
total_attempted : 50 ,
successful : 50 ,
failed : 0 ,
skipped : 0 ,
success_rate : 100.0 ,
has_failures : false ,
failed_sources : [],
duration_seconds : 12.5 ,
},
overall_success : true ,
warnings : [ "Code indexing had 2 failures" ],
// Legacy fields
code_count : 98 ,
docs_count : 50 ,
};
}
const response = await api . post < ReindexResponse >( "/optimal/kb/reindex" , null , {
params : {
force : request?.force ?? false ,
timeout_seconds : request?.timeout_seconds ?? 300 ,
},
});
return response . data ;
}
/**
* Check if indexes are stale (source files modified after last indexing)
*
* @returns Staleness info for each index type
*/
async function checkStaleness () : Promise < IndexStalenessResponse > {
if ( isMockMode ()) {
return {
needs_reindex : true ,
stale_indexes : [ "code" ],
details : {
code : {
status : "stale" ,
last_indexed : new Date ( Date . now () - 86400000 ). toISOString (),
stale_file_count : 5 ,
stale_files_sample : [
"/roboco/services/task.py" ,
"/roboco/services/optimal.py" ,
],
recommendation : "Run /kb/reindex?force=true to update" ,
},
documentation : {
status : "current" ,
last_indexed : new Date (). toISOString (),
indexed_sources_count : 50 ,
},
},
};
}
const response = await api . get < IndexStalenessResponse >(
"/optimal/stats/staleness"
);
return response . data ;
}
// =============================================================================
// MENTOR ENDPOINTS
// =============================================================================
/**
* Ask the mentor for help
*/
async function askMentor ( request : MentorAskRequest ) : Promise < MentorAskResponse > {
if ( isMockMode ()) {
return {
answer : `Here's what I found about " ${ request . question } ": \ n \ nBased on our organizational knowledge, I recommend following the established patterns and consulting the relevant documentation.` ,
sources : mockSearchResults.slice ( 0 , 2 ),
conversation_id : "conv-" + Date . now (),
suggested_followups : [ "Can you elaborate?" , "What are the alternatives?" ],
};
}
const response = await api . post < MentorAskResponse >( "/optimal/mentor/ask" , request );
return response . data ;
}
// =============================================================================
// ERROR ENDPOINTS
// =============================================================================
/**
* Search for known error solutions
*/
async function searchErrors ( request : ErrorSearchRequest ) : Promise < ErrorSearchResponse > {
if ( isMockMode ()) {
return { results : [], total : 0 };
}
const response = await api . post < ErrorSearchResponse >( "/optimal/errors/search" , request );
return response . data ;
}
/**
* Record an error solution
*/
async function recordError ( request : ErrorRecordRequest ) : Promise < ErrorRecordResponse > {
if ( isMockMode ()) {
return { error_id : "err-" + Date . now (), status : "recorded" };
}
const response = await api . post < ErrorRecordResponse >( "/optimal/errors/record" , request );
return response . data ;
}
// =============================================================================
// DECISION ENDPOINTS
// =============================================================================
/**
* Check if a similar decision was made before
*/
async function checkDecision ( request : DecisionCheckRequest ) : Promise < DecisionCheckResponse > {
if ( isMockMode ()) {
return { has_precedent : false , decisions : [], recommendation : "No similar decisions found" };
}
const response = await api . post < DecisionCheckResponse >( "/optimal/decisions/check" , request );
return response . data ;
}
/**
* Record a decision for future reference
*/
async function recordDecision ( request : DecisionRecordRequest ) : Promise < DecisionRecordResponse > {
if ( isMockMode ()) {
return { decision_id : "dec-" + Date . now (), status : "recorded" };
}
const response = await api . post < DecisionRecordResponse >( "/optimal/decisions/record" , request );
return response . data ;
}
// =============================================================================
// STANDARDS ENDPOINTS
// =============================================================================
/**
* Get standards for a domain
*/
async function getStandards ( request : StandardsGetRequest ) : Promise < StandardsGetResponse > {
if ( isMockMode ()) {
return { standards : [], total : 0 };
}
const response = await api . post < StandardsGetResponse >( "/optimal/standards/get" , request );
return response . data ;
}
/**
* Validate an action against standards
*/
async function validateAction ( request : ValidateActionRequest ) : Promise < ValidateActionResponse > {
if ( isMockMode ()) {
return { allowed : true , violations : [], warnings : [], relevant_standards : [] };
}
const response = await api . post < ValidateActionResponse >( "/optimal/standards/validate" , request );
return response . data ;
}
// =============================================================================
// CODE REVIEW ENDPOINTS
// =============================================================================
/**
* Review code against standards
*/
async function reviewCode ( request : CodeReviewRequest ) : Promise < CodeReviewResponse > {
if ( isMockMode ()) {
return {
file_path : request.file_path ,
approved : true ,
score : 85 ,
comments : [],
standards_checked : [ "coding-standards" ],
similar_reviews : [],
};
}
const response = await api . post < CodeReviewResponse >( "/optimal/review/code" , request );
return response . data ;
}
// =============================================================================
// LEARNING ENDPOINTS
// =============================================================================
/**
* Record a learning for knowledge sharing
*/
async function recordLearning ( request : LearningRecordRequest ) : Promise < LearningRecordResponse > {
if ( isMockMode ()) {
return { learning_id : "learn-" + Date . now (), status : "recorded" };
}
const response = await api . post < LearningRecordResponse >( "/optimal/learnings/record" , request );
return response . data ;
}
/**
* Search for relevant learnings
*/
async function searchLearnings ( request : LearningSearchRequest ) : Promise < KBSearchResponse > {
if ( isMockMode ()) {
return { results : [], total : 0 , query : request.query };
}
const response = await api . post < KBSearchResponse >( "/optimal/learnings/search" , request );
return response . data ;
}
// =============================================================================
// PROACTIVE CONTEXT ENDPOINTS
// =============================================================================
/**
* Get proactive context for a task
*/
async function getProactiveContext ( request : ProactiveContextRequest ) : Promise < ProactiveContextResponse > {
if ( isMockMode ()) {
return {
task_id : request.task_id ,
similar_tasks : [],
relevant_learnings : [],
code_patterns : [],
applicable_standards : [],
recent_decisions : [],
known_issues : [],
summary : "No relevant context found for this task." ,
};
}
const response = await api . post < ProactiveContextResponse >( "/optimal/context/proactive" , request );
return response . data ;
}
// =============================================================================
// TOKEN ESTIMATION
// =============================================================================
/**
* Estimate token count for content
*/
async function estimateTokens ( request : TokenEstimateRequest ) : Promise < TokenEstimateResponse > {
if ( isMockMode ()) {
return {
token_count : Math.ceil ( request . content . length / 4 ),
model : request.model || "claude-sonnet-4-20250514" ,
content_length : request.content.length ,
};
}
const response = await api . post < TokenEstimateResponse >( "/optimal/tokens/estimate" , request );
return response . data ;
}
// =============================================================================
// EXPORT API
// =============================================================================
export const knowledgeBaseApi = {
// Search
search ,
// RAG
ragQuery ,
getContext ,
// Stats & Health
getStats ,
getIndexStats ,
getHealth ,
// Browse
listDocuments ,
// Index Management
deleteIndex ,
refreshIndex ,
reindexAll ,
checkStaleness ,
// Mentor
askMentor ,
// Errors
searchErrors ,
recordError ,
// Decisions
checkDecision ,
recordDecision ,
// Standards
getStandards ,
validateAction ,
// Code Review
reviewCode ,
// Learnings
recordLearning ,
searchLearnings ,
// Proactive Context
getProactiveContext ,
// Token Estimation
estimateTokens ,
};
export default knowledgeBaseApi ;