Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateExtension
CREATE EXTENSION IF NOT EXISTS "vector";

-- CreateTable
CREATE TABLE "NoteEmbedding" (
"id" TEXT NOT NULL,
"content" TEXT NOT NULL,
"embedding" vector(768),
"noteId" TEXT NOT NULL,

CONSTRAINT "NoteEmbedding_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "NoteEmbedding" ADD CONSTRAINT "NoteEmbedding_noteId_fkey" FOREIGN KEY ("noteId") REFERENCES "Note"("id") ON DELETE CASCADE ON UPDATE CASCADE;
37 changes: 24 additions & 13 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
generator client {
provider = "prisma-client-js"
provider = "prisma-client-js"
previewFeatures = ["postgresqlExtensions"]
}

datasource db {
provider = "postgresql"
provider = "postgresql"
extensions = [vector]
}

model User {
Expand Down Expand Up @@ -43,22 +45,31 @@ model Project {
}

model Note {
id String @id @default(uuid())
title String
content String?
tags String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
projectId String?
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
id String @id @default(uuid())
title String
content String?
tags String[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
projectId String?
project Project? @relation(fields: [projectId], references: [id], onDelete: SetNull)
searchVector Unsupported("tsvector")?
versions NoteVersion[]
versions NoteVersion[]
embeddings NoteEmbedding[]

@@index([searchVector], type: Gin)
}

model NoteEmbedding {
id String @id @default(uuid())
content String
embedding Unsupported("vector(768)")?
noteId String
note Note @relation(fields: [noteId], references: [id], onDelete: Cascade)
}

model NoteVersion {
id String @id @default(uuid())
title String
Expand Down
27 changes: 27 additions & 0 deletions apps/backend/src/ai/ai.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,28 @@ export class AiController {
return this.aiService.streamChat(req.user.id, body, res);
}

@Post('analyze-code')
async analyzeCode(@Body() body: { code: string; model?: string }) {
return this.aiService.analyzeCode(body.code, body.model);
}

@Post('debug')
async debugCode(
@Body() body: { code: string; errorMessage: string; model?: string },
) {
return this.aiService.debugCode(body.code, body.errorMessage, body.model);
}

@Post('generate-docs')
async generateDocs(@Body() body: { code: string; model?: string }) {
return this.aiService.generateDocs(body.code, body.model);
}

@Post('embeddings')
async generateEmbeddings(@Body() body: { text: string; model?: string }) {
return this.aiService.generateEmbeddings(body.text, body.model);
}

@Get('conversations')
getConversations(@Req() req: RequestWithUser) {
return this.aiService.getConversations(req.user.id);
Expand All @@ -39,4 +61,9 @@ export class AiController {
getMessages(@Req() req: RequestWithUser, @Param('id') id: string) {
return this.aiService.getMessages(id, req.user.id);
}

@Get('models')
getModels() {
return this.aiService.getModels();
}
}
164 changes: 161 additions & 3 deletions apps/backend/src/ai/ai.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,40 @@ export class AiService {
orderBy: { createdAt: 'asc' },
});

const messages = history.map((m) => ({ role: m.role, content: m.content }));
// Context trimming: Keep only the last 10 messages to prevent token overflow
const recentHistory = history.slice(-10);

const messages = recentHistory.map((m) => ({
role: m.role,
content: m.content,
}));

// RAG INJECTION
let contextTexts = '';
try {
const { embedding } = await this.generateEmbeddings(message);
const embeddingString = `[${embedding.join(',')}]`;

const similarChunks = await this.prisma.$queryRaw<{ content: string }[]>`
SELECT ne.content
FROM "NoteEmbedding" ne
JOIN "Note" n ON n.id = ne."noteId"
WHERE n."userId" = ${userId}
ORDER BY ne.embedding <=> ${embeddingString}::vector
LIMIT 3
`;
contextTexts = similarChunks.map((c) => c.content).join('\n---\n');
} catch (err) {
console.error('Failed to fetch RAG context:', err);
}

messages.unshift({
role: 'system',
content:
'You are DevFlow AI, a highly skilled and helpful senior software engineer. Provide clear, concise answers with accurate code examples when needed. Format your code blocks with the correct language identifiers.',
content: `You are a helpful AI coding assistant named DevFlow AI.
Here is some context from the user's notes that might be relevant to their query:
${contextTexts}

Use this context to inform your answer if it is relevant.`,
});

res.setHeader('Content-Type', 'text/event-stream');
Expand Down Expand Up @@ -133,4 +162,133 @@ export class AiService {
orderBy: { createdAt: 'asc' },
});
}
async analyzeCode(code: string, model = 'llama3') {
const prompt = `
You are an expert code reviewer. Analyze the following code.
Return ONLY valid JSON with this exact structure:
{
"summary": "A brief summary of what the code does",
"issues": [
{ "type": "bug", "description": "...", "line": 10 }
],
"suggestions": ["..."],
"complexity": "O(n)"
}
The 'type' for issues can be 'bug', 'security', 'performance', or 'style'.
If there are no issues, return an empty array for issues.
Do not wrap the JSON in Markdown backticks. Just output raw JSON.

Code to analyze:
${code}
`;

try {
const res = await fetch('http://127.0.0.1:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model,
prompt,
format: 'json',
stream: false,
}),
});

if (!res.ok) throw new Error('Ollama request failed');
const data = (await res.json()) as { response: string };
return JSON.parse(data.response) as Record<string, unknown>;
} catch (error) {
console.error('Code analysis failed:', error);
throw new Error('Failed to analyze code');
}
}

async debugCode(code: string, errorMessage: string, model = 'llama3') {
const prompt = `
You are an expert software engineer and debugger.
Analyze the following code and the associated error message.
Return ONLY valid JSON with this exact structure:
{
"rootCause": "Explanation of why the error occurs",
"solution": "High level description of how to fix it",
"fixedCode": "The corrected code snippet"
}
Do not wrap the JSON in Markdown backticks. Just output raw JSON.

Code:
${code}

Error Message:
${errorMessage}
`;

try {
const res = await fetch('http://127.0.0.1:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, prompt, format: 'json', stream: false }),
});
if (!res.ok) throw new Error('Ollama request failed');
const data = (await res.json()) as { response: string };
return JSON.parse(data.response) as Record<string, unknown>;
} catch (error) {
console.error('Code debugging failed:', error);
throw new Error('Failed to debug code');
}
}

async generateDocs(code: string, model = 'llama3') {
const prompt = `
You are a technical writer and senior developer.
Generate comprehensive markdown documentation for the following code.
Include:
- A high-level overview
- Parameters, arguments, and return types (if applicable)
- Examples of how to use it
Output ONLY the markdown documentation. Do not wrap in JSON.

Code:
${code}
`;

try {
const res = await fetch('http://127.0.0.1:11434/api/generate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, prompt, stream: false }),
});
if (!res.ok) throw new Error('Ollama request failed');
const data = (await res.json()) as { response: string };
return { documentation: data.response };
} catch (error) {
console.error('Docs generation failed:', error);
throw new Error('Failed to generate documentation');
}
}

async generateEmbeddings(text: string, model = 'nomic-embed-text') {
try {
const res = await fetch('http://127.0.0.1:11434/api/embeddings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ model, prompt: text }),
});
if (!res.ok) throw new Error('Ollama embeddings request failed');
const data = (await res.json()) as { embedding: number[] };
return { embedding: data.embedding };
} catch (error) {
console.error('Embeddings generation failed:', error);
throw new Error('Failed to generate embeddings');
}
}

async getModels() {
try {
const res = await fetch('http://127.0.0.1:11434/api/tags');
const data = (await res.json()) as { models: Array<{ name: string }> };
return data.models || [];
} catch {
return [{ name: 'llama3:latest' }];
}
}
}
1 change: 1 addition & 0 deletions apps/backend/src/notes/dto/create-note.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class CreateNoteDto {
content?: string;

@IsArray()
@IsString({ each: true })
@IsOptional()
tags?: string[];

Expand Down
1 change: 1 addition & 0 deletions apps/backend/src/notes/dto/update-note.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export class UpdateNoteDto {
content?: string;

@IsArray()
@IsString({ each: true })
@IsOptional()
tags?: string[];

Expand Down
3 changes: 3 additions & 0 deletions apps/backend/src/notes/notes.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ import { Module } from '@nestjs/common';
import { NotesService } from './notes.service';
import { NotesController } from './notes.controller';

import { AiModule } from '../ai/ai.module';

@Module({
imports: [AiModule],
providers: [NotesService],
controllers: [NotesController],
})
Expand Down
Loading
Loading