diff --git a/.env.local.template b/.env.local.template deleted file mode 100644 index ab2504cc..00000000 --- a/.env.local.template +++ /dev/null @@ -1,6 +0,0 @@ -NEXT_PUBLIC_MODE=UNLIMITED # Optional values LIMITED/UNLIMITED - -FALKORDB_URL=falkordb://localhost:6379 # FALKORDB_URL - -OPENAI_API_KEY=[API_KEY] # Set you openai api key here - diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml new file mode 100644 index 00000000..3893ea77 --- /dev/null +++ b/.github/workflows/playwright.yml @@ -0,0 +1,41 @@ +name: Playwright Tests +on: + push: + branches: [ main, staging ] + pull_request: + branches: [ main, staging ] +jobs: + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + services: + falkordb: + image: falkordb/falkordb:latest + ports: + - 6379:6379 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Set up environment variables and run tests + env: + FALKORDB_URL: ${{ secrets.FALKORDB_URL }} + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + SECRET_TOKEN: ${{ secrets.SECRET_TOKEN }} + NEXT_PUBLIC_MODE: UNLIMITED + BACKEND_URL: ${{ secrets.BACKEND_URL }} + run: | + npm install + npm run build + NEXTAUTH_SECRET=SECRET npm start & npx playwright test --reporter=dot,list + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 diff --git a/.gitignore b/.gitignore index 0d90b872..2e8cf5ed 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,9 @@ next-env.d.ts # vscode -/.vscode/ \ No newline at end of file +/.vscode/ +node_modules/ +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ diff --git a/app/api/chat/[graph]/route.ts b/app/api/chat/[graph]/route.ts new file mode 100644 index 00000000..d7dd5c64 --- /dev/null +++ b/app/api/chat/[graph]/route.ts @@ -0,0 +1,39 @@ +import { NextRequest, NextResponse } from "next/server" +import { getEnvVariables } from "../../utils" + + +export async function POST(request: NextRequest, { params }: { params: Promise<{ graph: string }> }) { + + const repo = (await params).graph + const msg = request.nextUrl.searchParams.get('msg') + + try { + + if (!msg) { + throw new Error("Message parameter is required") + } + + const { url, token } = getEnvVariables() + + const result = await fetch(`${url}/chat`, { + method: 'POST', + body: JSON.stringify({ repo, msg }), + headers: { + "Authorization": token, + "Content-Type": 'application/json' + }, + cache: 'no-store' + }) + + if (!result.ok) { + throw new Error(await result.text()) + } + + const json = await result.json() + + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + console.error(err) + return NextResponse.json((err as Error).message, { status: 400 }) + } +} \ No newline at end of file diff --git a/app/api/repo/[graph]/[node]/route.ts b/app/api/repo/[graph]/[node]/route.ts index 277e3945..b7723ad0 100644 --- a/app/api/repo/[graph]/[node]/route.ts +++ b/app/api/repo/[graph]/[node]/route.ts @@ -1,24 +1,45 @@ -import { FalkorDB, Graph } from "falkordb"; +import { getEnvVariables } from "@/app/api/utils"; import { NextRequest, NextResponse } from "next/server"; -export async function GET(request: NextRequest, { params }: { params: { graph: string, node: string } }) { - - const nodeId = parseInt(params.node); - const graphId = params.graph; +export async function POST(request: NextRequest, { params }: { params: Promise<{ graph: string, node: string }> }) { - const db = await FalkorDB.connect({url: process.env.FALKORDB_URL || 'falkor://localhost:6379',}); - const graph = db.selectGraph(graphId); + const p = await params; - // Get node's neighbors - const q_params = {nodeId: nodeId}; - const query = `MATCH (src)-[e]-(n) - WHERE ID(src) = $nodeId - RETURN collect(distinct { label:labels(n)[0], id:ID(n), name: n.name } ) as nodes, - collect( { src: ID(startNode(e)), id: ID(e), dest: ID(endNode(e)), type: type(e) } ) as edges`; + const repo = p.graph; + const src = Number(p.node); + const dest = Number(request.nextUrl.searchParams.get('targetId')) - let res: any = await graph.query(query, { params: q_params }); - let nodes = res.data[0]['nodes']; - let edges = res.data[0]['edges']; + try { - return NextResponse.json({ id: graphId, nodes: nodes, edges: edges }, { status: 200 }) + if (!dest) { + throw new Error("targetId is required"); + } + + const { url, token } = getEnvVariables() + + const result = await fetch(`${url}/find_paths`, { + method: 'POST', + headers: { + "Authorization": token, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + repo, + src, + dest + }), + cache: 'no-store' + }) + + if (!result.ok) { + throw new Error(await result.text()) + } + + const json = await result.json() + + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + console.error(err) + return NextResponse.json((err as Error).message, { status: 400 }) + } } \ No newline at end of file diff --git a/app/api/repo/[graph]/commit/route.ts b/app/api/repo/[graph]/commit/route.ts new file mode 100644 index 00000000..3e43e899 --- /dev/null +++ b/app/api/repo/[graph]/commit/route.ts @@ -0,0 +1,37 @@ +import { getEnvVariables } from "@/app/api/utils"; +import { NextRequest, NextResponse } from "next/server"; + +export async function GET(request: NextRequest, { params }: { params: Promise<{ graph: string }> }) { + + const repo = (await params).graph + + try { + + const { url, token } = getEnvVariables() + + const result = await fetch(`${url}/list_commits`, { + method: 'POST', + body: JSON.stringify({ repo }), + headers: { + "Authorization": token, + "Content-Type": 'application/json' + }, + cache: 'no-store' + }) + + if (!result.ok) { + throw new Error(await result.text()) + } + + const json = await result.json() + + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + console.error(err) + return NextResponse.json((err as Error).message, { status: 400 }) + } +} + +export async function POST(request: NextRequest, { params }: { params: Promise<{ graph: string }> }) { + +} \ No newline at end of file diff --git a/app/api/repo/[graph]/info/route.ts b/app/api/repo/[graph]/info/route.ts new file mode 100644 index 00000000..2dc250d9 --- /dev/null +++ b/app/api/repo/[graph]/info/route.ts @@ -0,0 +1,33 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getEnvVariables } from "@/app/api/utils"; + +export async function GET(request: NextRequest, { params }: { params: Promise<{ graph: string }> }) { + + const repo = (await params).graph + + try { + + const { url, token } = getEnvVariables(); + + const result = await fetch(`${url}/repo_info`, { + method: 'POST', + body: JSON.stringify({ repo }), + headers: { + "Authorization": token, + "Content-Type": 'application/json' + }, + cache: 'no-store' + }) + + if (!result.ok) { + throw new Error(await result.text()) + } + + const json = await result.json() + + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + console.error(err) + return NextResponse.json((err as Error).message, { status: 400 }) + } +} \ No newline at end of file diff --git a/app/api/repo/[graph]/neighbors/route.ts b/app/api/repo/[graph]/neighbors/route.ts new file mode 100644 index 00000000..044c5cc6 --- /dev/null +++ b/app/api/repo/[graph]/neighbors/route.ts @@ -0,0 +1,34 @@ +import { NextRequest, NextResponse } from "next/server"; +import { getEnvVariables } from "@/app/api/utils"; + +export async function POST(request: NextRequest, { params }: { params: Promise<{ graph: string }> }) { + + const repo = (await params).graph; + const node_ids = (await request.json()).nodeIds.map((id: string) => Number(id)); + + try { + + const { url, token } = getEnvVariables(); + + if (node_ids.length === 0) { + throw new Error("nodeIds is required"); + } + + const result = await fetch(`${url}/get_neighbors`, { + method: 'POST', + body: JSON.stringify({ node_ids, repo }), + headers: { + "Content-Type": 'application/json', + "Authorization": token, + }, + cache: 'no-store' + }) + + const json = await result.json() + + return NextResponse.json({ result: json }, { status: 200 }) + } catch (err) { + console.error(err) + return NextResponse.json((err as Error).message, { status: 400 }) + } +} diff --git a/app/api/repo/[graph]/route.ts b/app/api/repo/[graph]/route.ts index 1fe88b8b..eb684522 100644 --- a/app/api/repo/[graph]/route.ts +++ b/app/api/repo/[graph]/route.ts @@ -1,296 +1,66 @@ -import OpenAI from "openai"; -import { QUESTIONS } from '../questions'; -import { graphSchema } from "../graph_ops"; -import { FalkorDB, Graph } from 'falkordb'; -import { NextRequest, NextResponse } from "next/server"; -import { ChatCompletionCreateParams, ChatCompletionMessageParam, ChatCompletionMessageToolCall, ChatCompletionTool } from 'openai/resources/chat/completions.mjs'; +import { NextRequest, NextResponse } from "next/server" +import { getEnvVariables } from "../../utils" -// convert a structured graph schema into a string representation -// used in a model prompt -async function GraphSchemaToPrompt( - graph: Graph, - graphId: string, - db: FalkorDB -) { - // Retrieve graph schema - let schema: any = await graphSchema(graphId, db); +export async function GET(request: NextRequest, { params }: { params: Promise<{ graph: string }> }) { - // Build a string description of graph schema - let desc: string = "The knowladge graph schema is as follows:\n"; + const graphName = (await params).graph - //------------------------------------------------------------------------- - // Describe labels - //------------------------------------------------------------------------- + try { - // list labels - desc = desc + "The graph contains the following node labels:\n"; - for (const lbl in schema["labels"]) { - desc = desc + `${lbl}\n`; - } - - // specify attributes associated with each label - for (const lbl in schema["labels"]) { - let node_count = schema["labels"][lbl]['node_count']; - let attributes = schema["labels"][lbl]['attributes']; - let attr_count = Object.keys(attributes).length; - - if (attr_count == 0) { - desc = desc + `the ${lbl} label has ${node_count} nodes and has no attributes\n`; - } else { - desc = desc + `the ${lbl} label has ${node_count} nodes and is associated with the following attribute(s):\n`; - for (const attr in attributes) { - let type = attributes[attr]['type']; - desc = desc + `'${attr}' which is of type ${type}\n`; - } - } - } - - desc = desc + "The graph contains the following relationship types:\n" - - //------------------------------------------------------------------------- - // Describe relationships - //------------------------------------------------------------------------- - - // list relations - for (const relation in schema["relations"]) { - desc = desc + `${relation}\n`; - } - - // specify attributes associated with each relationship - for (const relation in schema["relations"]) { - let connect = schema["relations"][relation]['connect']; - let edge_count = schema["relations"][relation]['edge_count']; - let attributes = schema["relations"][relation]['attributes']; - let attr_count = Object.keys(attributes).length; - - if (attr_count == 0) { - desc = desc + `the ${relation} relationship has ${edge_count} edges and has no attributes\n`; - } else { - desc = desc + `the ${relation} relationship has ${edge_count} edges and is associated with the following attribute(s):\n`; - for (const attr in attributes) { - let type = attributes[attr]['type']; - desc = desc + `'${attr}' which is of type ${type}\n`; - } - } + const { url, token } = getEnvVariables() - if (connect.length > 0) { - desc = desc + `the ${relation} relationship connects the following labels:\n` - for (let i = 0; i < connect.length; i += 2) { - let src = connect[i]; - let dest = connect[i + 1]; - desc = desc + `${src} is connected via ${relation} to ${dest}\n`; + const result = await fetch(`${url}/graph_entities?repo=${graphName}`, { + method: 'GET', + headers: { + "Authorization": token, } - } - } - - desc = desc + `This is the end of the knowladge graph schema description.\n` - - //------------------------------------------------------------------------- - // include graph indices - //------------------------------------------------------------------------- - - // vector indices - let query = `CALL db.indexes() YIELD label, properties, types, entitytype`; - let res = await graph.query(query); - - // process indexes - let indexes: any = res.data; - if (indexes.length > 0) { - let index_prompt = "The knowladge graph contains the following indexes:\n" - for (let i = 0; i < indexes.length; i++) { - const index = indexes[i]; - const label: string = index['label']; - const entityType: string = index['entitytype']; - const props = index['properties']; - const types = index['types']; + }) - for (const prop of props) { - const propTypes: string[] = types[prop]; - for (let j = 0; j < propTypes.length; j++) { - const idxType: string = propTypes[j]; - index_prompt += `${entityType} of type ${label} have a ${idxType} index indexing its ${prop} attribute\n`; - } - } + if (!result.ok) { + throw new Error(await result.text()) } - index_prompt += `This is the end of our indexes list - To use a Vector index use the following procedure: - CALL db.idx.vector.queryNodes(