diff --git a/package.json b/package.json index 325ab0c..51a393f 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "Multi-person multi-agent AI interactions", "main": "index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "echo \"Error: no test specified\" && exit 1", + "serve": "vue-cli-service serve", + "build": "vue-cli-service build" }, "keywords": [ "agentic", @@ -27,18 +29,28 @@ "axios": "^1.7.9", "cheerio": "^1.0.0", "cors": "^2.8.5", + "d3": "^7.8.5", "dotenv": "^16.4.7", "express": "^4.21.2", "groq-sdk": "^0.9.1", "helmet": "^8.0.0", "jsonwebtoken": "^9.0.2", "multer": "^1.4.5-lts.1", + "neo4j-driver": "^5.12.0", "openai": "^4.76.0", "pdfjs-dist": "^4.10.38", "playht": "^0.16.0", "short-unique-id": "^5.2.0", "socket.io": "^4.8.1", "uuid": "^11.0.3", + "vue": "^3.3.4", "ws": "^8.18.0" + }, + "devDependencies": { + "@vue/cli-service": "~5.0.8", + "@vue/compiler-sfc": "^3.3.4", + "tailwindcss": "^3.3.5", + "postcss": "^8.4.31", + "autoprefixer": "^10.4.16" } -} +} \ No newline at end of file diff --git a/public/App.js b/public/App.js index aabb22a..8c0af83 100644 --- a/public/App.js +++ b/public/App.js @@ -3,69 +3,68 @@ import { useConfigs } from "./composables/useConfigs.js"; import { useModels } from "./composables/useModels.js"; // import { useRealTime } from "./composables/useRealTime.js"; import { useTextToSpeech } from "./composables/useTextToSpeech.js"; -import router from "../router/index.js"; +import router from "./router/index.js"; export default { template: `
-
- + +
+ +
`, setup() { @@ -81,9 +80,10 @@ export default { // Using router path directly const isLandingPage = Vue.computed(() => router.currentRoute.value.path === '/'); - // Define your menu items once here + // Define your menu items with the new knowledge graph page const menuItems = [ { label: "Binder", to: "/binder" }, + { label: "Knowledge Graph", to: "/knowledge-graph" } ]; function triggerFileInput() { @@ -132,6 +132,16 @@ export default { menuOpen.value = !menuOpen.value; } - return { isLandingPage, download, triggerFileInput, handleFileUpload, fileInput, menuOpen, toggleMenu, projects, menuItems }; + return { + isLandingPage, + download, + triggerFileInput, + handleFileUpload, + fileInput, + menuOpen, + toggleMenu, + projects, + menuItems + }; }, }; \ No newline at end of file diff --git a/public/components/KnowledgeGraph.js b/public/components/KnowledgeGraph.js new file mode 100644 index 0000000..646ffc9 --- /dev/null +++ b/public/components/KnowledgeGraph.js @@ -0,0 +1,396 @@ +// src/components/KnowledgeGraphComponent.js + +import * as d3 from 'd3'; +import { knowledgeGraphService } from '../services/knowledgeGraphService.js'; + +export default { + name: 'KnowledgeGraphComponent', + template: ` +
+
+

PDF Knowledge Graph Generator

+ + +
+ +
+

{{ status }}

+
+
+
+
+ +
+
+ + + + +
+
+
+ +
+

{{ selectedNode.label }}

+

Type: {{ selectedNode.type }}

+
+

Properties:

+
    +
  • + {{ key }}: {{ value }} +
  • +
+
+ +
+
+ `, + setup() { + // State variables + const fileInput = Vue.ref(null); + const selectedFile = Vue.ref(null); + const isProcessing = Vue.ref(false); + const status = Vue.ref(''); + const processingProgress = Vue.ref(0); + const graphContainer = Vue.ref(null); + const graphData = Vue.ref({ nodes: [], links: [] }); + const selectedNode = Vue.ref(null); + const selectedLayout = Vue.ref('force'); + const simulation = Vue.ref(null); + const svg = Vue.ref(null); + const zoom = Vue.ref(null); + + // Function to handle file upload + const handleFileUpload = (event) => { + const file = event.target.files[0]; + if (file && file.type === 'application/pdf') { + selectedFile.value = file; + status.value = `File selected: ${file.name}`; + } else { + status.value = 'Please select a PDF file.'; + } + }; + + // Progress callback for the service + const progressCallback = (type, value) => { + if (type === 'status' || type === 'extracting' || type === 'storing') { + status.value = value; + } else if (type === 'progress') { + processingProgress.value = value; + } else if (type === 'error') { + status.value = `Error: ${value}`; + } + }; + + // Main function to process PDF + const processPdf = async () => { + if (!selectedFile.value) { + status.value = 'Please select a PDF file first.'; + return; + } + + try { + isProcessing.value = true; + + // Process the PDF using the service + const result = await knowledgeGraphService.processPdf( + selectedFile.value, + progressCallback + ); + + // Update graph data + graphData.value = result; + + // Render the graph + renderGraph(); + } catch (error) { + console.error('Error processing PDF:', error); + status.value = `Error: ${error.message}`; + } finally { + isProcessing.value = false; + } + }; + + // Function to render the graph using D3.js + const renderGraph = () => { + if (!graphContainer.value) return; + + const width = graphContainer.value.clientWidth; + const height = graphContainer.value.clientHeight || 600; + + // Clear previous graph + d3.select(graphContainer.value).selectAll('*').remove(); + + // Create SVG + svg.value = d3.select(graphContainer.value) + .append('svg') + .attr('width', width) + .attr('height', height) + .attr('viewBox', [0, 0, width, height]) + .attr('style', 'max-width: 100%; height: auto; font: 12px sans-serif;'); + + // Create zoom behavior + zoom.value = d3.zoom() + .scaleExtent([0.1, 8]) + .on('zoom', (event) => { + container.attr('transform', event.transform); + }); + + svg.value.call(zoom.value); + + // Container for the graph + const container = svg.value.append('g'); + + // Reset zoom + svg.value.call(zoom.value.transform, d3.zoomIdentity); + + // Create links + const link = container.append('g') + .selectAll('line') + .data(graphData.value.links) + .join('line') + .attr('stroke', '#999') + .attr('stroke-opacity', 0.6) + .attr('stroke-width', 1.5); + + // Create link labels + const linkLabels = container.append('g') + .selectAll('text') + .data(graphData.value.links) + .join('text') + .text(d => d.type) + .attr('font-size', '8px') + .attr('fill', '#ccc') + .attr('text-anchor', 'middle'); + + // Create nodes + const node = container.append('g') + .selectAll('circle') + .data(graphData.value.nodes) + .join('circle') + .attr('r', 8) + .attr('fill', d => getNodeColor(d.type)) + .call(drag(simulation.value)) + .on('click', (event, d) => { + event.stopPropagation(); + selectedNode.value = d; + }); + + // Node labels + const labels = container.append('g') + .selectAll('text') + .data(graphData.value.nodes) + .join('text') + .text(d => d.label) + .attr('font-size', '10px') + .attr('fill', '#fff') + .attr('dx', 12) + .attr('dy', 4); + + // Create simulation + simulation.value = d3.forceSimulation(graphData.value.nodes) + .force('link', d3.forceLink(graphData.value.links) + .id(d => d.id) + .distance(100)) + .force('charge', d3.forceManyBody().strength(-200)) + .force('center', d3.forceCenter(width / 2, height / 2)) + .on('tick', () => { + link + .attr('x1', d => d.source.x) + .attr('y1', d => d.source.y) + .attr('x2', d => d.target.x) + .attr('y2', d => d.target.y); + + linkLabels + .attr('x', d => (d.source.x + d.target.x) / 2) + .attr('y', d => (d.source.y + d.target.y) / 2); + + node + .attr('cx', d => d.x) + .attr('cy', d => d.y); + + labels + .attr('x', d => d.x) + .attr('y', d => d.y); + }); + + // Click on background to deselect node + svg.value.on('click', () => { + selectedNode.value = null; + }); + }; + + // Function to get color based on node type + const getNodeColor = (type) => { + const colors = { + Person: '#FF6B6B', + Organization: '#4ECDC4', + Location: '#FFE66D', + Concept: '#6A0572', + Event: '#F7B801', + // Add more types as needed + }; + + return colors[type] || '#999'; + }; + + // Drag functions for d3 + const drag = (simulation) => { + function dragstarted(event) { + if (!event.active) simulation.alphaTarget(0.3).restart(); + event.subject.fx = event.subject.x; + event.subject.fy = event.subject.y; + } + + function dragged(event) { + event.subject.fx = event.x; + event.subject.fy = event.y; + } + + function dragended(event) { + if (!event.active) simulation.alphaTarget(0); + event.subject.fx = null; + event.subject.fy = null; + } + + return d3.drag() + .on('start', dragstarted) + .on('drag', dragged) + .on('end', dragended); + }; + + // Functions for zoom control + const zoomIn = () => { + svg.value.transition().call( + zoom.value.scaleBy, 1.5 + ); + }; + + const zoomOut = () => { + svg.value.transition().call( + zoom.value.scaleBy, 0.75 + ); + }; + + const resetZoom = () => { + svg.value.transition().call( + zoom.value.transform, d3.zoomIdentity + ); + }; + + // Function to update layout + const updateLayout = () => { + if (!simulation.value) return; + + simulation.value.stop(); + + if (selectedLayout.value === 'force') { + simulation.value + .force('link', d3.forceLink(graphData.value.links).id(d => d.id).distance(100)) + .force('charge', d3.forceManyBody().strength(-200)) + .force('center', d3.forceCenter( + graphContainer.value.clientWidth / 2, + graphContainer.value.clientHeight / 2 + )); + } else if (selectedLayout.value === 'circular') { + simulation.value + .force('link', d3.forceLink(graphData.value.links).id(d => d.id).distance(50)) + .force('charge', d3.forceManyBody().strength(-50)) + .force('center', d3.forceCenter( + graphContainer.value.clientWidth / 2, + graphContainer.value.clientHeight / 2 + )) + .force('radial', d3.forceRadial( + graphContainer.value.clientWidth / 4, + graphContainer.value.clientWidth / 2, + graphContainer.value.clientHeight / 2 + )); + } else if (selectedLayout.value === 'hierarchical') { + // Apply hierarchical layout + const stratify = d3.stratify() + .id(d => d.id) + .parentId(d => { + const parentLink = graphData.value.links.find(link => link.target === d.id); + return parentLink ? parentLink.source : null; + }); + + try { + const root = stratify(graphData.value.nodes); + + const treeLayout = d3.tree() + .size([ + graphContainer.value.clientWidth - 100, + graphContainer.value.clientHeight - 100 + ]); + + const nodes = treeLayout(root); + + nodes.each(node => { + const originalNode = graphData.value.nodes.find(n => n.id === node.id); + if (originalNode) { + originalNode.x = node.x + 50; + originalNode.y = node.y + 50; + } + }); + } catch (error) { + console.error('Error applying hierarchical layout:', error); + // Fallback to force layout + simulation.value + .force('link', d3.forceLink(graphData.value.links).id(d => d.id).distance(100)) + .force('charge', d3.forceManyBody().strength(-200)) + .force('center', d3.forceCenter( + graphContainer.value.clientWidth / 2, + graphContainer.value.clientHeight / 2 + )); + } + } + + simulation.value.alpha(1).restart(); + }; + + // Function to close the info panel + const closeInfoPanel = () => { + selectedNode.value = null; + }; + + // Lifecycle hooks + Vue.onMounted(() => { + // Initialize container dimensions + if (graphContainer.value) { + graphContainer.value.style.height = '600px'; + } + }); + + Vue.onBeforeUnmount(() => { + // Clean up D3 simulation + if (simulation.value) { + simulation.value.stop(); + } + }); + + return { + fileInput, + selectedFile, + isProcessing, + status, + processingProgress, + graphContainer, + graphData, + selectedNode, + selectedLayout, + handleFileUpload, + processPdf, + zoomIn, + zoomOut, + resetZoom, + updateLayout, + closeInfoPanel + }; + } +}; \ No newline at end of file diff --git a/public/components/KnowledgeGraphView.js b/public/components/KnowledgeGraphView.js new file mode 100644 index 0000000..6be7bc4 --- /dev/null +++ b/public/components/KnowledgeGraphView.js @@ -0,0 +1,21 @@ +// components/KnowledgeGraphView.js +import KnowledgeGraphComponent from './KnowledgeGraphComponent.js'; + +export default { + name: 'KnowledgeGraphView', + template: ` +
+
+

Knowledge Graph Generator

+

+ Upload PDF documents to automatically create an interactive knowledge graph that visualizes the relationships between concepts, entities, and ideas. +

+ + +
+
+ `, + components: { + KnowledgeGraphComponent + } +}; \ No newline at end of file diff --git a/public/main.js b/public/main.js index 0acfd8a..e638160 100644 --- a/public/main.js +++ b/public/main.js @@ -1,9 +1,13 @@ // Import App and router (which are now simple objects or functions) import App from './App.js'; import router from './router/index.js'; +import { createApp } from 'vue'; console.log(Vue.version); +document.body.classList.add('bg-gray-800', 'text-white', 'm-0', 'font-sans'); + + // // Create the Vue app and use the router const app = Vue.createApp(App); app.use(router); diff --git a/public/router/index.js b/public/router/index.js index 98b5f05..b4401ee 100644 --- a/public/router/index.js +++ b/public/router/index.js @@ -1,5 +1,6 @@ import Landing from "../components/Landing.js"; import Binder from "../components/Binder.js"; +import KnowledgeGraphView from "../components/KnowledgeGraphView.js"; const routes = [ { @@ -13,8 +14,14 @@ const routes = [ component: Binder, name: "binder", // requiresAuth:true, //Setup your own auth if you want SSO/Logins + }, + + { + path: "/knowledge-graph", + component: KnowledgeGraphView, + name: "knowledge-graph", + // requiresAuth:true, // Uncomment if you want to require authentication } - ]; const router = VueRouter.createRouter({ @@ -36,4 +43,4 @@ router.beforeEach((to, from, next) => { } }); -export default router; +export default router; \ No newline at end of file diff --git a/public/services/knowledgeGraphService.js b/public/services/knowledgeGraphService.js new file mode 100644 index 0000000..e369e3f --- /dev/null +++ b/public/services/knowledgeGraphService.js @@ -0,0 +1,263 @@ +// src/services/knowledgeGraphService.js + +import axios from 'axios'; +import neo4j from 'neo4j-driver'; +import * as pdfjsLib from 'pdfjs-dist'; + +/** + * Service for creating and managing knowledge graphs from PDF documents + */ +export const knowledgeGraphService = { + /** + * Initialize the Neo4j driver + * @returns {object} Neo4j driver instance + */ + initDriver() { + return neo4j.driver( + process.env.VUE_APP_NEO4J_URI || 'neo4j://localhost:7687', + neo4j.auth.basic( + process.env.VUE_APP_NEO4J_USER || 'neo4j', + process.env.VUE_APP_NEO4J_PASSWORD || 'password' + ) + ); + }, + + /** + * Extract text content from a PDF file + * @param {File} file - PDF file to process + * @param {Function} progressCallback - Callback for progress updates + * @returns {Promise} Extracted text + */ + async extractTextFromPdf(file, progressCallback) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = async (event) => { + try { + const typedArray = new Uint8Array(event.target.result); + + // Set worker path to pdf.worker.js + pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@4.10.38/build/pdf.worker.min.js'; + + const pdf = await pdfjsLib.getDocument({ data: typedArray }).promise; + progressCallback?.('extracting', `Extracting text from ${pdf.numPages} pages...`); + + let fullText = ''; + for (let i = 1; i <= pdf.numPages; i++) { + const progress = (i / pdf.numPages) * 30; // First 30% of progress + progressCallback?.('progress', progress); + + const page = await pdf.getPage(i); + const textContent = await page.getTextContent(); + const pageText = textContent.items.map(item => item.str).join(' '); + fullText += pageText + '\n'; + } + + resolve(fullText); + } catch (error) { + reject(error); + } + }; + reader.onerror = reject; + reader.readAsArrayBuffer(file); + }); + }, + + /** + * Extract entities and relationships using OpenAI + * @param {string} text - Text content to analyze + * @param {Function} progressCallback - Callback for progress updates + * @returns {Promise} Extracted entities and relationships + */ + async extractEntitiesAndRelationships(text, progressCallback) { + try { + progressCallback?.('extracting', 'Analyzing text with OpenAI...'); + progressCallback?.('progress', 40); // 40% progress + + // Use API key from environment variables + const apiKey = process.env.VUE_APP_OPENAI_API_KEY; + + // You can also use the existing openai client if already set up in your project + const response = await axios.post( + 'https://api.openai.com/v1/chat/completions', + { + model: 'gpt-4', // or another appropriate model + messages: [ + { + role: 'system', + content: `You are a knowledge graph assistant. Extract entities and relationships from the following text. + Format your output as a JSON object with two arrays: + 1. "entities": Each entity should have "id", "label", "type", and optional "properties". + 2. "relationships": Each relationship should have "source" (entity id), "target" (entity id), "type", and optional "properties". + Focus on key concepts, people, organizations, locations, and important ideas.` + }, + { + role: 'user', + content: text + } + ], + temperature: 0.1, + }, + { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${apiKey}` + } + } + ); + + progressCallback?.('progress', 70); // 70% progress + const content = response.data.choices[0].message.content; + return JSON.parse(content); + } catch (error) { + console.error('Error calling OpenAI API:', error); + throw error; + } + }, + + /** + * Store entities and relationships in Neo4j + * @param {object} data - Data containing entities and relationships + * @param {Function} progressCallback - Callback for progress updates + * @returns {Promise} The stored data + */ + async storeInNeo4j(data, progressCallback) { + const driver = this.initDriver(); + const session = driver.session(); + + try { + progressCallback?.('storing', 'Storing data in Neo4j...'); + progressCallback?.('progress', 80); // 80% progress + + // Clear existing data (optional) + await session.run('MATCH (n) DETACH DELETE n'); + + // Create entities + for (const entity of data.entities) { + const propertiesString = entity.properties + ? `, ${Object.entries(entity.properties).map(([key, value]) => + `${key}: $${key}`).join(', ')}` + : ''; + + await session.run( + `CREATE (n:${entity.type} {id: $id, label: $label${propertiesString}})`, + { + id: entity.id, + label: entity.label, + ...entity.properties + } + ); + } + + // Create relationships + for (const rel of data.relationships) { + const propertiesString = rel.properties + ? `, ${Object.entries(rel.properties).map(([key, value]) => + `${key}: $${key}`).join(', ')}` + : ''; + + await session.run( + `MATCH (a), (b) + WHERE a.id = $sourceId AND b.id = $targetId + CREATE (a)-[r:${rel.type} {${propertiesString}}]->(b)`, + { + sourceId: rel.source, + targetId: rel.target, + ...rel.properties + } + ); + } + + progressCallback?.('progress', 90); // 90% progress + return data; + } finally { + await session.close(); + await driver.close(); + } + }, + + /** + * Retrieve graph data from Neo4j + * @returns {Promise} Nodes and links for visualization + */ + async retrieveFromNeo4j() { + const driver = this.initDriver(); + const session = driver.session(); + + try { + // Get nodes + const nodesResult = await session.run( + `MATCH (n) + RETURN n.id AS id, n.label AS label, labels(n)[0] AS type, + properties(n) AS properties` + ); + + const nodes = nodesResult.records.map(record => { + const properties = record.get('properties'); + delete properties.id; + delete properties.label; + + return { + id: record.get('id'), + label: record.get('label'), + type: record.get('type'), + properties: properties + }; + }); + + // Get relationships + const relsResult = await session.run( + `MATCH (a)-[r]->(b) + RETURN a.id AS source, b.id AS target, type(r) AS type, + properties(r) AS properties` + ); + + const links = relsResult.records.map(record => ({ + source: record.get('source'), + target: record.get('target'), + type: record.get('type'), + properties: record.get('properties') + })); + + return { nodes, links }; + } finally { + await session.close(); + await driver.close(); + } + }, + + /** + * Process a PDF file to create a knowledge graph + * @param {File} file - PDF file to process + * @param {Function} progressCallback - Callback for progress updates + * @returns {Promise} The graph data (nodes and links) + */ + async processPdf(file, progressCallback) { + try { + progressCallback?.('status', 'Starting to process PDF...'); + progressCallback?.('progress', 0); + + // Step 1: Extract text from PDF + const text = await this.extractTextFromPdf(file, progressCallback); + + // Step 2: Extract entities and relationships using OpenAI + const extractedData = await this.extractEntitiesAndRelationships(text, progressCallback); + + // Step 3: Store in Neo4j + await this.storeInNeo4j(extractedData, progressCallback); + + // Step 4: Retrieve from Neo4j for visualization + const graphData = await this.retrieveFromNeo4j(); + + progressCallback?.('status', 'PDF processed successfully!'); + progressCallback?.('progress', 100); + + return graphData; + } catch (error) { + console.error('Error processing PDF:', error); + progressCallback?.('error', error.message); + throw error; + } + } +}; + +export default knowledgeGraphService; \ No newline at end of file