gsearch / server /routes.ts
nanoppa's picture
Upload 100 files
10c8635 verified
raw
history blame
8.08 kB
import type { Express } from "express";
import { createServer, type Server } from "http";
import {
GoogleGenerativeAI,
type ChatSession,
type GenerateContentResult,
} from "@google/generative-ai";
import { marked } from "marked";
import { setupEnvironment } from "./env";
const env = setupEnvironment();
const genAI = new GoogleGenerativeAI(env.GOOGLE_API_KEY);
const model = genAI.getGenerativeModel({
model: "gemini-2.0-flash-exp",
generationConfig: {
temperature: 0.9,
topP: 1,
topK: 1,
maxOutputTokens: 2048,
},
});
// Store chat sessions in memory
const chatSessions = new Map<string, ChatSession>();
// Format raw text into proper markdown
async function formatResponseToMarkdown(
text: string | Promise<string>
): Promise<string> {
// Ensure we have a string to work with
const resolvedText = await Promise.resolve(text);
// First, ensure consistent newlines
let processedText = resolvedText.replace(/\r\n/g, "\n");
// Process main sections (lines that start with word(s) followed by colon)
processedText = processedText.replace(
/^([A-Za-z][A-Za-z\s]+):(\s*)/gm,
"## $1$2"
);
// Process sub-sections (any remaining word(s) followed by colon within text)
processedText = processedText.replace(
/(?<=\n|^)([A-Za-z][A-Za-z\s]+):(?!\d)/gm,
"### $1"
);
// Process bullet points
processedText = processedText.replace(/^[•●○]\s*/gm, "* ");
// Split into paragraphs
const paragraphs = processedText.split("\n\n").filter(Boolean);
// Process each paragraph
const formatted = paragraphs
.map((p) => {
// If it's a header or list item, preserve it
if (p.startsWith("#") || p.startsWith("*") || p.startsWith("-")) {
return p;
}
// Add proper paragraph formatting
return `${p}\n`;
})
.join("\n\n");
// Configure marked options for better header rendering
marked.setOptions({
gfm: true,
breaks: true,
});
// Convert markdown to HTML using marked
return marked.parse(formatted);
}
interface WebSource {
uri: string;
title: string;
}
interface GroundingChunk {
web?: WebSource;
}
interface TextSegment {
startIndex: number;
endIndex: number;
text: string;
}
interface GroundingSupport {
segment: TextSegment;
groundingChunkIndices: number[];
confidenceScores: number[];
}
interface GroundingMetadata {
groundingChunks: GroundingChunk[];
groundingSupports: GroundingSupport[];
searchEntryPoint?: any;
webSearchQueries?: string[];
}
export function registerRoutes(app: Express): Server {
// Search endpoint - creates a new chat session
app.get("/api/search", async (req, res) => {
try {
const query = req.query.q as string;
if (!query) {
return res.status(400).json({
message: "Query parameter 'q' is required",
});
}
// Create a new chat session with search capability
const chat = model.startChat({
tools: [
{
// @ts-ignore - google_search is a valid tool but not typed in the SDK yet
google_search: {},
},
],
});
// Generate content with search tool
const result = await chat.sendMessage(query);
const response = await result.response;
console.log(
"Raw Google API Response:",
JSON.stringify(
{
text: response.text(),
candidates: response.candidates,
groundingMetadata: response.candidates?.[0]?.groundingMetadata,
},
null,
2
)
);
const text = response.text();
// Format the response text to proper markdown/HTML
const formattedText = await formatResponseToMarkdown(text);
// Extract sources from grounding metadata
const sourceMap = new Map<
string,
{ title: string; url: string; snippet: string }
>();
// Get grounding metadata from response
const metadata = response.candidates?.[0]?.groundingMetadata as any;
if (metadata) {
const chunks = metadata.groundingChunks || [];
const supports = metadata.groundingSupports || [];
chunks.forEach((chunk: any, index: number) => {
if (chunk.web?.uri && chunk.web?.title) {
const url = chunk.web.uri;
if (!sourceMap.has(url)) {
// Find snippets that reference this chunk
const snippets = supports
.filter((support: any) =>
support.groundingChunkIndices.includes(index)
)
.map((support: any) => support.segment.text)
.join(" ");
sourceMap.set(url, {
title: chunk.web.title,
url: url,
snippet: snippets || "",
});
}
}
});
}
const sources = Array.from(sourceMap.values());
// Generate a session ID and store the chat
const sessionId = Math.random().toString(36).substring(7);
chatSessions.set(sessionId, chat);
res.json({
sessionId,
summary: formattedText,
sources,
});
} catch (error: any) {
console.error("Search error:", error);
res.status(500).json({
message:
error.message || "An error occurred while processing your search",
});
}
});
// Follow-up endpoint - continues existing chat session
app.post("/api/follow-up", async (req, res) => {
try {
const { sessionId, query } = req.body;
if (!sessionId || !query) {
return res.status(400).json({
message: "Both sessionId and query are required",
});
}
const chat = chatSessions.get(sessionId);
if (!chat) {
return res.status(404).json({
message: "Chat session not found",
});
}
// Send follow-up message in existing chat
const result = await chat.sendMessage(query);
const response = await result.response;
console.log(
"Raw Google API Follow-up Response:",
JSON.stringify(
{
text: response.text(),
candidates: response.candidates,
groundingMetadata: response.candidates?.[0]?.groundingMetadata,
},
null,
2
)
);
const text = response.text();
// Format the response text to proper markdown/HTML
const formattedText = await formatResponseToMarkdown(text);
// Extract sources from grounding metadata
const sourceMap = new Map<
string,
{ title: string; url: string; snippet: string }
>();
// Get grounding metadata from response
const metadata = response.candidates?.[0]?.groundingMetadata as any;
if (metadata) {
const chunks = metadata.groundingChunks || [];
const supports = metadata.groundingSupports || [];
chunks.forEach((chunk: any, index: number) => {
if (chunk.web?.uri && chunk.web?.title) {
const url = chunk.web.uri;
if (!sourceMap.has(url)) {
// Find snippets that reference this chunk
const snippets = supports
.filter((support: any) =>
support.groundingChunkIndices.includes(index)
)
.map((support: any) => support.segment.text)
.join(" ");
sourceMap.set(url, {
title: chunk.web.title,
url: url,
snippet: snippets || "",
});
}
}
});
}
const sources = Array.from(sourceMap.values());
res.json({
summary: formattedText,
sources,
});
} catch (error: any) {
console.error("Follow-up error:", error);
res.status(500).json({
message:
error.message ||
"An error occurred while processing your follow-up question",
});
}
});
const httpServer = createServer(app);
return httpServer;
}