Skip to content
Draft
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
21 changes: 21 additions & 0 deletions f-assistant/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
OPENAI_API_KEY=
OPENAI_MODEL=gpt-5.2
OPENAI_EMBED_MODEL=text-embedding-3-small

QDRANT_URL=http://localhost:6333
QDRANT_COLLECTION=foblex_flow

GITHUB_REPO=Foblex/f-flow
GITHUB_WEBHOOK_SECRET=

GITHUB_APP_ID=
GITHUB_APP_PRIVATE_KEY_PEM=
GITHUB_APP_INSTALLATION_ID=

ALLOW_PAT=false
GITHUB_PAT=

TELEGRAM_BOT_TOKEN=
TELEGRAM_ADMIN_CHAT_ID=

PREFERRED_LANGUAGE=en
9 changes: 9 additions & 0 deletions f-assistant/KNOWLEDGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Project Knowledge Base

- Repository: `Foblex/f-flow` (Angular-based flow editor and related tooling).
- Primary content sources for RAG:
- `projects/**/*`, `src/**/*`, `server/**/*`, `public/**/*`, `**/*.md`.
- GitHub Issues and Discussions (body + comments).
- Language defaults to English; respect `PREFERRED_LANGUAGE` for generation.
- The bot must never publish to GitHub without explicit admin approval over Telegram.
- Always include citations to repository files or GitHub URLs when providing answers.
42 changes: 42 additions & 0 deletions f-assistant/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# F-Assistant: Human-in-the-loop RAG bot for GitHub Issues/Discussions

This NestJS service builds draft answers for GitHub issues, discussions, or site questions and sends them to a Telegram admin for approval before posting back to GitHub. Responses are generated via a Retrieval-Augmented Generation (RAG) pipeline over repository code, docs, and past issues/discussions.

## Prerequisites
- Node.js 20+
- npm
- Docker (for Qdrant)

## Quickstart
1. Copy `.env.example` to `.env` and fill in required secrets.
2. Start Qdrant locally:
```bash
docker compose -f docker-compose.yml up -d
```
3. Install dependencies and generate Prisma client:
```bash
npm install
npx prisma generate
```
4. Ingest the repository content into Qdrant:
```bash
npm run ingest:repo
```
5. (Optional) Ingest GitHub issues/discussions history:
```bash
npm run ingest:github -- --since=2024-01-01
```
6. Run the NestJS service in development mode:
```bash
npm run start:dev
```
7. Use the Telegram bot (as admin) to draft an answer for an issue:
```
/draft https://github.com/Foblex/f-flow/issues/123
```
The bot will send three draft responses (Answer Pack). Approve one to publish back to GitHub.

## Notes
- The bot never posts to GitHub without explicit admin approval via Telegram.
- Preferred language can be set via `PREFERRED_LANGUAGE` (default `en`).
- The service uses OpenAI `gpt-5.2` for chat and `text-embedding-3-small` for embeddings.
27 changes: 27 additions & 0 deletions f-assistant/SPEC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Answer Pack Specification

The generator must produce exactly three drafts in JSON format:

```json
{
"source": {
"type": "github_issue|github_discussion|website",
"repo": "Foblex/f-flow",
"url": "https://github.com/Foblex/f-flow/issues/258",
"number": 258,
"author": "username",
"title": "..."
},
"drafts": [
{"id": "A", "tone": "short", "description": "Short engineering answer", "text": "markdown...", "citations": []},
{"id": "B", "tone": "technical", "description": "Detailed technical answer", "text": "markdown...", "citations": []},
{"id": "C", "tone": "clarify", "description": "Ask for missing info / repro", "text": "markdown...", "citations": []}
],
"confidence": 0.0,
"missing_context": ["Angular version","@foblex/flow version","minimal reproduction"]
}
```

- The response must be **valid JSON only** without extra text.
- Include citations to code or docs URLs when available.
- If confidence is low, produce a clarifying draft with questions.
12 changes: 12 additions & 0 deletions f-assistant/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
version: '3.9'
services:
qdrant:
image: qdrant/qdrant:v1.8.4
restart: unless-stopped
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
volumes:
qdrant_data: {}
61 changes: 61 additions & 0 deletions f-assistant/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
{
"name": "f-assistant",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"start:dev": "ts-node-dev --respawn --transpile-only src/main.ts",
"build": "tsc -p tsconfig.json",
"ingest:repo": "ts-node src/scripts/ingest-repo.ts",
"ingest:github": "ts-node src/scripts/ingest-github.ts",
"telegram": "ts-node src/integrations/telegram/telegram.entry.ts",
"test": "jest --passWithNoTests"
},
"dependencies": {
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@octokit/graphql": "^7.0.0",
"@octokit/rest": "^21.0.0",
"@qdrant/js-client-rest": "^1.9.0",
"@telegraf/types": "^7.1.0",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"dotenv": "^16.4.5",
"fast-glob": "^3.3.2",
"marked": "^9.1.6",
"nestjs-telegraf": "^2.9.0",
"openai": "^4.67.1",
"prisma": "^5.15.0",
"@prisma/client": "^5.15.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",
"telegraf": "^4.15.3",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/jest": "^29.5.11",
"@types/node": "^20.14.9",
"jest": "^29.7.0",
"ts-jest": "^29.1.2",
"ts-node": "^10.9.2",
"ts-node-dev": "^2.0.0",
"typescript": "^5.4.5"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"extensionsToTreatAsEsm": [".ts"],
"roots": ["<rootDir>/src"],
"moduleFileExtensions": ["ts", "js"],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"globals": {
"ts-jest": {
"useESM": true
}
}
}
}
46 changes: 46 additions & 0 deletions f-assistant/prisma/schema.prisma
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Prisma schema for storing draft jobs and context chunks
// SQLite database

generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "sqlite"
url = "file:./dev.db"
}

model Draft {
id String @id @default(cuid())
sourceType String
sourceUrl String
sourceNumber Int
author String
title String
draftId String
tone String
text String
citations String // JSON string
createdAt DateTime @default(now())
}

model Job {
id String @id @default(cuid())
status String
sourceUrl String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
payload String // JSON string
}

model SourceChunk {
id String @id @default(cuid())
source String
kind String
path String
ref String?
lines String?
content String
embedding String?
createdAt DateTime @default(now())
}
21 changes: 21 additions & 0 deletions f-assistant/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { Module } from '@nestjs/common';
import { ConfigModule } from './config/config.module.js';
import { WebhookModule } from './webhook/webhook.module.js';
import { RagModule } from './rag/rag.module.js';
import { IngestModule } from './ingest/ingest.module.js';
import { IntegrationsGithubModule } from './integrations/github/github.module.js';
import { TelegramModule } from './integrations/telegram/telegram.module.js';
import { StorageModule } from './storage/storage.module.js';

@Module({
imports: [
ConfigModule,
StorageModule,
RagModule,
IngestModule,
IntegrationsGithubModule,
TelegramModule,
WebhookModule,
],
})
export class AppModule {}
13 changes: 13 additions & 0 deletions f-assistant/src/config/config.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Module } from '@nestjs/common';
import { ConfigModule as NestConfigModule } from '@nestjs/config';
import { envSchema } from './env.schema.js';

@Module({
imports: [
NestConfigModule.forRoot({
isGlobal: true,
validate: (env) => envSchema.parse(env),
}),
],
})
export class ConfigModule {}
21 changes: 21 additions & 0 deletions f-assistant/src/config/env.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { z } from 'zod';

export const envSchema = z.object({
OPENAI_API_KEY: z.string().min(1),
OPENAI_MODEL: z.string().default('gpt-5.2'),
OPENAI_EMBED_MODEL: z.string().default('text-embedding-3-small'),
QDRANT_URL: z.string().url(),
QDRANT_COLLECTION: z.string().default('foblex_flow'),
GITHUB_REPO: z.string().min(1),
GITHUB_WEBHOOK_SECRET: z.string().min(1).optional(),
GITHUB_APP_ID: z.string().optional(),
GITHUB_APP_PRIVATE_KEY_PEM: z.string().optional(),
GITHUB_APP_INSTALLATION_ID: z.string().optional(),
ALLOW_PAT: z.string().optional(),
GITHUB_PAT: z.string().optional(),
TELEGRAM_BOT_TOKEN: z.string().min(1),
TELEGRAM_ADMIN_CHAT_ID: z.string().min(1),
PREFERRED_LANGUAGE: z.string().default('en'),
});

export type Env = z.infer<typeof envSchema>;
10 changes: 10 additions & 0 deletions f-assistant/src/ingest/github-ingest.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Injectable, Logger } from '@nestjs/common';

@Injectable()
export class GithubIngestService {
private readonly logger = new Logger(GithubIngestService.name);

async ingestIssues(since?: string) {
this.logger.log(`Stub ingest GitHub issues since ${since ?? 'beginning'}`);
}
}
11 changes: 11 additions & 0 deletions f-assistant/src/ingest/ingest.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { RepoIngestService } from './repo-ingest.service.js';
import { GithubIngestService } from './github-ingest.service.js';
import { RagModule } from '../rag/rag.module.js';

@Module({
imports: [RagModule],
providers: [RepoIngestService, GithubIngestService],
exports: [RepoIngestService, GithubIngestService],
})
export class IngestModule {}
16 changes: 16 additions & 0 deletions f-assistant/src/ingest/repo-ingest.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { Injectable, Logger } from '@nestjs/common';
import { ChunkingService } from '../rag/chunking.service.js';
import { RetrieverService } from '../rag/retriever.service.js';

@Injectable()
export class RepoIngestService {
private readonly logger = new Logger(RepoIngestService.name);

constructor(private readonly chunker: ChunkingService, private readonly retriever: RetrieverService) {}

async ingest(root = '..') {
const chunks = await this.chunker.chunkRepo(root);
await this.retriever.upsertChunks(chunks);
this.logger.log(`Ingested ${chunks.length} chunks from ${root}`);
}
}
17 changes: 17 additions & 0 deletions f-assistant/src/integrations/github/github-auth.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class GithubAuthService {
constructor(private readonly configService: ConfigService) {}

getAuthToken(): string {
const allowPat = this.configService.get<string>('ALLOW_PAT') === 'true';
const pat = this.configService.get<string>('GITHUB_PAT');
if (allowPat && pat) {
return pat;
}
// For MVP fall back to PAT if provided, otherwise empty.
return pat ?? '';
}
}
31 changes: 31 additions & 0 deletions f-assistant/src/integrations/github/github-client.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Injectable } from '@nestjs/common';
import { Octokit } from '@octokit/rest';
import { graphql } from '@octokit/graphql';
import { GithubAuthService } from './github-auth.service.js';
import { ConfigService } from '@nestjs/config';

@Injectable()
export class GithubClientService {
private readonly octokit: Octokit;
private readonly graphqlClient: typeof graphql;
private readonly repo: string;

constructor(auth: GithubAuthService, config: ConfigService) {
const token = auth.getAuthToken();
this.repo = config.get<string>('GITHUB_REPO') ?? 'Foblex/f-flow';
this.octokit = new Octokit({ auth: token });
this.graphqlClient = graphql.defaults({ headers: { authorization: `token ${token}` } });
}

getRest() {
return this.octokit;
}

getGraphql() {
return this.graphqlClient;
}

getRepo() {
return this.repo;
}
}
25 changes: 25 additions & 0 deletions f-assistant/src/integrations/github/github-discussions.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Injectable, Logger } from '@nestjs/common';
import { GithubClientService } from './github-client.service.js';

@Injectable()
export class GithubDiscussionsService {
private readonly logger = new Logger(GithubDiscussionsService.name);

constructor(private readonly github: GithubClientService) {}

async fetchIssue(url: string) {
const [owner, repo, , , numberStr] = url.split('/').slice(-5);
const number = Number(numberStr);
const rest = this.github.getRest();
const { data } = await rest.issues.get({ owner, repo, issue_number: number });
return data;
}

async commentIssue(url: string, body: string) {
const [owner, repo, , , numberStr] = url.split('/').slice(-5);
const number = Number(numberStr);
const rest = this.github.getRest();
await rest.issues.createComment({ owner, repo, issue_number: number, body });
this.logger.log(`Posted comment to ${url}`);
}
}
Loading