@agentics.org/sparc2
Version:
SPARC 2.0 - Autonomous Vector Coding Agent + MCP. SPARC 2.0, vectorized AI code analysis, is an intelligent coding agent framework built to automate and streamline software development. It combines secure execution environments, and version control into a
384 lines (330 loc) • 11.5 kB
text/typescript
/**
* VectorStore module for SPARC 2.0
* Provides methods to index diff logs and perform vector searches
* using OpenAI's vector store API
*/
import { LogEntry } from "../logger.ts";
import { load } from "https://deno.land/std@0.203.0/dotenv/mod.ts";
// Dynamically import OpenAI to avoid issues in test environments
let OpenAI: any;
try {
OpenAI = (await import("jsr:@openai/openai")).default;
} catch (error) {
console.debug("OpenAI import failed, using mock implementation", { error: String(error) });
}
/**
* Diff entry interface for storing code changes
*/
export interface DiffEntry {
id: string;
file: string;
diff: string;
metadata: Record<string, unknown>;
}
/**
* Vector search result interface
*/
export interface VectorSearchResult {
entry: LogEntry | DiffEntry;
score: number;
}
// Store ID for the vector store
let vectorStoreId: string | null = null;
// Initialize OpenAI client if API key is available
let openai: any = null;
let apiKey: string | null = null;
// Try to load environment variables
try {
// Only try to load environment variables if not in test mode
if (Deno.env.get("DENO_ENV") !== "test") {
await load({ export: true, allowEmptyValues: true });
}
apiKey = Deno.env.get("OPENAI_API_KEY") || Deno.env.get("VITE_OPENAI_API_KEY") || null;
if (apiKey && OpenAI) {
openai = new OpenAI({
apiKey,
});
console.debug("OpenAI client initialized");
}
} catch (error) {
console.debug("Failed to load environment variables", { error: String(error) });
}
/**
* Initialize the vector store
* @param name Name for the vector store
* @returns The vector store ID
*/
export async function initializeVectorStore(name: string = "sparc2-vector-store"): Promise<string> {
try {
// If we already have a vector store ID, return it
if (vectorStoreId !== null) {
console.debug("Using existing vector store", { id: vectorStoreId });
return vectorStoreId as string;
}
// If OpenAI client is not available, use mock implementation
if (!openai) {
vectorStoreId = "mock-vector-store-id";
console.debug("Using mock vector store", { id: vectorStoreId });
return vectorStoreId;
}
// Create a new vector store
const vectorStore = await openai.vectorStores.create({ name });
vectorStoreId = vectorStore.id;
console.debug("Vector store initialized", { id: vectorStoreId });
return vectorStoreId as string;
} catch (error: unknown) {
console.debug("Failed to initialize vector store, using mock", { error: String(error) });
vectorStoreId = "mock-vector-store-id" as string;
return vectorStoreId;
}
}
/**
* Store a log entry in the vector database
* @param entry Log entry to store
*/
export async function vectorStoreLog(entry: LogEntry): Promise<void> {
try {
// If OpenAI client is not available, use mock implementation
if (!openai) {
console.debug("Vector store: Storing log entry (mock)", entry.message, entry.level);
return;
}
// Ensure vector store is initialized
const storeId = await initializeVectorStore();
// Create a text file with the log entry
const timestamp = new Date().toISOString();
const content = `Log Entry (${timestamp})
Level: ${entry.level}
Message: ${entry.message}
Metadata: ${JSON.stringify(entry.metadata || {}, null, 2)}`;
const file = new File(
[content],
`log-${Date.now()}.txt`,
{ type: "text/plain" },
);
// Upload the file to OpenAI
const uploadedFile = await openai.files.create({
file,
purpose: "assistants",
});
// Add the file to the vector store
await openai.vectorStores.files.create(storeId, {
file_id: uploadedFile.id,
});
console.debug("Log entry stored in vector database", {
timestamp: entry.timestamp,
level: entry.level,
message: entry.message.substring(0, 50) + (entry.message.length > 50 ? "..." : ""),
});
} catch (error: unknown) {
console.debug("Failed to store log entry in vector database, using mock", {
error: String(error),
});
console.debug("Vector store: Storing log entry (mock)", entry.message, entry.level);
}
}
/**
* Index a diff entry in the vector database
* @param entry Diff entry to index
*/
export async function indexDiffEntry(entry: DiffEntry): Promise<void> {
try {
// If OpenAI client is not available, use mock implementation
if (!openai) {
console.debug("Vector store: Indexing diff entry (mock)", entry.id, entry.file);
return;
}
// Ensure vector store is initialized
const storeId = await initializeVectorStore();
// Create a text file with the diff entry
const content = `Diff Entry (${entry.id})
File: ${entry.file}
Diff:
${entry.diff}
Metadata: ${JSON.stringify(entry.metadata || {}, null, 2)}`;
const file = new File(
[content],
`diff-${entry.id}.txt`,
{ type: "text/plain" },
);
// Upload the file to OpenAI
const uploadedFile = await openai.files.create({
file,
purpose: "assistants",
});
// Add the file to the vector store
await openai.vectorStores.files.create(storeId, {
file_id: uploadedFile.id,
});
console.debug("Diff entry indexed in vector database", {
id: entry.id,
file: entry.file,
diffPreview: entry.diff.substring(0, 50) + (entry.diff.length > 50 ? "..." : ""),
});
} catch (error: unknown) {
console.debug("Failed to index diff entry in vector database, using mock", {
error: String(error),
});
console.debug("Vector store: Indexing diff entry (mock)", entry.id, entry.file);
}
}
/**
* Search for diff entries similar to the query
* @param query Search query
* @param maxResults Maximum number of results to return
* @returns Array of matching diff entries with similarity scores
*/
export async function searchDiffEntries(
query: string,
maxResults: number = 5,
): Promise<VectorSearchResult[]> {
try {
// If OpenAI client is not available, use mock implementation
if (!openai) {
console.debug("Vector store: Searching for diff entries (mock)", query);
return [];
}
// Ensure vector store is initialized
const storeId = await initializeVectorStore();
// Search the vector store
const searchResponse = await openai.vectorStores.search(storeId, {
query,
max_num_results: maxResults,
});
// Transform the results into the expected format
const results: VectorSearchResult[] = searchResponse.data.map((result: any) => {
// Extract diff entry information from the content
const content = result.content[0]?.text || "";
// Parse the content to extract diff entry information
const idMatch = content.match(/Diff Entry \((.*?)\)/);
const fileMatch = content.match(/File: (.*?)$/m);
const diffMatch = content.match(/Diff:\n([\s\S]*?)Metadata:/);
const metadataMatch = content.match(/Metadata: ([\s\S]*?)$/);
const id = idMatch ? idMatch[1] : "";
const file = fileMatch ? fileMatch[1] : "";
const diff = diffMatch ? diffMatch[1].trim() : "";
const metadata = metadataMatch ? JSON.parse(metadataMatch[1]) : {};
return {
entry: {
id,
file,
diff,
metadata,
} as DiffEntry,
score: result.score || 0,
};
});
console.debug("Searched for diff entries", {
query,
maxResults,
resultsCount: results.length,
});
return results;
} catch (error: unknown) {
console.debug("Failed to search diff entries, using mock", { error: String(error) });
console.debug("Vector store: Searching for diff entries (mock)", query);
return [];
}
}
/**
* Search for log entries similar to the query
* @param query Search query
* @param maxResults Maximum number of results to return
* @returns Array of matching log entries with similarity scores
*/
export async function searchLogEntries(
query: string,
maxResults: number = 5,
): Promise<VectorSearchResult[]> {
try {
// If OpenAI client is not available, use mock implementation
if (!openai) {
console.debug("Vector store: Searching for log entries (mock)", query);
return [];
}
// Ensure vector store is initialized
const storeId = await initializeVectorStore();
// Search the vector store
const searchResponse = await openai.vectorStores.search(storeId, {
query,
max_num_results: maxResults,
});
// Transform the results into the expected format
const results: VectorSearchResult[] = searchResponse.data.map((result: any) => {
// Extract log entry information from the content
const content = result.content[0]?.text || "";
// Parse the content to extract log entry information
const timestampMatch = content.match(/Log Entry \((.*?)\)/);
const levelMatch = content.match(/Level: (.*?)$/m);
const messageMatch = content.match(/Message: (.*?)$/m);
const metadataMatch = content.match(/Metadata: ([\s\S]*?)$/);
const timestamp = timestampMatch ? timestampMatch[1] : new Date().toISOString();
const level = levelMatch ? levelMatch[1] : "info";
const message = messageMatch ? messageMatch[1] : "";
const metadata = metadataMatch ? JSON.parse(metadataMatch[1]) : {};
return {
entry: {
timestamp,
level,
message,
metadata,
} as LogEntry,
score: result.score || 0,
};
});
console.debug("Searched for log entries", {
query,
maxResults,
resultsCount: results.length,
});
return results;
} catch (error: unknown) {
console.debug("Failed to search log entries, using mock", { error: String(error) });
console.debug("Vector store: Searching for log entries (mock)", query);
return [];
}
}
/**
* Clear all entries from the vector store
* Useful for testing or resetting the database
*/
export async function clearVectorStore(): Promise<void> {
try {
// If OpenAI client is not available or no vector store ID, use mock implementation
if (!openai || !vectorStoreId) {
console.debug("Vector store: Clearing all entries (mock)");
vectorStoreId = null;
return;
}
// Get all files in the vector store
const files = await openai.vectorStores.files.list(vectorStoreId);
// Delete each file
for (const file of files.data) {
await openai.vectorStores.files.delete(vectorStoreId, file.id);
}
// Reset the vector store ID
vectorStoreId = null;
console.debug("Cleared all entries from vector store");
} catch (error: unknown) {
console.debug("Failed to clear vector store, using mock", { error: String(error) });
console.debug("Vector store: Clearing all entries (mock)");
vectorStoreId = null;
}
}
export async function searchVectorStore(
query: string,
maxResults: number = 10,
): Promise<VectorSearchResult[]> {
// Split the max results between logs and diffs
const halfResults = Math.ceil(maxResults / 2);
// Search both logs and diffs
const [logResults, diffResults] = await Promise.all([
searchLogEntries(query, halfResults),
searchDiffEntries(query, halfResults),
]);
// Combine and sort results by score
const combinedResults = [...logResults, ...diffResults]
.sort((a, b) => b.score - a.score)
.slice(0, maxResults);
return combinedResults;
}