Repository navigation
add chat function #54
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
556b7b1
22ca86c
4d24e1d
0b9a431
e37bdfd
8277bed
d2c04bb
7741b42
6c1ab01
06f80eb
692ea82
d8060ef
8d918e3
19b0f5e
e723a1d
fc595a9
9e0fca0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| # Optional: set when frontend and backend are on different origins. | ||
| # local dev with Vite proxy can leave this unset. | ||
| # VITE_CHAT_API_BASE_URL=https://your-backend.example.com |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -11,6 +11,7 @@ node_modules | |
| dist | ||
| dist-ssr | ||
| *.local | ||
| backend/.env | ||
|
|
||
| # Editor directories and files | ||
| .vscode/* | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| PORT=8787 | ||
| FRONTEND_ORIGIN=http://localhost:8080,http://localhost:5173 | ||
| CEREBRAS_API_KEY= | ||
| CEREBRAS_MODEL=llama-3.3-70b |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| # Portfolio Chat Backend | ||
|
|
||
| Express + TypeScript backend for portfolio chat. | ||
|
|
||
| ## Features | ||
|
|
||
| - `/api/chat` endpoint for frontend chat UI | ||
| - Retrieval from: | ||
| - `public/llm.txt` | ||
| - `src/data/**/*` | ||
| - `backend/knowledge/cv/**/*` | ||
| - `backend/knowledge/about/**/*` | ||
| - Cerebras chat completion integration | ||
|
|
||
| ## Setup | ||
|
|
||
| ```bash | ||
| cd backend | ||
| bun install | ||
| cp .env.example .env | ||
| # set CEREBRAS_API_KEY in .env | ||
| bun run dev | ||
| ``` | ||
|
|
||
| Server runs at `http://localhost:8787` by default. |
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # About Knowledge | ||
|
|
||
| Put additional markdown files here. | ||
| These files are included in RAG context automatically. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # CV Knowledge | ||
|
|
||
| Put CV text/markdown files here. | ||
| For PDF, convert to text/markdown before ingest for now. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| { | ||
| "name": "portfolio-chat-backend", | ||
| "version": "0.1.0", | ||
| "private": true, | ||
| "type": "module", | ||
| "scripts": { | ||
| "dev": "tsx watch src/server.ts", | ||
| "build": "tsc -p tsconfig.json", | ||
| "start": "node dist/server.js" | ||
| }, | ||
| "dependencies": { | ||
| "cors": "^2.8.5", | ||
| "dotenv": "^16.4.5", | ||
| "express": "^4.21.0", | ||
| "zod": "^3.23.8" | ||
| }, | ||
| "devDependencies": { | ||
| "@types/cors": "^2.8.17", | ||
| "@types/express": "^5.0.0", | ||
| "@types/node": "^22.5.5", | ||
| "tsx": "^4.19.2", | ||
| "typescript": "^5.5.3" | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| import { promises as fs } from 'node:fs'; | ||
| import path from 'node:path'; | ||
| import { SourceDocument } from '../types/chat.js'; | ||
|
|
||
| const SUPPORTED_EXTENSIONS = new Set(['.md', '.markdown', '.txt', '.json', '.ts', '.tsx']); | ||
|
|
||
| type SourceConfig = { | ||
| type: SourceDocument['sourceType']; | ||
| path: string; | ||
| }; | ||
|
|
||
| const readDirRecursive = async (dir: string): Promise<string[]> => { | ||
| const entries = await fs.readdir(dir, { withFileTypes: true }); | ||
| const files: string[] = []; | ||
|
|
||
| for (const entry of entries) { | ||
| const fullPath = path.join(dir, entry.name); | ||
| if (entry.isDirectory()) { | ||
| files.push(...(await readDirRecursive(fullPath))); | ||
| continue; | ||
| } | ||
|
|
||
| if (entry.isFile() && SUPPORTED_EXTENSIONS.has(path.extname(entry.name).toLowerCase())) { | ||
| files.push(fullPath); | ||
| } | ||
| } | ||
|
|
||
| return files; | ||
| }; | ||
|
|
||
| const readSourcePath = async (source: SourceConfig): Promise<SourceDocument[]> => { | ||
| try { | ||
| const stat = await fs.stat(source.path); | ||
|
|
||
| if (stat.isFile()) { | ||
| const content = await fs.readFile(source.path, 'utf8'); | ||
| return [ | ||
| { | ||
| id: source.path, | ||
| title: path.basename(source.path), | ||
| path: source.path, | ||
| content, | ||
| sourceType: source.type, | ||
| }, | ||
| ]; | ||
| } | ||
|
|
||
| if (stat.isDirectory()) { | ||
| const files = await readDirRecursive(source.path); | ||
| const docs: SourceDocument[] = []; | ||
|
|
||
| for (const file of files) { | ||
| const content = await fs.readFile(file, 'utf8'); | ||
| docs.push({ | ||
| id: file, | ||
| title: path.basename(file), | ||
| path: file, | ||
| content, | ||
| sourceType: source.type, | ||
| }); | ||
| } | ||
|
|
||
| return docs; | ||
| } | ||
| } catch (error) { | ||
| console.error(`Failed to read source path "${source.path}":`, error); | ||
| return []; | ||
| } | ||
|
|
||
| return []; | ||
| }; | ||
|
|
||
| export const loadPortfolioDocuments = async (): Promise<SourceDocument[]> => { | ||
| const backendRoot = process.cwd(); | ||
| const projectRoot = path.resolve(backendRoot, '..'); | ||
|
|
||
| const sources: SourceConfig[] = [ | ||
| { type: 'site', path: path.join(projectRoot, 'public', 'llm.txt') }, | ||
| { type: 'site', path: path.join(projectRoot, 'src', 'data') }, | ||
| { type: 'cv', path: path.join(backendRoot, 'knowledge', 'cv') }, | ||
| { type: 'about', path: path.join(backendRoot, 'knowledge', 'about') }, | ||
| ]; | ||
|
|
||
| const all = await Promise.all(sources.map((source) => readSourcePath(source))); | ||
| return all.flat().filter((doc) => doc.content.trim().length > 0); | ||
| }; |
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,43 @@ | ||||||||
| import { Router } from 'express'; | ||||||||
| import { z } from 'zod'; | ||||||||
| import { ChatService } from '../services/chatService.js'; | ||||||||
|
|
||||||||
| const schema = z.object({ | ||||||||
| message: z.string().min(1), | ||||||||
|
||||||||
| message: z.string().min(1), | |
| message: z.string().min(1).max(5000), |
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The chat history array has no maximum length validation. This could allow unbounded history arrays to be sent to the API, potentially causing:
- Large payload sizes that could trigger the 1mb body limit
- Excessive token usage in the Cerebras API call
- Potential performance issues
Consider adding a maximum length constraint to the history array validation, for example:
history: z.array(...).max(50).default([])
This would prevent abuse and ensure predictable API usage.
| ) | |
| ) | |
| .max(50) |
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,53 @@ | ||||||||||||||
| import cors from "cors"; | ||||||||||||||
| import dotenv from "dotenv"; | ||||||||||||||
| import express from "express"; | ||||||||||||||
| import { createChatRouter } from "./routes/chat.js"; | ||||||||||||||
| import { CerebrasClient } from "./services/cerebras.js"; | ||||||||||||||
| import { ChatService } from "./services/chatService.js"; | ||||||||||||||
|
|
||||||||||||||
| dotenv.config(); | ||||||||||||||
| const BASE_MODEL = "llama-3.3-70b"; | ||||||||||||||
| const port = Number(process.env.PORT ?? 8787); | ||||||||||||||
|
||||||||||||||
| const port = Number(process.env.PORT ?? 8787); | |
| const envPort = process.env.PORT; | |
| const port = envPort === undefined ? 8787 : Number(envPort); | |
| if (!Number.isFinite(port)) { | |
| throw new Error(`Invalid PORT environment variable: "${envPort}". Expected a numeric port.`); | |
| } |
Copilot
AI
Feb 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The CORS origin check allows requests with no origin (line 26). While this is necessary for some scenarios (like server-to-server requests or certain development tools), it could potentially allow requests from contexts where the origin header is stripped or not sent.
In production, consider:
- Being more restrictive about requests without an origin header
- Adding additional validation for requests without an origin
- Documenting why requests without origins are allowed
However, this is often necessary for development and non-browser clients, so this may be intentional. Just ensure it aligns with your security requirements.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The root README uses
bun run devto start the backend, while the backend’s own README/scripts are written forpnpm dev(and the repo’s packageManager is pnpm). This inconsistency can confuse setup.Align the command in the root README with the backend package.json scripts (e.g.,
pnpm devfrom withinbackend/).