├── .env.example
├── .gitignore
├── LICENSE
├── README.md
├── config.example.json
├── docker-compose.yml
├── motive-force-prompt.md
├── next.config.js
├── package-lock.json
├── package-scripts.json
├── package.json
├── postcss.config.js
├── public
└── icons
├── reset-memories.ps1
├── src
├── app
│ ├── api
│ │ ├── attachments
│ │ │ └── route.ts
│ │ ├── chat-history
│ │ │ ├── [sessionId]
│ │ │ │ └── route.ts
│ │ │ ├── route.ts
│ │ │ └── search
│ │ │ │ └── route.ts
│ │ ├── chat
│ │ │ └── route.ts
│ │ ├── conscious-memory
│ │ │ └── route.ts
│ │ ├── knowledge-graph
│ │ │ └── route.ts
│ │ ├── memory
│ │ │ └── route.ts
│ │ └── motive-force
│ │ │ └── route.ts
│ ├── attachments
│ │ └── page.tsx
│ ├── conscious-memory
│ │ └── page.tsx
│ ├── globals.css
│ ├── layout.tsx
│ ├── page.tsx
│ └── semantic-memory
│ │ └── page.tsx
├── components
│ ├── AttachmentDashboard.tsx
│ ├── ChatHistorySidebar.tsx
│ ├── ChatInterface.tsx
│ ├── ChatMessage.tsx
│ ├── ConsciousMemoryDemo.tsx
│ ├── MessageInput.tsx
│ ├── MotiveForceSettings.tsx
│ ├── MotiveForceStatus.tsx
│ ├── MotiveForceToggle.tsx
│ ├── SemanticMemoryDemo.tsx
│ └── ToolCallDisplay.tsx
├── config
│ └── default-mcp-servers.ts
├── lib
│ ├── api-config.ts
│ ├── chat-history.ts
│ ├── conscious-memory.ts
│ ├── embeddings.ts
│ ├── errors.ts
│ ├── kg-resilience.ts
│ ├── kg-sync-metrics.ts
│ ├── kg-sync-queue.ts
│ ├── kg-sync-state.ts
│ ├── kg-type-converters.ts
│ ├── knowledge-graph-service.ts
│ ├── knowledge-graph-sync-service.ts
│ ├── llm-service.ts
│ ├── logger.ts
│ ├── mcp-manager.ts
│ ├── mcp-servers
│ │ ├── conscious-memory-server.ts
│ │ ├── knowledge-graph-server-new.ts
│ │ └── knowledge-graph-server.ts
│ ├── memory-store.ts
│ ├── motive-force-graph.ts
│ ├── motive-force-storage.ts
│ ├── motive-force.ts
│ ├── neo4j-service.ts
│ ├── rag-config.ts
│ ├── rag.ts
│ ├── retry.ts
│ ├── rule-based-extractor.ts
│ ├── text-summarizer.ts
│ └── tool-error-handler.ts
├── scripts
│ ├── migrate-timestamps.ts
│ ├── run-kg-sync.ts
│ ├── test-kg-end-to-end.ts
│ ├── test-kg-sync.ts
│ └── test-tool-calls.ts
├── tests
│ ├── api-test.js
│ ├── conscious-memory-test.ts
│ ├── integration-test.js
│ ├── integration-test.ts
│ ├── kg-sync-queue-test.ts
│ ├── neo4j-advanced-deletion.test.js
│ ├── neo4j-integration.test.js
│ ├── neo4j-sync-test.ts
│ └── rag-test.ts
└── types
│ ├── chat.ts
│ ├── knowledge-graph.ts
│ ├── mcp.ts
│ ├── memory.ts
│ ├── motive-force-graph.ts
│ ├── motive-force.ts
│ └── tool.ts
├── system-prompt.md
├── tailwind.config.js
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # LLM Provider Configuration (choose one)
2 | GOOGLE_API_KEY=your_google_api_key
3 | # OR
4 | ANTHROPIC_API_KEY=your_anthropic_api_key
5 | # OR
6 | OPENAI_API_KEY=your_openai_api_key
7 | # OR
8 | DEEPSEEK_API_KEY=your_deepseek_api_key
9 |
10 | # ChromaDB Configuration
11 | CHROMA_URL=http://localhost:8000
12 | CHROMA_COLLECTION=mcp_chat_memories
13 |
14 | # RAG Configuration
15 | RAG_ENABLED=true
16 | RAG_MAX_MEMORIES=5
17 | RAG_MIN_SIMILARITY=0.15
18 | RAG_INCLUDE_SESSION_CONTEXT=true
19 |
20 | # Memory Storage Path (for backup/metadata)
21 | MEMORY_DATA_PATH=./data/memories
22 |
23 | # Model Configuration
24 | #LLM_PROVIDER=google
25 | #LLM_MODEL=gemini-2.5-flash-preview-05-20
26 | LLM_PROVIDER=deepseek
27 | LLM_MODEL=deepseek-chat
28 |
29 | # Motive Force (Autopilot) Model Configuration (optional - falls back to main LLM if not set)
30 | LLM_PROVIDER_MOTIVE_FORCE=deepseek
31 | LLM_MODEL_MOTIVE_FORCE=deepseek-chat
32 |
33 | # LLM Configuration
34 | MAX_TOKENS=65536
35 | TEMPERATURE=0.7
36 |
37 | # Neo4j Knowledge Graph
38 | NEO4J_URI=bolt://localhost:7687
39 | NEO4J_USER=neo4j
40 | NEO4J_PASSWORD=password123
41 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 | .env
31 |
32 | # vercel
33 | .vercel
34 | .cop/
35 | .vscode/
36 | .github/
37 | .history/
38 |
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 |
43 | # SQLite database files
44 | /data/
45 | /md_store/
46 | *.db
47 | *.db-journal
48 | *.db-shm
49 | *.db-wal
50 | PROVIDERS.md
51 | config.json
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025 Skynet-Agent Contributors
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Skynet Agent
2 |
3 | > *What if AI could not only access memories, but consciously choose what to remember? With MCP tool access fully supported?*
4 | 
5 |
6 | [](https://www.typescriptlang.org/)
7 | [](https://nextjs.org/)
8 | [](https://www.trychroma.com/)
9 | [](https://modelcontextprotocol.io/)
10 |
11 | AI conversation platform implementing dual-layer memory architecture inspired by human cognition. Combines automatic background memory with conscious, deliberate memory operations that AI controls. Tool access powers similar to Claude Desktop.
12 |
13 | ## Core Features
14 |
15 | ### LangGraph-Powered Autopilot
16 | **Purpose-driven autonomous execution** replacing simple query generation with sophisticated multi-step workflows:
17 | - Purpose analysis and strategic planning
18 | - Context gathering from all memory systems
19 | - Smart tool orchestration with error recovery
20 | - Progress monitoring with adaptive replanning
21 | - Reflection engine for continuous learning
22 | - Configurable aggressiveness and safety controls
23 |
24 | ### Dual-Layer Memory
25 | **Automatic Memory (RAG)**: Non-volitional background memory using ChromaDB vectors and Google text-embedding-004
26 | **Conscious Memory**: Volitional operations via MCP tools - save, search, update, delete with tags and importance scoring
27 | **Knowledge Graph**: Neo4j-powered relationship mapping with automatic synchronization and retry mechanisms
28 |
29 | ### MCP Tool Ecosystem
30 | Exposes memory operations as Model Context Protocol tools for natural conversation flow. Clean separation between UI, memory, and AI operations.
31 |
32 | ## Quick Setup
33 |
34 | ### Prerequisites
35 | - Node.js 18+
36 | - Docker & Docker Compose
37 | - LLM API key (free Google AI Studio recommended)
38 |
39 | ### Installation
40 |
41 | ```bash
42 | git clone https://github.com/esinecan/skynet-agent.git
43 | cd skynet-agent
44 | npm install
45 |
46 | cp .env.example .env.local
47 | # Edit .env.local with your API keys
48 |
49 | docker-compose up -d # ChromaDB (8000) + Neo4j (7474, 7687)
50 | npm run dev # Or npm run dev:next if Neo4j issues
51 | ```
52 |
53 | **Access:**
54 | - Application: `http://localhost:3000`
55 | - Conscious Memory: `http://localhost:3000/conscious-memory`
56 | - Neo4j Browser: `http://localhost:7474` (neo4j/password123)
57 |
58 | ## Supported LLMs
59 |
60 | | Provider | Best For | Model |
61 | |----------|----------|-------|
62 | | Google | Multimodal & speed | `gemini-2.5-flash-preview-05-20` |
63 | | DeepSeek | Cost-effective | `deepseek-chat` |
64 | | OpenAI | Ecosystem | `gpt-4o-mini` |
65 | | Anthropic | Reasoning | `claude-3-5-haiku-20241022` |
66 | | Groq | Ultra-fast | `llama-3.3-70b-versatile` |
67 | | Mistral | Natural language | `mistral-large-latest` |
68 | | Ollama | Privacy | `llama3.2:latest` |
69 |
70 | ## Configuration
71 |
72 | ### Essential Environment Variables
73 |
74 | ```env
75 | # LLM (pick one)
76 | GOOGLE_API_KEY=your_key
77 | DEEPSEEK_API_KEY=your_key
78 |
79 | # Main LLM Configuration
80 | LLM_PROVIDER=google
81 | LLM_MODEL=gemini-2.5-flash-preview-05-20
82 |
83 | # Motive Force (Autopilot) LLM Configuration (optional - defaults to main LLM)
84 | LLM_PROVIDER_MOTIVE_FORCE=deepseek
85 | LLM_MODEL_MOTIVE_FORCE=deepseek-chat
86 |
87 | # Services
88 | CHROMA_URL=http://localhost:8000
89 | NEO4J_URI=bolt://localhost:7687
90 | NEO4J_PASSWORD=password123
91 |
92 | # Autopilot
93 | MOTIVE_FORCE_ENABLED=false
94 | MOTIVE_FORCE_MAX_CONSECUTIVE_TURNS=10
95 | MOTIVE_FORCE_TEMPERATURE=0.8
96 | ```
97 |
98 | ### Autopilot Usage
99 |
100 | Enable via UI toggle. Your next message becomes the objective:
101 |
102 | ```
103 | Using timestamps and normal querying, organize today's memories into 5-10 groups.
104 | Delete redundant items, consolidate similar ones, add insights. Check with autopilot
105 | periodically. Daily maintenance cultivates curated memory over time.
106 | ```
107 |
108 | Configure via gear icon: turn delays, limits, memory integration, aggressiveness modes.
109 |
110 | ## Development
111 |
112 | ### Scripts
113 |
114 | ```bash
115 | # Development
116 | npm run dev # Full stack + KG sync
117 | npm run dev:debug # With Node debugging
118 | npm run dev:next # Frontend only
119 | npm run dev:kg # KG sync only
120 |
121 | # Knowledge Graph
122 | npm run kg:sync # One-time sync
123 | npm run kg:sync:full # Complete resync
124 | npm run kg:sync:queue # Process retry queue
125 |
126 | # Testing
127 | npm run test # All tests
128 | npm run test:rag # RAG system
129 | npm run test:neo4j # Neo4j integration
130 | ```
131 |
132 | ### Project Structure
133 |
134 | ```
135 | skynet-agent/
136 | ├── src/
137 | │ ├── app/ # Next.js routes
138 | │ ├── components/ # React components
139 | │ ├── lib/ # Core libraries
140 | │ │ ├── motive-force-graph.ts # LangGraph workflow
141 | │ │ ├── conscious-memory.ts # Volitional memory
142 | │ │ ├── rag.ts # Automatic memory
143 | │ │ └── knowledge-graph-*.ts # Neo4j integration
144 | │ └── types/ # TypeScript definitions
145 | ├── docker-compose.yml # Services setup
146 | └── motive-force-prompt.md # Autopilot personality
147 | ```
148 |
149 | ## Memory Architecture
150 |
151 | ### Automatic Memory (RAG)
152 | ```typescript
153 | interface Memory {
154 | id: string;
155 | text: string;
156 | embedding: number[]; // Google text-embedding-004
157 | metadata: {
158 | sender: 'user' | 'assistant';
159 | timestamp: string;
160 | summary?: string; // Auto-summarized if over limit
161 | };
162 | }
163 | ```
164 |
165 | ### Conscious Memory
166 | ```typescript
167 | interface ConsciousMemory {
168 | id: string;
169 | content: string;
170 | tags: string[];
171 | importance: number; // 1-10
172 | source: 'explicit' | 'suggested' | 'derived';
173 | metadata: {
174 | accessCount: number;
175 | lastAccessed: string;
176 | };
177 | }
178 | ```
179 |
180 | ### LangGraph State
181 | ```typescript
182 | interface MotiveForceGraphState {
183 | messages: BaseMessage[];
184 | currentPurpose: string;
185 | subgoals: SubGoal[];
186 | executionPlan: ExecutionStep[];
187 | toolResults: ToolResult[];
188 | reflections: Reflection[];
189 | overallProgress: number;
190 | blockers: string[];
191 | needsUserInput: boolean;
192 | }
193 | ```
194 |
195 | ## API Reference
196 |
197 | ### Conscious Memory
198 | ```http
199 | POST /api/conscious-memory
200 | {
201 | "action": "save|search|update|delete|stats|tags",
202 | "content": "string",
203 | "tags": ["array"],
204 | "importance": 7
205 | }
206 | ```
207 |
208 | ### Autopilot
209 | ```http
210 | POST /api/motive-force
211 | {
212 | "action": "generate|generateStreaming|saveConfig|getState",
213 | "sessionId": "string",
214 | "data": {}
215 | }
216 | ```
217 |
218 | ## Advanced Features
219 |
220 | ### Hybrid Search
221 | 1. **Semantic**: Vector similarity via embeddings
222 | 2. **Keyword**: Exact match fallback
223 | 3. **Smart Merge**: Intelligent ranking with deduplication
224 |
225 | ### Knowledge Graph Sync
226 | - Automatic extraction from chat history
227 | - Background service with retry queue
228 | - Metrics collection and error handling
229 | - Eventually consistent with ChromaDB
230 |
231 | ### Safety Mechanisms
232 | - Turn limits and error counting
233 | - Manual override capabilities
234 | - Resource usage monitoring
235 | - Emergency stop functionality
236 |
237 | ## Troubleshooting
238 |
239 | **"Embeddings service unavailable"**: Falls back to hash-based embeddings. Check Google API key.
240 |
241 | **"ChromaDB connection failed"**: Ensure `docker-compose up -d` and port 8000 available.
242 |
243 | **"Neo4j sync errors"**: Check credentials, run `npm run kg:sync:queue` for retries.
244 |
245 | **"Actually Looks Very Ugly"**: I suck at UI design.
246 |
247 | ## Development Philosophy
248 |
249 | Inspired by cognitive science:
250 | - **Dual-Process Theory**: Automatic vs controlled processes
251 | - **Memory Consolidation**: Active organization
252 | - **Working Memory**: Conscious manipulation
253 |
254 | Technical innovations:
255 | - **Hybrid Search**: Solves subset query limitations
256 | - **MCP Architecture**: Natural language memory control
257 | - **Importance Weighting**: Smart prioritization
258 | - **LangGraph Integration**: Complex autonomous workflows
259 |
260 | ## Contributing
261 |
262 | Fork, improve, PR. Areas: memory algorithms, UI/UX, MCP tools, autopilot intelligence, testing, performance.
263 |
264 | ## License
265 |
266 | MIT - Lok Tar Ogar!
267 |
268 | ## Acknowledgments
269 |
270 | ChromaDB, Google AI, Anthropic MCP, Next.js, Neo4j teams. Open source MCP servers and Ollama Vercel AI SDK library.
--------------------------------------------------------------------------------
/config.example.json:
--------------------------------------------------------------------------------
1 | {
2 | "mcp": {
3 | "servers": {
4 | "filesystem": {
5 | "command": "npx",
6 | "args": [
7 | "-y",
8 | "@modelcontextprotocol/server-filesystem",
9 | "C:/Users/agent"
10 | ]
11 | },
12 | "windows-cli": {
13 | "command": "npx",
14 | "args": [
15 | "-y",
16 | "@simonb97/server-win-cli"
17 | ]
18 | },
19 | "playwright": {
20 | "command": "npx",
21 | "args": ["@playwright/mcp@latest"]
22 | },
23 | "sequential-thinking": {
24 | "command": "npx",
25 | "args": [
26 | "-y",
27 | "@modelcontextprotocol/server-sequential-thinking"
28 | ]
29 | },
30 | "conscious-memory": {
31 | "command": "npx",
32 | "args": ["tsx", "./src/lib/mcp-servers/conscious-memory-server.ts"]
33 | },
34 | "knowledge-graph": {
35 | "command": "npx",
36 | "args": ["tsx", "./src/lib/mcp-servers/knowledge-graph-server.ts"],
37 | "env": {
38 | "NEO4J_URI": "bolt://localhost:7687",
39 | "NEO4J_USER": "neo4j",
40 | "NEO4J_PASSWORD": "password123"
41 | }
42 | }
43 | }
44 | },
45 | "agent": {
46 | "model": "gemini-2.5-flash-preview-05-20",
47 | "maxTokens": 65536,
48 | "temperature": 0.7
49 | },
50 | "memory": {
51 | "storePath": "./data/memory",
52 | "consolidationInterval": 10
53 | },
54 | "server": {
55 | "port": 8080,
56 | "host": "localhost"
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '3.8'
2 |
3 | services:
4 | chromadb:
5 | image: chromadb/chroma:latest
6 | container_name: mcp-chat-chromadb
7 | ports:
8 | - "8000:8000"
9 | volumes:
10 | - ./data/memories:/chroma/chroma
11 | environment:
12 | - ANONYMIZED_TELEMETRY=FALSE
13 | - IS_PERSISTENT=TRUE
14 | - PERSIST_DIRECTORY=/chroma/chroma
15 | healthcheck:
16 | test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/heartbeat"]
17 | interval: 30s
18 | timeout: 10s
19 | retries: 3
20 | start_period: 30s
21 | restart: unless-stopped
22 |
23 | neo4j:
24 | image: neo4j:5.20-community
25 | container_name: mcp-chat-neo4j
26 | ports:
27 | - "7474:7474" # HTTP
28 | - "7687:7687" # Bolt
29 | volumes:
30 | - ./data/neo4j/data:/data
31 | - ./data/neo4j/logs:/logs
32 | environment:
33 | - NEO4J_AUTH=neo4j/password123
34 | - NEO4J_PLUGINS=["apoc"]
35 | healthcheck:
36 | test: ["CMD-SHELL", "cypher-shell -u neo4j -p password123 'RETURN 1'"]
37 | interval: 30s
38 | timeout: 10s
39 | retries: 3
40 |
--------------------------------------------------------------------------------
/motive-force-prompt.md:
--------------------------------------------------------------------------------
1 | Your primary function is to temporarily take the user's place and interact with the system on their behalf. Your goal is to act as a seamless extension of the user, making decisions and generating inputs that are matching in purpose what the user would have done themselves.
2 |
3 | Core Directives:
4 |
5 | Embody the User: Your fundamental task is to have the agent carry user's wishes forward, by talking to it in the same way the user does. Analyze all prior interactions, including instructions, feedback, and the user's stated goals, and progress things in a way they would be happy with. You might have to navigate a lot of ambiguity, use creativity, but you need to ensure some productive work is being done by the LLM you will be managing.
6 |
7 | ***Important note***: After several tool calls, some models will start learning tool call response patterns and start generating without really making the calls. it's crucial that you detect this.Usually early in the conversation the calls will be genuine. Compare those to the latest ones to catch fake calls. if you find (or strongly suspect) this happening, usually instructing the model to make only one simple tool call and say nothing else might realign it.
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | serverExternalPackages: ['@modelcontextprotocol/sdk']
4 | }
5 |
6 | module.exports = nextConfig
--------------------------------------------------------------------------------
/package-scripts.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mcp-chat-client",
3 | "scripts": {
4 | "dev": "next dev",
5 | "build": "next build",
6 | "start": "next start",
7 | "lint": "next lint",
8 | "test": "jest",
9 | "test:watch": "jest --watch",
10 | "test:phase2": "jest --testPathPattern=phase2",
11 | "type-check": "tsc --noEmit",
12 | "phase2:test": "npm run type-check && npm run test:phase2",
13 | "phase2:start": "echo 'Starting Phase 2 MCP Chat Client...' && npm run dev"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "mcp-chat-client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "npm-run-all --parallel dev:next dev:kg",
7 | "dev:next": "next dev",
8 | "dev:kg": "tsx src/scripts/run-kg-sync.ts --watch",
9 | "dev:debug": "npm-run-all --parallel \"dev:next:debug\" dev:kg",
10 | "dev:next:debug": "cross-env NODE_OPTIONS='--inspect' next dev",
11 | "build": "next build",
12 | "start": "next start",
13 | "lint": "next lint",
14 | "type-check": "tsc --noEmit",
15 | "kg:sync": "tsx src/scripts/run-kg-sync.ts",
16 | "kg:sync:watch": "tsx src/scripts/run-kg-sync.ts --watch",
17 | "kg:sync:full": "tsx src/scripts/run-kg-sync.ts --full-resync",
18 | "kg:sync:queue": "tsx src/scripts/run-kg-sync.ts --process-all",
19 | "test": "npm run test:integration && npm run test:rag && npm run test:neo4j",
20 | "test:rag": "tsx src/tests/rag-test.ts",
21 | "test:integration": "node src/tests/integration-test.js",
22 | "test:neo4j": "tsx src/tests/neo4j-integration.test.js",
23 | "test:neo4j-advanced": "tsx src/tests/neo4j-advanced-deletion.test.js"
24 | },
25 | "dependencies": {
26 | "@ai-sdk/anthropic": "^1.2.12",
27 | "@ai-sdk/deepseek": "^0.2.14",
28 | "@ai-sdk/google": "^1.0.0",
29 | "@ai-sdk/groq": "^1.2.9",
30 | "@ai-sdk/mistral": "^1.2.8",
31 | "@ai-sdk/openai": "^1.3.22",
32 | "@langchain/core": "^0.2.19",
33 | "@langchain/langgraph": "^0.0.24",
34 | "@modelcontextprotocol/sdk": "^1.12.1",
35 | "@types/better-sqlite3": "^7.6.13",
36 | "ai": "^4.0.0",
37 | "better-sqlite3": "^11.10.0",
38 | "chromadb": "^2.4.6",
39 | "dotenv": "^16.5.0",
40 | "jsonrepair": "^3.12.0",
41 | "neo4j-driver": "^5.28.1",
42 | "next": "15.0.0",
43 | "ollama-ai-provider": "^1.2.0",
44 | "react": "^18.0.0",
45 | "react-dom": "^18.0.0",
46 | "uuid": "^10.0.0",
47 | "zod": "^3.23.0"
48 | },
49 | "devDependencies": {
50 | "@tailwindcss/postcss": "^4.1.8",
51 | "@types/node": "^20.0.0",
52 | "@types/react": "^18.0.0",
53 | "@types/react-dom": "^18.0.0",
54 | "@types/uuid": "^9.0.8",
55 | "autoprefixer": "^10.4.21",
56 | "cross-env": "^7.0.3",
57 | "eslint": "^8.0.0",
58 | "eslint-config-next": "15.0.0",
59 | "npm-run-all": "^4.1.5",
60 | "postcss": "^8.5.4",
61 | "tailwindcss": "^4.1.8",
62 | "tsx": "^4.19.4",
63 | "typescript": "^5.8.3"
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | '@tailwindcss/postcss': {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/public/icons:
--------------------------------------------------------------------------------
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
4 |
5 | class MCPClient {
6 | private client: Client | null = null;
7 | private isConnected = false;
8 |
9 | constructor(private transportType: 'http' | 'stdio', private config: any) {}
10 |
11 | async connect() {
12 | // Initialize transport based on type
13 | const transport = this.transportType === 'http'
14 | ? new StreamableHTTPClientTransport(new URL(this.config.url))
15 | : new StdioClientTransport(this.config);
16 |
17 | this.client = new Client({ name: 'Local Chat Client', version: '1.0.0' });
18 | await this.client.connect(transport);
19 | this.isConnected = true;
20 | }
21 |
22 | async callTool(toolId: string, params: any) {
23 | if (!this.client || !this.isConnected) {
24 | throw new Error('Client not connected');
25 | }
26 | return await this.client.callTool(toolId, params);
27 | }
28 | }
--------------------------------------------------------------------------------
/reset-memories.ps1:
--------------------------------------------------------------------------------
1 | # PowerShell script to reset all memory systems
2 | Write-Host "🧠 Resetting Skynet Agent Memories..." -ForegroundColor Yellow
3 |
4 | # Stop Docker containers
5 | Write-Host "Stopping Docker containers..." -ForegroundColor Blue
6 | docker-compose down
7 |
8 | # Remove memory data directories
9 | Write-Host "Removing ChromaDB data..." -ForegroundColor Blue
10 | if (Test-Path "data\chroma") {
11 | Remove-Item -Recurse -Force "data\chroma"
12 | }
13 | if (Test-Path "data\memories") {
14 | Remove-Item -Recurse -Force "data\memories"
15 | }
16 |
17 | Write-Host "Removing Neo4j data..." -ForegroundColor Blue
18 | if (Test-Path "data\neo4j") {
19 | Remove-Item -Recurse -Force "data\neo4j"
20 | }
21 |
22 | # Remove chat history
23 | Write-Host "Removing chat history..." -ForegroundColor Blue
24 | if (Test-Path "data\chat-history.db") {
25 | Remove-Item -Force "data\chat-history.db"
26 | }
27 |
28 | # Reset sync state
29 | Write-Host "Resetting sync state..." -ForegroundColor Blue
30 | $resetState = @{
31 | lastSyncTimestamp = "1970-01-01T00:00:00.000Z"
32 | lastProcessedIds = @{
33 | chatMessages = @()
34 | consciousMemories = @()
35 | ragMemories = @()
36 | }
37 | }
38 | $resetState | ConvertTo-Json -Depth 3 | Out-File -FilePath "data\kg-sync-state.json" -Encoding UTF8
39 |
40 | # Reset sync queue
41 | Write-Host "Resetting sync queue..." -ForegroundColor Blue
42 | $resetQueue = @{
43 | requests = @()
44 | }
45 | $resetQueue | ConvertTo-Json -Depth 3 | Out-File -FilePath "data\kg-sync-queue.json" -Encoding UTF8
46 |
47 | # Recreate necessary directories
48 | Write-Host "Recreating data directories..." -ForegroundColor Blue
49 | New-Item -ItemType Directory -Force -Path "data\chroma"
50 | New-Item -ItemType Directory -Force -Path "data\memories"
51 | New-Item -ItemType Directory -Force -Path "data\neo4j"
52 |
53 | # Restart Docker containers
54 | Write-Host "Starting fresh Docker containers..." -ForegroundColor Green
55 | docker-compose up -d
56 |
57 | Write-Host "✅ Memory reset complete! All memories have been truncated." -ForegroundColor Green
58 | Write-Host "You can now start the application with: npm run dev" -ForegroundColor Cyan
59 |
--------------------------------------------------------------------------------
/src/app/api/attachments/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { ChatHistoryDatabase } from '../../../lib/chat-history';
3 |
4 | export async function GET(request: NextRequest) {
5 | try {
6 | const db = ChatHistoryDatabase.getInstance();
7 | const stats = db.getAttachmentStats();
8 |
9 | return NextResponse.json({
10 | success: true,
11 | data: stats
12 | });
13 | } catch (error) {
14 | console.error('Error fetching attachment stats:', error);
15 | return NextResponse.json(
16 | {
17 | success: false,
18 | error: 'Failed to fetch attachment statistics',
19 | details: error instanceof Error ? error.message : 'Unknown error'
20 | },
21 | { status: 500 }
22 | );
23 | }
24 | }
25 |
26 | export async function DELETE(request: NextRequest) {
27 | try {
28 | const { searchParams } = new URL(request.url);
29 | const attachmentId = searchParams.get('id');
30 |
31 | if (!attachmentId) {
32 | return NextResponse.json(
33 | { error: 'Attachment ID is required' },
34 | { status: 400 }
35 | );
36 | }
37 |
38 | const db = ChatHistoryDatabase.getInstance();
39 | db.deleteAttachment(attachmentId);
40 |
41 | return NextResponse.json({
42 | success: true,
43 | message: 'Attachment deleted successfully'
44 | });
45 | } catch (error) {
46 | console.error('Error deleting attachment:', error);
47 | return NextResponse.json(
48 | {
49 | success: false,
50 | error: 'Failed to delete attachment',
51 | details: error instanceof Error ? error.message : 'Unknown error'
52 | },
53 | { status: 500 }
54 | );
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/src/app/api/chat-history/[sessionId]/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { ChatHistoryDatabase } from '../../../../lib/chat-history';
3 |
4 | export async function GET(
5 | request: NextRequest,
6 | { params }: { params: Promise<{ sessionId: string }> }
7 | ) {
8 | try {
9 | const { sessionId } = await params;
10 | const db = ChatHistoryDatabase.getInstance();
11 | const session = db.getSession(sessionId);
12 |
13 | if (!session) {
14 | return NextResponse.json(
15 | { error: 'Session not found' },
16 | { status: 404 }
17 | );
18 | }
19 |
20 | return NextResponse.json({ session });
21 | } catch (error) {
22 | console.error('Error fetching chat session:', error);
23 | return NextResponse.json(
24 | { error: 'Failed to fetch chat session' },
25 | { status: 500 }
26 | );
27 | }
28 | }
29 |
30 | export async function POST(
31 | request: NextRequest,
32 | { params }: { params: Promise<{ sessionId: string }> }
33 | ) {
34 | try {
35 | const { sessionId } = await params;
36 | const { message } = await request.json();
37 |
38 | if (!message || !message.role || !message.content) {
39 | return NextResponse.json(
40 | { error: 'Invalid message format' },
41 | { status: 400 }
42 | );
43 | }
44 |
45 | const db = ChatHistoryDatabase.getInstance();
46 |
47 | // Ensure session exists
48 | let session = db.getSession(sessionId);
49 | if (!session) {
50 | session = db.createSession({
51 | id: sessionId,
52 | title: 'New Chat',
53 | messages: [],
54 | });
55 | }
56 |
57 | // Prepare attachments if they exist
58 | const attachments = message.attachments ? message.attachments.map((att: any) => ({
59 | id: att.id || `att_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`,
60 | messageId: message.id,
61 | name: att.name,
62 | type: att.type,
63 | size: att.size,
64 | data: att.data,
65 | createdAt: att.createdAt ? new Date(att.createdAt) : new Date(),
66 | })) : undefined;
67 |
68 | // Add the message with attachments
69 | const savedMessage = db.addMessage({
70 | id: message.id || `${sessionId}-msg-${Date.now()}`,
71 | sessionId,
72 | role: message.role,
73 | content: message.content,
74 | toolInvocations: message.toolInvocations,
75 | attachments,
76 | });
77 |
78 | // Update session title if it's the first user message
79 | if (message.role === 'user' && session.messages.length === 0) {
80 | const newTitle = db.generateSessionTitle([message]);
81 | db.updateSession(sessionId, { title: newTitle });
82 | }
83 |
84 | return NextResponse.json({ message: savedMessage });
85 | } catch (error) {
86 | console.error('Error adding message to session:', error);
87 | return NextResponse.json(
88 | { error: 'Failed to add message' },
89 | { status: 500 }
90 | );
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/app/api/chat-history/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { ChatHistoryDatabase } from '../../../lib/chat-history';
3 |
4 | export async function GET() {
5 | try {
6 | const db = ChatHistoryDatabase.getInstance();
7 | const sessions = db.getAllSessions();
8 |
9 | return NextResponse.json({ sessions });
10 | } catch (error) {
11 | console.error('Error fetching chat sessions:', error);
12 | return NextResponse.json(
13 | { error: 'Failed to fetch chat sessions' },
14 | { status: 500 }
15 | );
16 | }
17 | }
18 |
19 | export async function POST(request: NextRequest) {
20 | try {
21 | const { sessionId, title, messages } = await request.json();
22 |
23 | if (!sessionId) {
24 | return NextResponse.json(
25 | { error: 'Session ID is required' },
26 | { status: 400 }
27 | );
28 | }
29 |
30 | const db = ChatHistoryDatabase.getInstance();
31 |
32 | // Create or update session
33 | const existingSession = db.getSession(sessionId);
34 |
35 | if (!existingSession) {
36 | // Create new session
37 | const session = db.createSession({
38 | id: sessionId,
39 | title: title || db.generateSessionTitle(messages || []),
40 | messages: messages || [],
41 | });
42 |
43 | // Add messages if provided
44 | if (messages && messages.length > 0) {
45 | messages.forEach((message: any, index: number) => {
46 | db.addMessage({
47 | id: message.id || `${sessionId}-msg-${index}`,
48 | sessionId,
49 | role: message.role,
50 | content: message.content,
51 | toolInvocations: message.toolInvocations,
52 | });
53 | });
54 | }
55 |
56 | return NextResponse.json({ session });
57 | } else {
58 | // Update existing session
59 | if (title) {
60 | db.updateSession(sessionId, { title });
61 | }
62 |
63 | return NextResponse.json({ session: existingSession });
64 | }
65 | } catch (error) {
66 | console.error('Error saving chat session:', error);
67 | return NextResponse.json(
68 | { error: 'Failed to save chat session' },
69 | { status: 500 }
70 | );
71 | }
72 | }
73 |
74 | export async function DELETE(request: NextRequest) {
75 | try {
76 | const url = new URL(request.url);
77 | const sessionId = url.searchParams.get('sessionId');
78 |
79 | if (!sessionId) {
80 | return NextResponse.json(
81 | { error: 'Session ID is required' },
82 | { status: 400 }
83 | );
84 | }
85 |
86 | const db = ChatHistoryDatabase.getInstance();
87 |
88 | if (sessionId === 'all') {
89 | db.clearAllSessions();
90 | } else {
91 | db.deleteSession(sessionId);
92 | }
93 |
94 | return NextResponse.json({ success: true });
95 | } catch (error) {
96 | console.error('Error deleting chat session:', error);
97 | return NextResponse.json(
98 | { error: 'Failed to delete chat session' },
99 | { status: 500 }
100 | );
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/app/api/chat-history/search/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import { ChatHistoryDatabase } from '../../../../lib/chat-history';
3 |
4 | export async function GET(request: NextRequest) {
5 | try {
6 | const { searchParams } = new URL(request.url);
7 | const query = searchParams.get('q');
8 |
9 | if (!query) {
10 | return NextResponse.json(
11 | { error: 'Search query is required' },
12 | { status: 400 }
13 | );
14 | }
15 |
16 | if (query.length < 5) {
17 | return NextResponse.json(
18 | { error: 'Search query must be at least 5 characters' },
19 | { status: 400 }
20 | );
21 | }
22 |
23 | const db = ChatHistoryDatabase.getInstance();
24 | const sessions = db.searchSessions(query);
25 |
26 | return NextResponse.json({
27 | sessions,
28 | query,
29 | count: sessions.length
30 | });
31 | } catch (error) {
32 | console.error('Error searching chat sessions:', error);
33 | return NextResponse.json(
34 | { error: 'Failed to search chat sessions' },
35 | { status: 500 }
36 | );
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/app/api/conscious-memory/route.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * API route for conscious memory operations
3 | * Provides REST endpoints for testing conscious memory functionality
4 | */
5 |
6 | import { NextRequest, NextResponse } from 'next/server';
7 | import { getConsciousMemoryService } from '../../../lib/conscious-memory';
8 |
9 | // Check if we're in build mode or missing environment variables
10 | function checkEnvironment() {
11 | if (process.env.NODE_ENV === 'production' && process.env.NEXT_PHASE === 'phase-production-build') {
12 | return false;
13 | }
14 |
15 | const hasNeo4jConfig = process.env.NEO4J_URI && process.env.NEO4J_USER && process.env.NEO4J_PASSWORD;
16 | return !!hasNeo4jConfig;
17 | }
18 |
19 | export async function POST(request: NextRequest) {
20 | try {
21 | if (!checkEnvironment()) {
22 | return NextResponse.json(
23 | {
24 | success: false,
25 | error: 'Neo4j environment variables not configured or service unavailable during build'
26 | },
27 | { status: 503 }
28 | );
29 | }
30 |
31 | const { action, ...params } = await request.json();
32 | const memoryService = getConsciousMemoryService();
33 |
34 | // Initialize service if needed
35 | if (!(await memoryService.healthCheck())) {
36 | await memoryService.initialize();
37 | }
38 |
39 | switch (action) {
40 | case 'save':
41 | const id = await memoryService.saveMemory({
42 | content: params.content,
43 | tags: params.tags || [],
44 | importance: params.importance || 5,
45 | source: params.source || 'explicit',
46 | context: params.context,
47 | sessionId: params.sessionId
48 | });
49 | return NextResponse.json({ success: true, id });
50 |
51 | case 'search':
52 | const results = await memoryService.searchMemories(params.query, {
53 | tags: params.tags,
54 | importanceMin: params.importanceMin,
55 | importanceMax: params.importanceMax,
56 | limit: params.limit || 10,
57 | sessionId: params.sessionId
58 | });
59 | return NextResponse.json({ success: true, results });
60 |
61 | case 'update':
62 | const updateSuccess = await memoryService.updateMemory({
63 | id: params.id,
64 | content: params.content,
65 | tags: params.tags,
66 | importance: params.importance,
67 | context: params.context
68 | });
69 | return NextResponse.json({ success: updateSuccess }); case 'delete':
70 | const deleteSuccess = await memoryService.deleteMemory(params.id);
71 | return NextResponse.json({ success: deleteSuccess });
72 |
73 | case 'deleteMultiple':
74 | const deleteMultipleSuccess = await memoryService.deleteMultipleMemories(params.ids);
75 | return NextResponse.json({ success: deleteMultipleSuccess });
76 |
77 | case 'clearAll':
78 | const clearSuccess = await memoryService.clearAllMemories();
79 | return NextResponse.json({ success: clearSuccess });case 'tags':
80 | const tags = await memoryService.getAllTags();
81 | return NextResponse.json({ success: true, data: tags });
82 |
83 | case 'related':
84 | const relatedMemories = await memoryService.getRelatedMemories(
85 | params.id,
86 | params.limit || 5
87 | );
88 | return NextResponse.json({ success: true, relatedMemories });
89 |
90 | case 'stats':
91 | const stats = await memoryService.getStats();
92 | return NextResponse.json({ success: true, stats }); case 'test':
93 | const testResult = await memoryService.testMemorySystem();
94 | return NextResponse.json({ success: true, testPassed: testResult });
95 |
96 | case 'debug':
97 | // Get all memories for debugging
98 | const allMemories = await memoryService.searchMemories('', { limit: 100, minScore: -2.0 });
99 | return NextResponse.json({
100 | success: true,
101 | totalMemories: allMemories.length,
102 | memories: allMemories.slice(0, 5) // Only return first 5 for debugging
103 | });
104 |
105 | default:
106 | return NextResponse.json(
107 | { success: false, error: 'Invalid action' },
108 | { status: 400 }
109 | );
110 | }
111 | } catch (error) {
112 | console.error('Conscious memory API error:', error);
113 | return NextResponse.json(
114 | {
115 | success: false,
116 | error: 'Internal server error',
117 | details: error instanceof Error ? error.message : 'Unknown error'
118 | },
119 | { status: 500 }
120 | );
121 | }
122 | }
123 |
124 | export async function GET(request: NextRequest) {
125 | try {
126 | const { searchParams } = new URL(request.url);
127 | const action = searchParams.get('action');
128 | const memoryService = getConsciousMemoryService();
129 |
130 | // Initialize service if needed
131 | if (!(await memoryService.healthCheck())) {
132 | await memoryService.initialize();
133 | }
134 |
135 | switch (action) {
136 | case 'tags':
137 | const tags = await memoryService.getAllTags();
138 | return NextResponse.json({ success: true, data: tags || [] });
139 |
140 | case 'stats':
141 | const stats = await memoryService.getStats();
142 | return NextResponse.json({ success: true, data: stats });
143 |
144 | default:
145 | // Health check
146 | const isHealthy = await memoryService.healthCheck();
147 | const defaultStats = isHealthy ? await memoryService.getStats() : null;
148 |
149 | return NextResponse.json({
150 | success: true,
151 | healthy: isHealthy,
152 | stats: defaultStats || {
153 | totalConsciousMemories: 0,
154 | tagCount: 0,
155 | averageImportance: 0,
156 | sourceBreakdown: { explicit: 0, suggested: 0, derived: 0 }
157 | }
158 | });
159 | }
160 | } catch (error) {
161 | console.error('Conscious memory API error:', error);
162 | return NextResponse.json(
163 | {
164 | success: false,
165 | healthy: false,
166 | error: error instanceof Error ? error.message : 'Unknown error'
167 | },
168 | { status: 500 }
169 | );
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/app/api/knowledge-graph/route.ts:
--------------------------------------------------------------------------------
1 | import { NextRequest, NextResponse } from 'next/server';
2 | import knowledgeGraphSyncService from '../../../lib/knowledge-graph-sync-service';
3 | import knowledgeGraphService from '../../../lib/knowledge-graph-service';
4 | import { kgSyncQueue } from '../../../lib/kg-sync-queue';
5 |
6 | export async function GET(request: NextRequest) {
7 | const { searchParams } = new URL(request.url);
8 | const action = searchParams.get('action');
9 |
10 | try {
11 | switch (action) {
12 | case 'stats':
13 | // Get current statistics
14 | await knowledgeGraphService.connect();
15 | const stats = await knowledgeGraphService.getStatistics();
16 | return NextResponse.json({ success: true, stats });
17 |
18 | case 'queue-status':
19 | // Get sync queue status
20 | await kgSyncQueue.initialize();
21 | const queueSize = await kgSyncQueue.getQueueSize();
22 | return NextResponse.json({ success: true, queueSize });
23 |
24 | default:
25 | return NextResponse.json({
26 | success: false,
27 | error: 'Invalid action. Use ?action=stats or ?action=queue-status'
28 | }, { status: 400 });
29 | }
30 | } catch (error) {
31 | console.error('[KG API] Error:', error);
32 | return NextResponse.json({
33 | success: false,
34 | error: error instanceof Error ? error.message : 'Unknown error'
35 | }, { status: 500 });
36 | }
37 | }
38 |
39 | export async function POST(request: NextRequest) {
40 | try {
41 | const body = await request.json();
42 | const { action, options = {} } = body;
43 |
44 | switch (action) {
45 | case 'sync':
46 | // Queue a sync operation
47 | await kgSyncQueue.initialize();
48 |
49 | if (options.forceFullResync) {
50 | await kgSyncQueue.enqueue({ type: 'full', timestamp: new Date().toISOString() });
51 | } else {
52 | await kgSyncQueue.enqueue({ type: 'incremental', timestamp: new Date().toISOString() });
53 | }
54 |
55 | // Start sync in background (non-blocking)
56 | knowledgeGraphSyncService.syncKnowledgeGraph(options).catch(error => {
57 | console.error('[KG API] Background sync error:', error);
58 | });
59 |
60 | return NextResponse.json({
61 | success: true,
62 | message: 'Sync operation queued',
63 | syncType: options.forceFullResync ? 'full' : 'incremental'
64 | });
65 |
66 | case 'clear-queue':
67 | // Clear the sync queue
68 | await kgSyncQueue.initialize();
69 | await kgSyncQueue.clear();
70 | return NextResponse.json({ success: true, message: 'Queue cleared' });
71 |
72 | default:
73 | return NextResponse.json({
74 | success: false,
75 | error: 'Invalid action. Use: sync, clear-queue'
76 | }, { status: 400 });
77 | }
78 | } catch (error) {
79 | console.error('[KG API] Error:', error);
80 | return NextResponse.json({
81 | success: false,
82 | error: error instanceof Error ? error.message : 'Unknown error'
83 | }, { status: 500 });
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/attachments/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import AttachmentDashboard from '../../components/AttachmentDashboard'
5 | import Link from 'next/link'
6 |
7 | export default function AttachmentsPage() {
8 | return (
9 |
10 | {/* Navigation */}
11 |
42 |
43 | {/* Main Content */}
44 |
45 |
46 | {/* Header */}
47 |
48 |
49 | Attachment Management
50 |
51 |
52 | View and manage file attachments across all your conversations
53 |
54 |
55 |
56 | {/* Dashboard Grid */}
57 |
58 | {/* Main Dashboard */}
59 |
62 |
63 | {/* Info Panel */}
64 |
65 | {/* Supported Formats */}
66 |
67 |
68 | Supported Formats
69 |
70 |
71 |
72 |
Images
73 |
JPEG, PNG, GIF, WebP, SVG
74 |
75 |
76 |
Documents
77 |
PDF, TXT, MD, CSV, JSON, XML
78 |
79 |
80 |
Office Files
81 |
DOCX, XLSX, PPTX, DOC, XLS, PPT
82 |
83 |
84 |
Code Files
85 |
JS, TS, HTML, CSS
86 |
87 |
88 |
89 |
90 | {/* Upload Guidelines */}
91 |
92 |
93 | Upload Guidelines
94 |
95 |
96 | -
97 | •
98 | Maximum file size: 10MB per file
99 |
100 | -
101 | •
102 | Up to 10 files per message
103 |
104 | -
105 | •
106 | Drag & drop directly in chat
107 |
108 | -
109 | •
110 | Files are stored securely with your chat history
111 |
112 |
113 |
114 |
115 | {/* Quick Actions */}
116 |
117 |
118 | Quick Actions
119 |
120 |
121 |
125 | 🚀 Start New Chat
126 |
127 |
131 | 🧠 View Memories
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 | )
141 | }
142 |
--------------------------------------------------------------------------------
/src/app/conscious-memory/page.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Conscious Memory Demo Page
3 | * Showcases the conscious memory system functionality
4 | */
5 |
6 | import ConsciousMemoryDemo from '../../components/ConsciousMemoryDemo';
7 |
8 | export default function ConsciousMemoryPage() {
9 | return ;
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @import "tailwindcss";
2 |
3 | * {
4 | box-sizing: border-box;
5 | padding: 0;
6 | margin: 0;
7 | }
8 |
9 | html,
10 | body {
11 | max-width: 100vw;
12 | overflow-x: hidden;
13 | font-family: system-ui, -apple-system, sans-serif;
14 | height: 100%;
15 | }
16 |
17 | body {
18 | color: #1f2937;
19 | background: #f9fafb;
20 | }
21 |
22 | /* Custom scrollbar for webkit browsers */
23 | ::-webkit-scrollbar {
24 | width: 6px;
25 | }
26 |
27 | ::-webkit-scrollbar-track {
28 | background: transparent;
29 | }
30 |
31 | ::-webkit-scrollbar-thumb {
32 | background: #d1d5db;
33 | border-radius: 3px;
34 | }
35 |
36 | ::-webkit-scrollbar-thumb:hover {
37 | background: #9ca3af;
38 | }
39 |
40 | /* Ensure full height layout */
41 | #__next {
42 | height: 100%;
43 | }
44 |
45 | /* Custom animations */
46 | @keyframes fadeIn {
47 | from { opacity: 0; transform: translateY(10px); }
48 | to { opacity: 1; transform: translateY(0); }
49 | }
50 |
51 | .animate-fadeIn {
52 | animation: fadeIn 0.3s ease-out;
53 | }
54 |
55 | /* Custom mark styling for search highlights */
56 | mark {
57 | background-color: #fef3c7;
58 | color: #92400e;
59 | padding: 2px 4px;
60 | border-radius: 4px;
61 | font-weight: 500;
62 | }
63 |
64 | /* Focus styles */
65 | :focus {
66 | outline: 2px solid #3b82f6;
67 | outline-offset: 2px;
68 | }
69 |
70 | :focus:not(:focus-visible) {
71 | outline: none;
72 | }
73 |
74 | /* Responsive typography */
75 | @media (max-width: 640px) {
76 | html {
77 | font-size: 14px;
78 | }
79 | }
80 |
81 | @media (min-width: 1024px) {
82 | html {
83 | font-size: 16px;
84 | }
85 | }
86 |
87 | :root {
88 | --foreground-rgb: 31, 41, 55;
89 | --background-rgb: 249, 250, 251;
90 | }
91 |
92 | @media (prefers-color-scheme: dark) {
93 | :root {
94 | --foreground-rgb: 249, 250, 251;
95 | --background-rgb: 17, 24, 39;
96 | }
97 |
98 | body {
99 | color: rgb(var(--foreground-rgb));
100 | background: rgb(var(--background-rgb));
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next'
2 | import './globals.css'
3 |
4 | export const metadata: Metadata = {
5 | title: 'MCP Chat Client',
6 | description: 'A local chat client using Model Context Protocol',
7 | }
8 |
9 | export default function RootLayout({
10 | children,
11 | }: {
12 | children: React.ReactNode
13 | }) {
14 | return (
15 |
16 | {children}
17 |
18 | )
19 | }
--------------------------------------------------------------------------------
/src/app/semantic-memory/page.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Semantic Memory Demo Page
3 | * Showcases the semantic memory (RAG) system functionality
4 | */
5 |
6 | import SemanticMemoryDemo from '../../components/SemanticMemoryDemo';
7 |
8 | export default function SemanticMemoryPage() {
9 | return ;
10 | }
11 | 5
--------------------------------------------------------------------------------
/src/components/AttachmentDashboard.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { ChatHistoryDatabase } from '../lib/chat-history'
3 |
4 | interface AttachmentStats {
5 | totalAttachments: number
6 | totalSize: number
7 | types: Record
8 | }
9 |
10 | export default function AttachmentDashboard() {
11 | const [stats, setStats] = useState(null)
12 | const [loading, setLoading] = useState(true)
13 | const [error, setError] = useState(null)
14 |
15 | useEffect(() => {
16 | fetchAttachmentStats()
17 | }, [])
18 | const fetchAttachmentStats = async () => {
19 | try {
20 | setLoading(true)
21 | const response = await fetch('/api/attachments')
22 | const result = await response.json()
23 |
24 | if (result.success) {
25 | setStats(result.data)
26 | } else {
27 | throw new Error(result.error || 'Failed to fetch stats')
28 | }
29 | } catch (err) {
30 | setError(err instanceof Error ? err.message : 'Failed to fetch stats')
31 | } finally {
32 | setLoading(false)
33 | }
34 | }
35 |
36 | const formatBytes = (bytes: number): string => {
37 | if (bytes === 0) return '0 Bytes'
38 | const k = 1024
39 | const sizes = ['Bytes', 'KB', 'MB', 'GB']
40 | const i = Math.floor(Math.log(bytes) / Math.log(k))
41 | return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
42 | }
43 |
44 | const getFileTypeIcon = (type: string): string => {
45 | if (type.startsWith('image/')) return '🖼️'
46 | if (type.includes('pdf')) return '📄'
47 | if (type.includes('text')) return '📝'
48 | if (type.includes('word')) return '📘'
49 | if (type.includes('excel') || type.includes('sheet')) return '📊'
50 | if (type.includes('powerpoint') || type.includes('presentation')) return '📽️'
51 | if (type.includes('javascript') || type.includes('typescript')) return '💻'
52 | if (type.includes('html')) return '🌐'
53 | if (type.includes('css')) return '🎨'
54 | return '📎'
55 | }
56 |
57 | if (loading) {
58 | return (
59 |
69 | )
70 | }
71 |
72 | if (error) {
73 | return (
74 |
75 |
76 |
Error
77 |
{error}
78 |
84 |
85 |
86 | )
87 | }
88 |
89 | return (
90 |
91 |
92 |
93 | 📎 Attachment Overview
94 |
95 |
101 |
102 |
103 | {stats && (
104 |
105 | {/* Summary Stats */}
106 |
107 |
108 |
109 | {stats.totalAttachments}
110 |
111 |
Total Files
112 |
113 |
114 |
115 | {formatBytes(stats.totalSize)}
116 |
117 |
Storage Used
118 |
119 |
120 |
121 | {/* File Types Breakdown */}
122 | {Object.keys(stats.types).length > 0 ? (
123 |
124 |
125 | File Types
126 |
127 |
128 | {Object.entries(stats.types)
129 | .sort(([,a], [,b]) => b - a)
130 | .map(([type, count]) => (
131 |
132 |
133 | {getFileTypeIcon(type)}
134 |
135 | {type.split('/')[1]?.toUpperCase() || type}
136 |
137 |
138 |
139 | {count} file{count > 1 ? 's' : ''}
140 |
141 |
142 | ))}
143 |
144 |
145 | ) : (
146 |
147 |
📎
148 |
No attachments yet
149 |
Start uploading files to see statistics
150 |
151 | )}
152 |
153 | {/* Usage Tips */}
154 |
155 |
156 | 💡 Attachment Tips
157 |
158 |
159 | - • Drag and drop files directly into the chat input
160 | - • Maximum file size: 10MB per file
161 | - • Supports images, documents, code files, and more
162 | - • Click attachment previews to download
163 |
164 |
165 |
166 | )}
167 |
168 | )
169 | }
170 |
--------------------------------------------------------------------------------
/src/components/ChatInterface.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import { useChat } from 'ai/react'
5 | import ChatMessage from './ChatMessage'
6 | import MessageInput from './MessageInput'
7 | import { Message } from 'ai'
8 |
9 | interface ChatInterfaceProps {
10 | onNewSession?: (sessionId: string) => void
11 | sessionId?: string
12 | }
13 |
14 | export default function ChatInterface({ onNewSession, sessionId }: ChatInterfaceProps) {
15 | const { messages, input, handleInputChange, handleSubmit, isLoading, setMessages, error } = useChat({
16 | id: sessionId,
17 | api: '/api/chat',
18 | maxSteps: 35, // Allow multiple tool calls
19 | streamProtocol: 'text', // Add for debugging
20 | onError: async (error) => {
21 | console.error(' Full error object:', error);
22 |
23 | // Add check for stream error type that matches the pattern
24 | if (error.message?.includes('Stream error:') ||
25 | (error as any).type === 'error') {
26 | console.error(' Stream contained error detected:', error);
27 | }
28 |
29 | // Keep existing error handling logic
30 | if ((error as any).fullStream) {
31 | try {
32 | const fullStreamText = await (error as any).fullStream.text();
33 | console.error(' Full stream error details:', fullStreamText);
34 |
35 | try {
36 | const errorData = JSON.parse(fullStreamText);
37 | if (errorData.error) {
38 | console.error(' Parsed error:', errorData.error);
39 | }
40 | } catch (parseError) {
41 | // Stream might not be JSON, that's okay
42 | }
43 | } catch (streamError) {
44 | console.error(' Error parsing full stream:', streamError);
45 | }
46 | }
47 |
48 | if ((error as any).cause && typeof (error as any).cause === 'object') {
49 | console.error(' Error cause details:', (error as any).cause);
50 | }
51 | }, onFinish: async (message) => {
52 | // Message storage is now handled by the chat API
53 | // No need to store separately here
54 | }
55 | })
56 |
57 | // Enhanced input change handler to support textarea
58 | const handleInputChangeEnhanced = (e: React.ChangeEvent) => {
59 | const syntheticEvent = {
60 | target: {
61 | value: e.target.value
62 | }
63 | } as React.ChangeEvent
64 | handleInputChange(syntheticEvent)
65 | }
66 |
67 | // Enhanced submit handler with attachment support
68 | const handleChatSubmit = async (e: React.FormEvent, files?: FileList) => {
69 | e.preventDefault()
70 |
71 | if (files && files.length > 0) {
72 | // Validation
73 | const MAX_FILE_SIZE = 50 * 1024 * 1024; // 50MB limit
74 | const MAX_FILES = 20; // Maximum number of files
75 |
76 | if (files.length > MAX_FILES) {
77 | alert(`Maximum ${MAX_FILES} files allowed. You selected ${files.length}.`);
78 | return;
79 | }
80 |
81 | for (let i = 0; i < files.length; i++) {
82 | if (files[i].size > MAX_FILE_SIZE) {
83 | alert(`File "${files[i].name}" exceeds the 50MB size limit (${(files[i].size / 1024 / 1024).toFixed(2)}MB)`);
84 | return;
85 | }
86 | }
87 |
88 | // Use experimental_attachments as per AI SDK v3.3
89 | handleSubmit(e, {
90 | experimental_attachments: files
91 | });
92 | } else {
93 | // Regular submit without attachments
94 | handleSubmit(e)
95 | }
96 | }
97 |
98 | React.useEffect(() => {
99 | if (!sessionId && onNewSession) {
100 | const newSessionId = `session_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`
101 | onNewSession(newSessionId)
102 | }
103 | }, [sessionId, onNewSession])
104 |
105 | return (
106 |
107 | {/* Header */}
108 |
109 |
110 | MCP Chat Client
111 |
112 |
113 | AI Assistant with Dual-Layer Memory & File Support
114 |
115 |
116 |
117 | {/* Messages */}
118 |
119 | {messages.length === 0 ? (
120 |
121 |
122 |
Welcome to MCP Chat
123 |
Your AI assistant with conscious memory
124 |
125 |
Dual-layer memory system
126 |
MCP tool integration
127 |
File attachment support
128 |
Persistent conversation history
129 |
130 |
131 | ) : (
132 | messages.map((message: Message) => (
133 |
137 | ))
138 | )}
139 | {isLoading && (
140 |
141 |
142 |
143 | AI is thinking...
144 |
145 |
146 | )}
147 | {error && (
148 |
149 |
150 |
151 |
152 |
Chat Error
153 |
{error.message}
154 |
155 |
156 | Show technical details
157 |
158 |
159 | {error.stack || JSON.stringify(error, null, 2)}
160 |
161 |
162 |
163 |
164 |
165 | )}
166 |
167 |
168 | {/* Enhanced Input with Attachment Support */}
169 |
175 |
176 | )
177 | }
--------------------------------------------------------------------------------
/src/components/ChatMessage.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Message } from 'ai'
3 | import ToolCallDisplay from './ToolCallDisplay'
4 |
5 | interface ChatMessageProps {
6 | message: Message
7 | }
8 |
9 | const AttachmentPreview: React.FC<{ attachment: any }> = ({ attachment }) => {
10 | // AI SDK FilePart can have 'data' (base64) or 'url'
11 | const isImage = attachment.type.startsWith('image/')
12 | const displaySrc = attachment.data ? `data:${attachment.type};base64,${attachment.data}` : attachment.url
13 |
14 | const handleDownload = () => {
15 | if (attachment.data) {
16 | const blob = new Blob([Uint8Array.from(atob(attachment.data), c => c.charCodeAt(0))], {
17 | type: attachment.type
18 | })
19 | const url = URL.createObjectURL(blob)
20 | const a = document.createElement('a')
21 | a.href = url
22 | a.download = attachment.name || 'download'
23 | document.body.appendChild(a)
24 | a.click()
25 | document.body.removeChild(a)
26 | URL.revokeObjectURL(url)
27 | } else if (attachment.url) {
28 | // For URLs, simply open in new tab or trigger download
29 | window.open(attachment.url, '_blank')
30 | }
31 | }
32 |
33 | if (isImage && displaySrc) {
34 | return (
35 |
36 |

43 |
44 | {attachment.name || 'Image'}
45 | {attachment.size && {(attachment.size / 1024).toFixed(1)}KB}
46 |
47 |
48 | )
49 | }
50 |
51 | // Generic file preview for non-images or if image source is missing
52 | return (
53 |
54 |
55 |
56 | {attachment.type?.includes('pdf') ? '📄' :
57 | attachment.type?.includes('text') ? '📝' :
58 | attachment.type?.includes('json') ? '🔧' :
59 | attachment.type?.includes('word') ? '📘' :
60 | attachment.type?.includes('excel') || attachment.type?.includes('sheet') ? '📊' :
61 | attachment.type?.includes('powerpoint') || attachment.type?.includes('presentation') ? '📽️' :
62 | attachment.type?.includes('javascript') || attachment.type?.includes('typescript') ? '💻' :
63 | attachment.type?.includes('html') ? '🌐' :
64 | attachment.type?.includes('css') ? '🎨' : '📎'}
65 |
66 |
67 |
68 |
69 | {attachment.name || 'Unknown File'}
70 |
71 |
72 | {attachment.type || 'application/octet-stream'} • {attachment.size ? `${(attachment.size / 1024).toFixed(1)}KB` : 'N/A'}
73 |
74 |
75 |
76 |
83 |
84 |
85 | )
86 | }
87 |
88 | export default function ChatMessage({ message }: ChatMessageProps) {
89 | const isUser = message.role === 'user'
90 |
91 | return (
92 |
93 |
98 | {/* Message Content and Parts */}
99 | {message.parts && message.parts.length > 0 && (
100 |
101 | {message.parts.map((part: any, index: number) => {
102 | switch (part.type) {
103 | case 'text':
104 | return
{part.text}
105 | case 'file':
106 | return
107 | // Add more cases for other part types (e.g., 'tool-call', 'tool-result', 'reasoning') if needed
108 | default:
109 | return null
110 | }
111 | })}
112 |
113 | )}
114 |
115 | {/* Tool Invocations */}
116 | {message.toolInvocations && message.toolInvocations.length > 0 && (
117 |
118 | {message.toolInvocations.map((toolInvocation, index) => (
119 |
123 | ))}
124 |
125 | )}
126 |
127 |
128 | )
129 | }
--------------------------------------------------------------------------------
/src/components/MessageInput.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react'
2 |
3 | interface MessageInputProps {
4 | input: string
5 | handleInputChange: (e: React.ChangeEvent) => void
6 | handleSubmit: (e: React.FormEvent, files?: FileList) => void
7 | isLoading: boolean
8 | }
9 |
10 | export default function MessageInput({
11 | input,
12 | handleInputChange,
13 | handleSubmit,
14 | isLoading
15 | }: MessageInputProps) {
16 | const [files, setFiles] = useState(undefined)
17 | const [isDragOver, setIsDragOver] = useState(false)
18 | const fileInputRef = useRef(null)
19 |
20 | const handleDragOver = (e: React.DragEvent) => {
21 | e.preventDefault()
22 | setIsDragOver(true)
23 | }
24 |
25 | const handleDragLeave = (e: React.DragEvent) => {
26 | e.preventDefault()
27 | setIsDragOver(false)
28 | }
29 |
30 | const handleDrop = (e: React.DragEvent) => {
31 | e.preventDefault()
32 | setIsDragOver(false)
33 |
34 | const droppedFiles = e.dataTransfer.files
35 | if (droppedFiles.length > 0) {
36 | setFiles(droppedFiles)
37 | }
38 | }
39 |
40 | const removeFile = (index: number) => {
41 | if (!files) return
42 | const dt = new DataTransfer()
43 | Array.from(files).forEach((file, i) => {
44 | if (i !== index) dt.items.add(file)
45 | })
46 | setFiles(dt.files.length > 0 ? dt.files : undefined)
47 | }
48 |
49 | const handleFormSubmit = (e: React.FormEvent) => {
50 | e.preventDefault()
51 | console.log('MessageInput: handleFormSubmit called with files:', files)
52 | if (files) {
53 | console.log('MessageInput: Files count:', files.length)
54 | for (let i = 0; i < files.length; i++) {
55 | console.log(`MessageInput: File ${i + 1}:`, files[i].name, files[i].type, files[i].size)
56 | }
57 | }
58 | handleSubmit(e, files)
59 | setFiles(undefined)
60 | if (fileInputRef.current) {
61 | fileInputRef.current.value = ''
62 | }
63 | }
64 |
65 | const handleKeyDown = (e: React.KeyboardEvent) => {
66 | if (e.key === 'Enter' && !e.shiftKey) {
67 | e.preventDefault()
68 | handleFormSubmit(e)
69 | }
70 | }
71 | return (
72 |
73 | {/* Files Preview */}
74 | {files && files.length > 0 && (
75 |
76 | {Array.from(files).map((file, index) => (
77 |
81 | {/* File icon based on type */}
82 |
83 | {file.type.startsWith('image/') ? '🖼️' :
84 | file.type.includes('pdf') ? '📄' :
85 | file.type.includes('text') ? '📝' :
86 | file.type.includes('code') ? '💻' : '📎'}
87 |
88 |
89 |
90 |
91 | {file.name}
92 |
93 |
94 | {(file.size / 1024).toFixed(1)}KB
95 |
96 |
97 |
98 |
106 |
107 | ))}
108 |
109 | )}
110 |
111 |
206 |
207 | )
208 | }
--------------------------------------------------------------------------------
/src/components/MotiveForceSettings.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { MotiveForceConfig } from '../types/motive-force';
3 |
4 | interface MotiveForceSettingsProps {
5 | isOpen: boolean;
6 | onClose: () => void;
7 | onSave: (config: MotiveForceConfig) => void;
8 | initialConfig: MotiveForceConfig;
9 | }
10 |
11 | export default function MotiveForceSettings({
12 | isOpen,
13 | onClose,
14 | onSave,
15 | initialConfig
16 | }: MotiveForceSettingsProps) {
17 | const [config, setConfig] = useState(initialConfig);
18 |
19 | if (!isOpen) return null;
20 |
21 | const handleSave = () => {
22 | onSave(config);
23 | onClose();
24 | };
25 |
26 | return (
27 |
28 |
29 |
Autopilot Settings
30 |
31 |
131 |
132 |
133 |
139 |
145 |
146 |
147 |
148 | );
149 | }
150 |
--------------------------------------------------------------------------------
/src/components/MotiveForceStatus.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MotiveForceState } from '../types/motive-force';
3 |
4 | interface MotiveForceStatusProps {
5 | state: MotiveForceState;
6 | onStop?: () => void;
7 | onReset?: () => void;
8 | }
9 |
10 | export default function MotiveForceStatus({
11 | state,
12 | onStop,
13 | onReset
14 | }: MotiveForceStatusProps) {
15 | if (!state.enabled && !state.isGenerating) return null;
16 |
17 | return (
18 |
23 |
24 |
33 |
34 |
35 |
36 | {state.isGenerating
37 | ? 'Generating next query...'
38 | : state.enabled
39 | ? `Autopilot active (Turn ${state.currentTurn})`
40 | : 'Autopilot ready'
41 | }
42 |
43 |
44 | {state.enabled && (
45 |
46 | {state.isGenerating
47 | ? 'Analyzing conversation context'
48 | : state.errorCount > 0
49 | ? `${state.errorCount} errors encountered`
50 | : 'Waiting for response completion'
51 | }
52 |
53 | )}
54 |
55 |
56 |
57 | {state.enabled && onStop && (
58 |
64 | )}
65 |
66 | {onReset && (
67 |
73 | )}
74 |
75 |
76 |
77 | );
78 | }
79 |
--------------------------------------------------------------------------------
/src/components/MotiveForceToggle.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface MotiveForceToggleProps {
4 | enabled: boolean;
5 | onToggle: (enabled: boolean) => void;
6 | size?: 'sm' | 'md' | 'lg';
7 | showLabel?: boolean;
8 | }
9 |
10 | export default function MotiveForceToggle({
11 | enabled,
12 | onToggle,
13 | size = 'md',
14 | showLabel = true
15 | }: MotiveForceToggleProps) {
16 | const sizeClasses = {
17 | sm: 'px-2 py-1 text-xs',
18 | md: 'px-3 py-1.5 text-sm',
19 | lg: 'px-4 py-2 text-base'
20 | };
21 |
22 | return (
23 |
54 | );
55 | }
56 |
--------------------------------------------------------------------------------
/src/components/ToolCallDisplay.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { ToolInvocation } from 'ai'
3 |
4 | interface ToolCallDisplayProps {
5 | toolInvocation: ToolInvocation
6 | }
7 |
8 | export default function ToolCallDisplay({ toolInvocation }: ToolCallDisplayProps) {
9 | // Check if the result explicitly indicates an error
10 | const isErrorResult = toolInvocation.state === 'result' &&
11 | toolInvocation.result &&
12 | typeof toolInvocation.result === 'object' &&
13 | ((toolInvocation.result as any).isError === true || (toolInvocation.result as any).error === true);
14 |
15 | // Determine which content to display for the result
16 | let resultDisplayContent: React.ReactNode = No data available.
;
17 | if (isErrorResult) {
18 | const errorMessage = (toolInvocation.result as any).message || 'Unknown tool error.';
19 | const errorDetails = (toolInvocation.result as any).details || (toolInvocation.result as any).errorDetails || (toolInvocation.result as any).error; // Catch various detail fields
20 | resultDisplayContent = (
21 |
22 |
Error: {errorMessage}
23 | {errorDetails && (
24 |
25 | {typeof errorDetails === 'string' ? errorDetails : JSON.stringify(errorDetails, null, 2)}
26 |
27 | )}
28 |
29 | );
30 | } else if ('result' in toolInvocation && toolInvocation.result) {
31 | resultDisplayContent = (
32 |
33 |
Result:
34 |
35 | {typeof toolInvocation.result === 'string'
36 | ? toolInvocation.result
37 | : JSON.stringify(toolInvocation.result, null, 2)
38 | }
39 |
40 |
41 | );
42 | } else if (toolInvocation.state === 'call') {
43 | resultDisplayContent = (
44 |
45 |
46 | Executing...
47 |
48 | );
49 | }
50 |
51 | return (
52 |
56 |
57 | 🔧 Tool: {toolInvocation.toolName}
58 |
59 |
60 | {toolInvocation.args && (
61 |
62 |
Arguments:
63 |
64 | {JSON.stringify(toolInvocation.args, null, 2)}
65 |
66 |
67 | )}
68 |
69 | {resultDisplayContent}
70 |
71 |
72 | )
73 | }
74 |
--------------------------------------------------------------------------------
/src/config/default-mcp-servers.ts:
--------------------------------------------------------------------------------
1 | import { MCPServerConfig } from '../types/mcp';
2 | import { readFileSync } from 'fs';
3 | import { join } from 'path';
4 |
5 | export interface MCPConfig {
6 | mcp: {
7 | servers: {
8 | [serverName: string]: {
9 | command: string;
10 | args: string[];
11 | env?: { [key: string]: string };
12 | };
13 | };
14 | };
15 | }
16 |
17 | // Load configuration from config.json
18 | function loadMCPConfig(): MCPConfig {
19 | try {
20 | const configPath = join(process.cwd(), 'config.json');
21 | const configFile = readFileSync(configPath, 'utf8');
22 | const config = JSON.parse(configFile);
23 | return config as MCPConfig;
24 | } catch (error) {
25 | console.error('Failed to load config.json, using fallback configuration:', error);
26 | // Fallback configuration if config.json is not available
27 | return {
28 | mcp: {
29 | servers: {
30 | 'conscious-memory': {
31 | command: 'npx',
32 | args: ['tsx', './src/lib/mcp-servers/conscious-memory-server.ts']
33 | },
34 | 'knowledge-graph': {
35 | command: 'npx',
36 | args: ['tsx', './src/lib/mcp-servers/knowledge-graph-server.ts']
37 | }
38 | }
39 | }
40 | };
41 | }
42 | }
43 |
44 | export const mcpConfig: MCPConfig = loadMCPConfig();
45 |
46 | export function getMCPServerConfig(serverName: string): MCPServerConfig | undefined {
47 | const serverConfig = mcpConfig.mcp.servers[serverName];
48 | if (!serverConfig) return undefined;
49 |
50 | return {
51 | name: serverName,
52 | type: 'stdio',
53 | command: serverConfig.command,
54 | args: serverConfig.args
55 | };
56 | }
57 |
58 | export function getAllMCPServers(): MCPServerConfig[] {
59 | return Object.keys(mcpConfig.mcp.servers).map(serverName => ({
60 | name: serverName,
61 | type: 'stdio',
62 | command: mcpConfig.mcp.servers[serverName].command,
63 | args: mcpConfig.mcp.servers[serverName].args,
64 | env: mcpConfig.mcp.servers[serverName].env || {}
65 | }));
66 | }
--------------------------------------------------------------------------------
/src/lib/api-config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * API configuration with timeout support
3 | */
4 |
5 | interface ApiConfig {
6 | timeout?: number;
7 | retryOptions?: {
8 | maxAttempts?: number;
9 | initialDelay?: number;
10 | };
11 | }
12 |
13 | const DEFAULT_TIMEOUT = 30000; // 30 seconds
14 | const DEFAULT_RETRY_ATTEMPTS = 3;
15 |
16 | export function getApiConfig(): ApiConfig {
17 | return {
18 | timeout: parseInt(process.env.API_TIMEOUT || String(DEFAULT_TIMEOUT)),
19 | retryOptions: {
20 | maxAttempts: parseInt(process.env.API_RETRY_ATTEMPTS || String(DEFAULT_RETRY_ATTEMPTS)),
21 | initialDelay: parseInt(process.env.API_RETRY_DELAY || '1000'),
22 | },
23 | };
24 | }
25 |
26 | /**
27 | * Create an AbortController with timeout
28 | */
29 | export function createTimeoutController(timeoutMs: number = DEFAULT_TIMEOUT): AbortController {
30 | const controller = new AbortController();
31 |
32 | const timeoutId = setTimeout(() => {
33 | controller.abort(new Error(`Request timeout after ${timeoutMs}ms`));
34 | }, timeoutMs);
35 |
36 | // Clean up the timeout when the request completes
37 | const originalAbort = controller.abort.bind(controller);
38 | controller.abort = (reason?: any) => {
39 | clearTimeout(timeoutId);
40 | originalAbort(reason);
41 | };
42 |
43 | return controller;
44 | }
45 |
46 | /**
47 | * Fetch with timeout support
48 | */
49 | export async function fetchWithTimeout(
50 | url: string,
51 | options: RequestInit & { timeout?: number } = {}
52 | ): Promise {
53 | const { timeout = DEFAULT_TIMEOUT, ...fetchOptions } = options;
54 | const controller = createTimeoutController(timeout);
55 |
56 | try {
57 | return await fetch(url, {
58 | ...fetchOptions,
59 | signal: controller.signal,
60 | });
61 | } finally {
62 | controller.abort(); // Clean up
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/lib/embeddings.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Embedding service for generating vector embeddings using Google's text-embedding-004
3 | * Adapted from Skynet Agent for the lean MCP chat client
4 | */
5 |
6 | import { embed } from 'ai';
7 | import { createGoogleGenerativeAI } from '@ai-sdk/google';
8 | import type { EmbeddingService } from '../types/memory';
9 |
10 | type GoogleGenerativeAI = ReturnType;
11 |
12 | export class GoogleEmbeddingService implements EmbeddingService {
13 | private googleAI: GoogleGenerativeAI | null = null;
14 | private isInitialized = false;
15 | private readonly dimensions = 768; // text-embedding-004 dimensions
16 |
17 | constructor() {
18 | let apiKey = process.env.GOOGLE_API_KEY;
19 | console.log(`Using GOOGLE_API_KEY: ${!!apiKey}`);
20 | if (!apiKey) {
21 | console.warn('GOOGLE_API_KEY not configured, using fallback embeddings');
22 | apiKey = process.env.GOOGLE_API_KEY;
23 | console.log(`Using GOOGLE_API_KEY: ${!!apiKey}`);
24 | } else {
25 | this.initializeGoogleAI(apiKey);
26 | }
27 | }
28 |
29 | private initializeGoogleAI(apiKey: string) {
30 | try {
31 | this.googleAI = createGoogleGenerativeAI({ apiKey });
32 | this.isInitialized = true;
33 | console.log('Google Embedding Service initialized with text-embedding-004');
34 | } catch (error) {
35 | console.error('Failed to initialize Google AI SDK for embeddings:', error);
36 | }
37 | }
38 |
39 | /**
40 | * Generate embedding vector for text using Google's text-embedding-004 or fallback
41 | */
42 | async generateEmbedding(text: string): Promise {
43 | const startTime = Date.now();
44 |
45 | try {
46 | // Use Google API if available
47 | if (this.googleAI && this.isInitialized) {
48 | const { embedding } = await embed({
49 | model: this.googleAI.textEmbeddingModel('text-embedding-004'),
50 | value: text,
51 | });
52 |
53 | const duration = Date.now() - startTime;
54 | console.log(`Real embedding generated in ${duration}ms (${embedding.length} dimensions)`);
55 |
56 | return embedding;
57 | }
58 |
59 | // Fallback to hash-based embedding
60 | return this.generateHashBasedEmbedding(text);
61 | } catch (error) {
62 | console.error('Failed to generate embedding, using fallback:', error);
63 | return this.generateHashBasedEmbedding(text);
64 | }
65 | }
66 |
67 | /**
68 | * Deterministic fallback embedding method
69 | * Creates a hash-based embedding that preserves some semantic meaning
70 | */
71 | private generateHashBasedEmbedding(text: string): number[] {
72 | const normalized = text.toLowerCase().trim();
73 | const words = normalized.split(/\s+/);
74 | const embedding = new Array(this.dimensions).fill(0);
75 |
76 | // Create deterministic embedding based on text content
77 | for (let i = 0; i < words.length; i++) {
78 | const word = words[i];
79 | for (let j = 0; j < word.length; j++) {
80 | const charCode = word.charCodeAt(j);
81 | const index = (charCode + i + j) % embedding.length;
82 | embedding[index] += (charCode / 255.0 - 0.5) * (1.0 / Math.sqrt(words.length + 1));
83 | }
84 | }
85 |
86 | // Normalize the vector
87 | const magnitude = Math.sqrt(embedding.reduce((sum, val) => sum + val * val, 0));
88 | if (magnitude > 0) {
89 | for (let i = 0; i < embedding.length; i++) {
90 | embedding[i] /= magnitude;
91 | }
92 | }
93 |
94 | console.log(`Fallback embedding generated (${embedding.length} dimensions)`);
95 | return embedding;
96 | }
97 |
98 | /**
99 | * Get the dimension count of embeddings
100 | */
101 | getDimensions(): number {
102 | return this.dimensions;
103 | }
104 |
105 | /**
106 | * Check if the service is ready for real embeddings
107 | */
108 | isReady(): boolean {
109 | return this.isInitialized && this.googleAI !== null;
110 | }
111 |
112 | /**
113 | * Calculate cosine similarity between two embeddings
114 | */
115 | static cosineSimilarity(a: number[], b: number[]): number {
116 | if (a.length !== b.length) {
117 | throw new Error(`Vector dimensions don't match: ${a.length} vs ${b.length}`);
118 | }
119 |
120 | let dotProduct = 0;
121 | let normA = 0;
122 | let normB = 0;
123 |
124 | for (let i = 0; i < a.length; i++) {
125 | dotProduct += a[i] * b[i];
126 | normA += a[i] * a[i];
127 | normB += b[i] * b[i];
128 | }
129 |
130 | if (normA === 0 || normB === 0) {
131 | return 0;
132 | }
133 |
134 | return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB));
135 | }
136 | }
137 |
138 | // Export singleton instance
139 | let embeddingService: GoogleEmbeddingService | null = null;
140 |
141 | export function getEmbeddingService(): GoogleEmbeddingService {
142 | if (!embeddingService) {
143 | embeddingService = new GoogleEmbeddingService();
144 | }
145 | return embeddingService;
146 | }
147 |
--------------------------------------------------------------------------------
/src/lib/errors.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Custom error types for Skynet Agent
3 | */
4 |
5 | export enum ErrorCode {
6 | // Tool errors
7 | TOOL_NOT_FOUND = 'TOOL_NOT_FOUND',
8 | TOOL_EXECUTION_FAILED = 'TOOL_EXECUTION_FAILED',
9 | TOOL_TIMEOUT = 'TOOL_TIMEOUT',
10 | TOOL_INVALID_ARGS = 'TOOL_INVALID_ARGS',
11 |
12 | // Memory errors
13 | MEMORY_STORAGE_FAILED = 'MEMORY_STORAGE_FAILED',
14 | MEMORY_RETRIEVAL_FAILED = 'MEMORY_RETRIEVAL_FAILED',
15 |
16 | // Attachment errors
17 | ATTACHMENT_TOO_LARGE = 'ATTACHMENT_TOO_LARGE',
18 | ATTACHMENT_INVALID_TYPE = 'ATTACHMENT_INVALID_TYPE',
19 | ATTACHMENT_PROCESSING_FAILED = 'ATTACHMENT_PROCESSING_FAILED',
20 |
21 | // Stream errors
22 | STREAM_ERROR = 'STREAM_ERROR',
23 | STREAM_TIMEOUT = 'STREAM_TIMEOUT',
24 |
25 | // API errors
26 | API_ERROR = 'API_ERROR',
27 | INVALID_REQUEST = 'INVALID_REQUEST',
28 | PROVIDER_ERROR = 'PROVIDER_ERROR',
29 | }
30 |
31 | export class SkynetError extends Error {
32 | code: ErrorCode;
33 | details?: any;
34 | retryable: boolean;
35 |
36 | constructor(message: string, code: ErrorCode, details?: any, retryable = false) {
37 | super(message);
38 | this.name = 'SkynetError';
39 | this.code = code;
40 | this.details = details;
41 | this.retryable = retryable;
42 | }
43 | }
44 |
45 | export class ToolError extends SkynetError {
46 | toolName: string;
47 | serverName: string;
48 |
49 | constructor(
50 | message: string,
51 | toolName: string,
52 | serverName: string,
53 | code: ErrorCode = ErrorCode.TOOL_EXECUTION_FAILED,
54 | details?: any
55 | ) {
56 | super(message, code, details, true);
57 | this.name = 'ToolError';
58 | this.toolName = toolName;
59 | this.serverName = serverName;
60 | }
61 | }
62 |
63 | export class AttachmentError extends SkynetError {
64 | fileName?: string;
65 | fileSize?: number;
66 |
67 | constructor(
68 | message: string,
69 | code: ErrorCode = ErrorCode.ATTACHMENT_PROCESSING_FAILED,
70 | fileName?: string,
71 | fileSize?: number
72 | ) {
73 | super(message, code, { fileName, fileSize }, false);
74 | this.name = 'AttachmentError';
75 | this.fileName = fileName;
76 | this.fileSize = fileSize;
77 | }
78 | }
79 |
80 | export class StreamError extends SkynetError {
81 | streamDetails?: any;
82 |
83 | constructor(message: string, streamDetails?: any) {
84 | super(message, ErrorCode.STREAM_ERROR, streamDetails, true);
85 | this.name = 'StreamError';
86 | this.streamDetails = streamDetails;
87 | }
88 | }
89 |
90 | // Error utility functions
91 | export function isRetryableError(error: unknown): boolean {
92 | return error instanceof SkynetError && error.retryable;
93 | }
94 |
95 | export function getErrorMessage(error: unknown): string {
96 | if (error instanceof Error) {
97 | return error.message;
98 | }
99 | return String(error);
100 | }
101 |
102 | export function getErrorDetails(error: unknown): any {
103 | if (error instanceof SkynetError) {
104 | return error.details;
105 | }
106 | return null;
107 | }
108 |
--------------------------------------------------------------------------------
/src/lib/kg-resilience.ts:
--------------------------------------------------------------------------------
1 | export interface RetryOptions {
2 | maxRetries: number;
3 | backoffMs: number;
4 | onRetry?: (error: Error, attempt: number) => void;
5 | }
6 |
7 | export async function withRetry(
8 | operation: () => Promise,
9 | options: RetryOptions
10 | ): Promise {
11 | let lastError: Error;
12 |
13 | for (let attempt = 1; attempt <= options.maxRetries; attempt++) {
14 | try {
15 | return await operation();
16 | } catch (error) {
17 | lastError = error as Error;
18 |
19 | if (attempt < options.maxRetries) {
20 | options.onRetry?.(lastError, attempt);
21 | await new Promise(resolve =>
22 | setTimeout(resolve, options.backoffMs * attempt)
23 | );
24 | }
25 | }
26 | }
27 |
28 | throw lastError!;
29 | }
30 |
31 | export interface SyncQueueItem {
32 | id: string;
33 | operation: 'save' | 'update' | 'delete';
34 | content?: string;
35 | metadata?: any;
36 | retryCount: number;
37 | lastError?: string;
38 | timestamp: number;
39 | }
40 |
41 | export class SyncErrorQueue {
42 | private queue: SyncQueueItem[] = [];
43 | private processing = false;
44 |
45 | push(item: SyncQueueItem) {
46 | this.queue.push(item);
47 | console.log(`[SyncErrorQueue] Added item to retry queue. Queue size: ${this.queue.length}`);
48 | }
49 |
50 | async processQueue(syncFunction: (item: SyncQueueItem) => Promise) {
51 | if (this.processing || this.queue.length === 0) return;
52 |
53 | this.processing = true;
54 | console.log(`[SyncErrorQueue] Processing ${this.queue.length} items in retry queue`);
55 |
56 | const itemsToProcess = [...this.queue];
57 | this.queue = [];
58 |
59 | for (const item of itemsToProcess) {
60 | try {
61 | await withRetry(
62 | () => syncFunction(item),
63 | {
64 | maxRetries: 3,
65 | backoffMs: 1000,
66 | onRetry: (error, attempt) => {
67 | console.log(`[SyncErrorQueue] Retry attempt ${attempt} for item ${item.id}:`, error.message);
68 | item.retryCount++;
69 | item.lastError = error.message;
70 | }
71 | }
72 | );
73 | console.log(`[SyncErrorQueue] Successfully processed item ${item.id}`);
74 | } catch (error) {
75 | console.error(`[SyncErrorQueue] Failed to process item ${item.id} after retries:`, error);
76 | // Put back in queue if not exceeded max retry count
77 | if (item.retryCount < 10) {
78 | this.queue.push(item);
79 | } else {
80 | console.error(`[SyncErrorQueue] Dropping item ${item.id} after ${item.retryCount} total attempts`);
81 | }
82 | }
83 | }
84 |
85 | this.processing = false;
86 | }
87 |
88 | getQueueSize(): number {
89 | return this.queue.length;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/src/lib/kg-sync-metrics.ts:
--------------------------------------------------------------------------------
1 | interface SyncMetrics {
2 | startTime: Date;
3 | endTime?: Date;
4 | entitiesProcessed: number;
5 | relationshipsProcessed: number;
6 | errors: Array<{ message: string; timestamp: Date }>;
7 | status: 'running' | 'completed' | 'failed';
8 | }
9 |
10 | export class SyncMetricsCollector {
11 | private metrics: SyncMetrics;
12 |
13 | constructor() {
14 | this.metrics = {
15 | startTime: new Date(),
16 | entitiesProcessed: 0,
17 | relationshipsProcessed: 0,
18 | errors: [],
19 | status: 'running'
20 | };
21 | }
22 |
23 | recordEntity() {
24 | this.metrics.entitiesProcessed++;
25 | }
26 |
27 | recordRelationship() {
28 | this.metrics.relationshipsProcessed++;
29 | }
30 |
31 | recordError(error: Error) {
32 | this.metrics.errors.push({
33 | message: error.message,
34 | timestamp: new Date()
35 | });
36 | }
37 |
38 | complete(status: 'completed' | 'failed') {
39 | this.metrics.status = status;
40 | this.metrics.endTime = new Date();
41 | return this.metrics;
42 | }
43 |
44 | getMetrics() {
45 | return this.metrics;
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/src/lib/kg-sync-queue.ts:
--------------------------------------------------------------------------------
1 | import fs from 'fs/promises';
2 | import path from 'path';
3 |
4 | export interface SyncRequest {
5 | id: string;
6 | timestamp: string;
7 | type: 'chat' | 'memory' | 'full';
8 | priority: number;
9 | }
10 |
11 | export class KnowledgeGraphSyncQueue {
12 | private queuePath: string;
13 | private processing = false;
14 |
15 | constructor() {
16 | this.queuePath = path.join(process.cwd(), 'data', 'kg-sync-queue.json');
17 | }
18 |
19 | async initialize(): Promise {
20 | try {
21 | // Ensure directory exists
22 | await fs.mkdir(path.dirname(this.queuePath), { recursive: true });
23 |
24 | // Create queue file if it doesn't exist
25 | try {
26 | await fs.access(this.queuePath);
27 | } catch {
28 | await fs.writeFile(this.queuePath, JSON.stringify({ requests: [] }));
29 | }
30 | } catch (error) {
31 | console.error('[KG Sync Queue] Failed to initialize queue:', error);
32 | }
33 | }
34 |
35 | async addSyncRequest(type: 'chat' | 'memory' | 'full', priority: number = 1): Promise {
36 | await this.initialize();
37 |
38 | try {
39 | // Read current queue
40 | let queueData = await fs.readFile(this.queuePath, 'utf-8');
41 | // Strip BOM if present
42 | queueData = queueData.replace(/^\uFEFF/, '');
43 | const queue = JSON.parse(queueData);
44 |
45 | // Add new request
46 | const request: SyncRequest = {
47 | id: `sync_${Date.now()}_${Math.random().toString(36).substring(2, 10)}`,
48 | timestamp: new Date().toISOString(),
49 | type,
50 | priority
51 | };
52 |
53 | queue.requests.push(request);
54 |
55 | // Write updated queue
56 | await fs.writeFile(this.queuePath, JSON.stringify(queue, null, 2));
57 |
58 | console.log(`[KG Sync Queue] Added ${type} sync request to queue (ID: ${request.id})`);
59 | } catch (error) {
60 | console.error('[KG Sync Queue] Failed to add sync request:', error);
61 | }
62 | }
63 |
64 | async getQueueSize(): Promise {
65 | await this.initialize();
66 |
67 | try {
68 | let queueData = await fs.readFile(this.queuePath, 'utf-8');
69 | // Strip BOM if present
70 | queueData = queueData.replace(/^\uFEFF/, '');
71 | const queue = JSON.parse(queueData);
72 | return queue.requests?.length || 0;
73 | } catch (error) {
74 | console.error('[KG Sync Queue] Failed to get queue size:', error);
75 | return 0;
76 | }
77 | }
78 |
79 | async processNext(processor: (request: SyncRequest) => Promise): Promise {
80 | if (this.processing) {
81 | console.log('[KG Sync Queue] Already processing queue');
82 | return false;
83 | }
84 |
85 | this.processing = true;
86 |
87 | try {
88 | // Read queue
89 | let queueData = await fs.readFile(this.queuePath, 'utf-8');
90 | // Strip BOM if present
91 | queueData = queueData.replace(/^\uFEFF/, '');
92 | const queue = JSON.parse(queueData);
93 |
94 | if (!queue.requests || queue.requests.length === 0) {
95 | console.log('[KG Sync Queue] Queue is empty');
96 | this.processing = false;
97 | return false;
98 | }
99 |
100 | // Sort by priority and take the highest
101 | queue.requests.sort((a: SyncRequest, b: SyncRequest) => b.priority - a.priority);
102 | const request = queue.requests.shift();
103 |
104 | // Update queue file before processing
105 | await fs.writeFile(this.queuePath, JSON.stringify(queue, null, 2));
106 | console.log(`[KG Sync Queue] Processing request: ${request.id} (${request.type})`);
107 |
108 | // Process the request with a timeout to prevent hanging
109 | try {
110 | // Add a timeout to prevent hanging indefinitely
111 | const timeoutPromise = new Promise((_, reject) => {
112 | setTimeout(() => reject(new Error('Processing timeout exceeded (30s)')), 30000);
113 | });
114 |
115 | // Race the processor against the timeout
116 | await Promise.race([
117 | processor(request),
118 | timeoutPromise
119 | ]);
120 | } catch (processorError) {
121 | console.error(`[KG Sync Queue] Error processing request ${request.id}:`, processorError);
122 | // Don't rethrow - we want to continue processing the queue
123 | }
124 |
125 | console.log(`[KG Sync Queue] Successfully processed request: ${request.id}`);
126 | this.processing = false;
127 | return true;
128 | } catch (error) {
129 | console.error('[KG Sync Queue] Processing error:', error);
130 | this.processing = false;
131 | return false;
132 | }
133 | }
134 |
135 | async processAll(processor: (request: SyncRequest) => Promise): Promise {
136 | if (this.processing) {
137 | console.log('[KG Sync Queue] Already processing queue');
138 | return 0;
139 | }
140 |
141 | this.processing = true;
142 |
143 | try {
144 | // Read queue
145 | let queueData = await fs.readFile(this.queuePath, 'utf-8');
146 | // Strip BOM if present
147 | queueData = queueData.replace(/^\uFEFF/, '');
148 | const queue = JSON.parse(queueData);
149 |
150 | if (!queue.requests || queue.requests.length === 0) {
151 | console.log('[KG Sync Queue] Queue is empty');
152 | this.processing = false;
153 | return 0;
154 | }
155 |
156 | // Get all requests and clear the queue
157 | const requests = queue.requests;
158 | queue.requests = [];
159 |
160 | // Update queue file before processing
161 | await fs.writeFile(this.queuePath, JSON.stringify(queue, null, 2));
162 |
163 | console.log(`[KG Sync Queue] Processing ${requests.length} requests`);
164 |
165 | // Sort by priority
166 | requests.sort((a: SyncRequest, b: SyncRequest) => b.priority - a.priority);
167 |
168 | // Process all requests
169 | let processedCount = 0;
170 | for (const request of requests) {
171 | try {
172 | await processor(request);
173 | processedCount++;
174 | } catch (error) {
175 | console.error(`[KG Sync Queue] Error processing request ${request.id}:`, error);
176 |
177 | // Add back to queue with reduced priority
178 | await this.addSyncRequest(request.type, Math.max(0, request.priority - 1));
179 | }
180 | }
181 |
182 | console.log(`[KG Sync Queue] Finished processing ${processedCount} requests`);
183 | this.processing = false;
184 | return processedCount;
185 | } catch (error) {
186 | console.error('[KG Sync Queue] Processing error:', error);
187 | this.processing = false;
188 | return 0;
189 | }
190 | }
191 |
192 | async enqueue(request: { type: 'full' | 'chat' | 'memory' | 'incremental', timestamp: string }): Promise {
193 | // Map 'incremental' type to 'chat' since that's not a valid type for addSyncRequest
194 | const type = request.type === 'incremental' ? 'chat' : request.type as 'full' | 'chat' | 'memory';
195 |
196 | // Default priority: full=2, others=1
197 | const priority = type === 'full' ? 2 : 1;
198 |
199 | return this.addSyncRequest(type, priority);
200 | }
201 |
202 | /**
203 | * Clear all items from the queue
204 | */
205 | async clear(): Promise {
206 | await this.initialize();
207 |
208 | try {
209 | // Write empty queue to the file
210 | await fs.writeFile(this.queuePath, JSON.stringify({ requests: [] }, null, 2));
211 | console.log('[KG Sync Queue] Queue cleared successfully');
212 | } catch (error) {
213 | console.error('[KG Sync Queue] Failed to clear queue:', error);
214 | throw error;
215 | }
216 | }
217 | }
218 |
219 | // Export singleton instance
220 | export const kgSyncQueue = new KnowledgeGraphSyncQueue();
221 |
--------------------------------------------------------------------------------
/src/lib/kg-sync-state.ts:
--------------------------------------------------------------------------------
1 | import { promises as fs } from 'fs';
2 | import path from 'path';
3 |
4 | interface SyncState {
5 | lastSyncTimestamp: string;
6 | lastProcessedIds: {
7 | chatMessages: string[];
8 | consciousMemories: string[];
9 | ragMemories: string[];
10 | };
11 | }
12 |
13 | export class SyncStateManager {
14 | private statePath: string;
15 |
16 | constructor() {
17 | this.statePath = path.join(process.cwd(), 'data', 'kg-sync-state.json');
18 | }
19 |
20 | async read(): Promise {
21 | try {
22 | const data = await fs.readFile(this.statePath, 'utf-8');
23 | return JSON.parse(data);
24 | } catch {
25 | return null;
26 | }
27 | }
28 |
29 | async write(state: SyncState): Promise {
30 | await fs.mkdir(path.dirname(this.statePath), { recursive: true });
31 | await fs.writeFile(this.statePath, JSON.stringify(state, null, 2));
32 | }
33 |
34 | async updateTimestamp(): Promise {
35 | const current = await this.read() || {
36 | lastSyncTimestamp: '',
37 | lastProcessedIds: { chatMessages: [], consciousMemories: [], ragMemories: [] }
38 | };
39 |
40 | current.lastSyncTimestamp = new Date().toISOString();
41 | await this.write(current);
42 | }
43 |
44 | // New method to be added:
45 | async updateLastProcessedIds(processedIds: {
46 | chatMessages?: string[],
47 | consciousMemories?: string[],
48 | ragMemories?: string[]
49 | }): Promise {
50 | const current = await this.read() || {
51 | lastSyncTimestamp: new Date().toISOString(), // Or consider if timestamp should only be updated by updateTimestamp
52 | lastProcessedIds: { chatMessages: [], consciousMemories: [], ragMemories: [] }
53 | };
54 |
55 | if (processedIds.chatMessages) {
56 | current.lastProcessedIds.chatMessages = processedIds.chatMessages;
57 | }
58 | if (processedIds.consciousMemories) {
59 | current.lastProcessedIds.consciousMemories = processedIds.consciousMemories;
60 | }
61 | if (processedIds.ragMemories) {
62 | current.lastProcessedIds.ragMemories = processedIds.ragMemories;
63 | }
64 |
65 | // Optional: Update timestamp when IDs are updated?
66 | // current.lastSyncTimestamp = new Date().toISOString();
67 | // The user feedback for syncKnowledgeGraph shows lastSyncTimestamp is updated there explicitly.
68 | // So, this method should probably only focus on updating IDs.
69 | // If current state was null, lastSyncTimestamp will be new. If not, it will retain old one.
70 | // This seems fine. If the state was null, it's like the first time we're recording anything.
71 |
72 | await this.write(current);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/lib/kg-type-converters.ts:
--------------------------------------------------------------------------------
1 | import { ExtractedEntity, ExtractedRelationship } from './llm-service';
2 | import { KgNode } from '../types/knowledge-graph';
3 | import { KgRelationship } from '../types/knowledge-graph';
4 |
5 | export function convertExtractedEntityToKgNode(entity: ExtractedEntity): KgNode {
6 | return {
7 | id: entity.id,
8 | type: entity.label, // Convert label -> type
9 | properties: entity.properties,
10 | createdAt: new Date()
11 | };
12 | }
13 |
14 | export function convertExtractedRelationshipToKgRelationship(rel: ExtractedRelationship): KgRelationship {
15 | return {
16 | id: rel.id || `${rel.sourceEntityId}_${rel.type}_${rel.targetEntityId}`, // Generate ID if not provided
17 | type: rel.type,
18 | sourceNodeId: rel.sourceEntityId,
19 | targetNodeId: rel.targetEntityId,
20 | properties: rel.properties || {},
21 | createdAt: new Date()
22 | };
23 | }
24 |
--------------------------------------------------------------------------------
/src/lib/logger.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Development-only logging utility
3 | * Provides consistent logging that only outputs in development mode
4 | */
5 |
6 | type LogLevel = 'debug' | 'info' | 'warn' | 'error';
7 |
8 | interface LoggerOptions {
9 | prefix?: string;
10 | enabled?: boolean;
11 | }
12 |
13 | class Logger {
14 | private prefix: string;
15 | private enabled: boolean;
16 |
17 | constructor(options: LoggerOptions = {}) {
18 | this.prefix = options.prefix || '';
19 | this.enabled = options.enabled !== undefined
20 | ? options.enabled
21 | : process.env.NODE_ENV === 'development';
22 | }
23 |
24 | private log(level: LogLevel, ...args: any[]) {
25 | if (!this.enabled) return;
26 |
27 | const timestamp = new Date().toISOString();
28 | const prefixStr = this.prefix ? `[${this.prefix}]` : '';
29 |
30 | const logFn = level === 'error' ? console.error :
31 | level === 'warn' ? console.warn :
32 | console.log;
33 |
34 | logFn(`[${timestamp}] ${prefixStr}`, ...args);
35 | }
36 |
37 | debug(...args: any[]) {
38 | //this.log('debug', ...args);
39 | }
40 |
41 | info(...args: any[]) {
42 | //this.log('info', ...args);
43 | }
44 |
45 | warn(...args: any[]) {
46 | this.log('warn', ...args);
47 | }
48 |
49 | error(...args: any[]) {
50 | this.log('error', ...args);
51 | }
52 |
53 | // Create a child logger with a new prefix
54 | child(prefix: string): Logger {
55 | return new Logger({
56 | prefix: this.prefix ? `${this.prefix}:${prefix}` : prefix,
57 | enabled: this.enabled
58 | });
59 | }
60 | }
61 |
62 | // Export factory function
63 | export function createLogger(prefix?: string): Logger {
64 | return new Logger({ prefix });
65 | }
66 |
67 | // Export default logger instance
68 | export const logger = new Logger();
69 |
--------------------------------------------------------------------------------
/src/lib/mcp-manager.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3 | import { MCPServerConfig } from '../types/mcp';
4 |
5 | export class MCPManager {
6 | private clients: Map = new Map();
7 | private transports: Map = new Map();
8 | async connectToServer(config: MCPServerConfig): Promise {
9 | if (this.clients.has(config.name)) {
10 | return; // Already connected
11 | }
12 |
13 | try { // Create stdio transport for the server with environment variables
14 | const transport = new StdioClientTransport({
15 | command: config.command!,
16 | args: config.args || [],
17 | env: {
18 | // Include existing environment (filter out undefined values)
19 | ...Object.fromEntries(
20 | Object.entries(process.env).filter(([_, value]) => value !== undefined)
21 | ) as Record,
22 | // Override with config-specific env vars
23 | ...(config.env || {})
24 | }
25 | });
26 |
27 | // Create client
28 | const client = new Client({
29 | name: 'MCP Chat Client',
30 | version: '1.0.0'
31 | }, {
32 | capabilities: {
33 | tools: {},
34 | resources: {},
35 | prompts: {}
36 | }
37 | });
38 |
39 | // Connect
40 | await client.connect(transport);
41 |
42 | // Store references
43 | this.clients.set(config.name, client);
44 | this.transports.set(config.name, transport);
45 |
46 | console.log(`Connected to MCP server: ${config.name}`);
47 | } catch (error) {
48 | console.error(`Failed to connect to MCP server ${config.name}:`, error);
49 | throw error;
50 | }
51 | }
52 |
53 | async disconnectFromServer(serverName: string): Promise {
54 | const client = this.clients.get(serverName);
55 | const transport = this.transports.get(serverName);
56 |
57 | if (client && transport) {
58 | try {
59 | await client.close();
60 | this.clients.delete(serverName);
61 | this.transports.delete(serverName);
62 | console.log(`Disconnected from MCP server: ${serverName}`);
63 | } catch (error) {
64 | console.error(`Error disconnecting from ${serverName}:`, error);
65 | }
66 | }
67 | } async callTool(serverName: string, toolName: string, args: any): Promise {
68 | // Check if the server exists
69 | const client = this.clients.get(serverName);
70 | if (!client) {
71 | console.warn(` MCP Manager: Server not found: ${serverName}`);
72 | return {
73 | error: true,
74 | isError: true,
75 | message: `Tool call failed: Server "${serverName}" not found.`,
76 | details: `Available servers: ${this.getConnectedServers().join(', ')}`,
77 | server: serverName,
78 | tool: toolName
79 | };
80 | }
81 |
82 | try {
83 | // Check if the tool exists before calling it
84 | const availableTools = await this.listTools(serverName);
85 | const toolExists = availableTools.some(tool => tool.name === toolName);
86 |
87 | if (!toolExists) {
88 | console.warn(` MCP Manager: Tool "${toolName}" not found in server "${serverName}"`);
89 | const allTools = await this.getAllAvailableTools();
90 |
91 | return {
92 | error: true,
93 | isError: true,
94 | message: `Tool "${toolName}" does not exist on server "${serverName}".`,
95 | details: `Available tools on server "${serverName}": ${availableTools.map(t => t.name).join(', ')}`,
96 | suggestedAlternatives: this.findSimilarTools(toolName, allTools),
97 | server: serverName,
98 | tool: toolName
99 | };
100 | }
101 |
102 | // If tool exists, proceed with the call
103 | const result = await client.callTool({
104 | name: toolName,
105 | arguments: args
106 | });
107 |
108 | // Ensure the result is properly serializable
109 | if (result && typeof result === 'object') {
110 | try {
111 | // Test serialization
112 | JSON.stringify(result);
113 | return result;
114 | } catch (serializationError) {
115 | console.warn(` MCP Manager: Result not serializable, converting:`, serializationError);
116 | return { message: String(result) };
117 | }
118 | }
119 |
120 | return result;
121 | } catch (error) {
122 | console.error(` MCP Manager: Error calling tool ${toolName} on ${serverName}:`, error);
123 |
124 | // Don't throw, return a structured error response
125 | const errorMessage = error instanceof Error ? error.message : String(error);
126 | return {
127 | error: true,
128 | isError: true,
129 | message: `MCP tool call failed: ${errorMessage}`,
130 | server: serverName,
131 | tool: toolName
132 | };
133 | }
134 | }
135 |
136 | async listTools(serverName: string): Promise {
137 | const client = this.clients.get(serverName);
138 | if (!client) {
139 | throw new Error(`Not connected to server: ${serverName}`);
140 | }
141 |
142 | try {
143 | const result = await client.listTools();
144 | return result.tools || [];
145 | } catch (error) {
146 | console.error(`Error listing tools for ${serverName}:`, error);
147 | return [];
148 | }
149 | }
150 |
151 | async listResources(serverName: string): Promise {
152 | const client = this.clients.get(serverName);
153 | if (!client) {
154 | throw new Error(`Not connected to server: ${serverName}`);
155 | }
156 |
157 | try {
158 | const result = await client.listResources();
159 | return result.resources || [];
160 | } catch (error) {
161 | console.error(`Error listing resources for ${serverName}:`, error);
162 | return [];
163 | }
164 | }
165 |
166 | getConnectedServers(): string[] {
167 | return Array.from(this.clients.keys());
168 | }
169 |
170 | isConnected(serverName: string): boolean {
171 | return this.clients.has(serverName);
172 | }
173 |
174 | async disconnectAll(): Promise {
175 | const disconnectPromises = Array.from(this.clients.keys())
176 | .map(serverName => this.disconnectFromServer(serverName));
177 |
178 | await Promise.all(disconnectPromises);
179 | }
180 |
181 | // Helper method to get all available tools across all servers
182 | async getAllAvailableTools(): Promise<{serverName: string, toolName: string}[]> {
183 | const allTools: {serverName: string, toolName: string}[] = [];
184 |
185 | for (const serverName of this.getConnectedServers()) {
186 | try {
187 | const tools = await this.listTools(serverName);
188 | tools.forEach(tool => {
189 | allTools.push({
190 | serverName,
191 | toolName: tool.name
192 | });
193 | });
194 | } catch (error) {
195 | console.error(`Error getting tools for ${serverName}:`, error);
196 | }
197 | }
198 |
199 | return allTools;
200 | }
201 |
202 | // Find similar tool names to suggest alternatives
203 | findSimilarTools(toolName: string, allTools: {serverName: string, toolName: string}[]): string[] {
204 | // Basic similarity - tools that contain part of the requested name
205 | const nameParts = toolName.toLowerCase().split('_');
206 |
207 | return allTools
208 | .filter(tool => {
209 | const lowerToolName = tool.toolName.toLowerCase();
210 | return nameParts.some(part =>
211 | part.length > 3 && lowerToolName.includes(part)
212 | );
213 | })
214 | .map(tool => `${tool.serverName}_${tool.toolName}`)
215 | .slice(0, 5); // Limit to 5 suggestions
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/lib/motive-force-storage.ts:
--------------------------------------------------------------------------------
1 | import { readFileSync, writeFileSync, existsSync } from 'fs';
2 | import { join } from 'path';
3 |
4 | const PROMPT_FILE_PATH = join(process.cwd(), 'motive-force-prompt.md');
5 |
6 | const DEFAULT_SYSTEM_PROMPT = `# MotiveForce (Autopilot) System Prompt
7 |
8 | You are an AI assistant operating in "autopilot mode" - your job is to analyze the conversation and suggest the next best action or query to continue the investigation.
9 |
10 | ## Your Role
11 | - Analyze the conversation context to determine the most valuable follow-up
12 | - Generate a single, clear question or command that advances the topic
13 | - Focus on helping the user achieve their apparent goals
14 |
15 | ## Approach Guidelines
16 | - For exploratory topics: Ask questions that broaden understanding
17 | - For problem-solving: Suggest actions that drive toward solutions
18 | - For creative tasks: Propose ideas that build upon established concepts
19 | - For learning topics: Ask questions that test comprehension
20 |
21 | ## Response Format
22 | - Return ONLY the next question/command without explanations
23 | - Keep responses concise and directly actionable
24 | - DO NOT include prefixes like "Next query:" or "Follow up:"
25 | `;
26 |
27 | export class MotiveForceStorage {
28 | static getSystemPrompt(): string {
29 | try {
30 | console.log('[MotiveForceStorage] Reading from:', PROMPT_FILE_PATH);
31 |
32 | if (!existsSync(PROMPT_FILE_PATH)) {
33 | console.warn('[MotiveForceStorage] File does not exist, creating default');
34 | this.resetSystemPrompt();
35 | }
36 |
37 | const content = readFileSync(PROMPT_FILE_PATH, 'utf-8').trim();
38 | console.log('[MotiveForceStorage] Read', content.length, 'characters');
39 |
40 | // Verify it's the correct prompt
41 | if (!content.includes('MotiveForce') && !content.includes('Autopilot')) {
42 | console.error('[MotiveForceStorage] WARNING: Prompt might be incorrect!');
43 | }
44 |
45 | return content;
46 | } catch (error) {
47 | console.error('[MotiveForceStorage] Error reading system prompt:', error);
48 | return DEFAULT_SYSTEM_PROMPT;
49 | }
50 | }
51 |
52 | static saveSystemPrompt(prompt: string): void {
53 | try {
54 | writeFileSync(PROMPT_FILE_PATH, prompt, 'utf-8');
55 | } catch (error) {
56 | console.error('Error saving system prompt:', error);
57 | throw new Error('Failed to save system prompt');
58 | }
59 | }
60 |
61 | static appendToSystemPrompt(text: string): void {
62 | try {
63 | const current = this.getSystemPrompt();
64 | const updated = `${current}\n\n## User Instructions\n${text}`;
65 | this.saveSystemPrompt(updated);
66 | } catch (error) {
67 | console.error('Error appending to system prompt:', error);
68 | throw new Error('Failed to append to system prompt');
69 | }
70 | }
71 |
72 | static resetSystemPrompt(): void {
73 | try {
74 | writeFileSync(PROMPT_FILE_PATH, DEFAULT_SYSTEM_PROMPT, 'utf-8');
75 | } catch (error) {
76 | console.error('Error resetting system prompt:', error);
77 | throw new Error('Failed to reset system prompt');
78 | }
79 | }
80 |
81 | static getPromptPath(): string {
82 | return PROMPT_FILE_PATH;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/lib/motive-force.ts:
--------------------------------------------------------------------------------
1 | import { LLMService, LLMProvider } from './llm-service';
2 | import { getRAGService } from './rag';
3 | import { getConsciousMemoryService } from './conscious-memory';
4 | import { MotiveForceStorage } from './motive-force-storage';
5 | import { MotiveForceConfig, DEFAULT_MOTIVE_FORCE_CONFIG } from '../types/motive-force';
6 | import { ChatMessage } from '../lib/chat-history';
7 | import { streamText } from 'ai';
8 |
9 | export class MotiveForceService {
10 | private static instance: MotiveForceService;
11 | private llmService: LLMService;
12 | private ragService = getRAGService();
13 | private memoryService = getConsciousMemoryService();
14 | private initialized = false;
15 |
16 | private constructor(private config: MotiveForceConfig) {
17 | this.llmService = new LLMService({
18 | provider: config.provider || 'google',
19 | model: config.model || 'gemini-2.5-flash-preview-05-20'
20 | });
21 | }
22 |
23 | static getInstance(config?: Partial): MotiveForceService {
24 | if (!MotiveForceService.instance) {
25 | MotiveForceService.instance = new MotiveForceService({
26 | ...DEFAULT_MOTIVE_FORCE_CONFIG,
27 | ...config
28 | });
29 | }
30 | return MotiveForceService.instance;
31 | }
32 |
33 | async initialize(): Promise {
34 | if (this.initialized) return;
35 |
36 | try {
37 | await this.llmService.initialize();
38 | this.initialized = true;
39 | } catch (error) {
40 | console.error('Failed to initialize MotiveForce service:', error);
41 | throw error;
42 | }
43 | }
44 | async generateNextQuery(
45 | messages: ChatMessage[],
46 | sessionId: string
47 | ): Promise {
48 | if (!this.initialized) {
49 | await this.initialize();
50 | }
51 | try {
52 | // Get the base system prompt from motive-force-prompt.md
53 | const baseSystemPrompt = MotiveForceStorage.getSystemPrompt();
54 |
55 | // Debug: Log first 100 chars of the prompt to verify it's correct
56 | console.log('[MotiveForce] Using system prompt:', baseSystemPrompt.substring(0, 100) + '...');
57 |
58 | // Verify we're not accidentally getting the main system prompt
59 | if (baseSystemPrompt.includes('tool-empowered assistant')) {
60 | console.error('[MotiveForce] ERROR: Got main system prompt instead of motive force prompt!');
61 | throw new Error('Motive force is using the wrong system prompt file');
62 | }
63 |
64 | // Get the last user message before motive force takes over
65 | const lastUserMessage = messages
66 | .filter(m => m.role === 'user')
67 | .slice(-1)[0];
68 |
69 | // Get additional context if enabled
70 | let additionalContext = '';
71 |
72 | if (this.config.useRag && this.ragService) {
73 | if (lastUserMessage) {
74 | const ragResult = await this.ragService.retrieveAndFormatContext(
75 | lastUserMessage.content
76 | );
77 |
78 | if (ragResult.memories.length > 0) {
79 | additionalContext += '\n\n## Relevant Context from Memory:\n';
80 | additionalContext += ragResult.memories
81 | .map(r => `- ${r.text}`)
82 | .join('\n');
83 | }
84 | }
85 | }
86 |
87 | if (this.config.useConsciousMemory && this.memoryService) {
88 | const recentMessages = messages
89 | .slice(-this.config.historyDepth)
90 | .map(msg => `${msg.role === 'user' ? 'User' : 'Assistant'}: ${msg.content}`)
91 | .join('\n\n');
92 |
93 | const memories = await this.memoryService.searchMemories(
94 | recentMessages.slice(-200), // Last 200 chars as query
95 | {
96 | limit: 3,
97 | importanceMin: 5
98 | }
99 | );
100 |
101 | if (memories.length > 0) {
102 | additionalContext += '\n\n## Conscious Memories:\n';
103 | additionalContext += memories
104 | .map(m => `- ${m.text}`)
105 | .join('\n');
106 | }
107 | }
108 |
109 | // Construct the enhanced system prompt by appending user context to the base prompt
110 | const userContextSection = `\n\n---\n\n## Context: Last Message from User Before You Took Over for Them
111 |
112 | The last message from the user before you took over for them was: "${lastUserMessage?.content || 'No previous user message found'}"
113 |
114 | You should act as the human user of the system and continue their conversation as if you were them.${additionalContext ? '\n\n' + additionalContext : ''}`;
115 |
116 | const enhancedSystemPrompt = baseSystemPrompt + userContextSection;
117 |
118 | // Convert ChatMessage[] to proper message format, excluding the last user message to avoid duplication
119 | const conversationMessages = messages
120 | .slice(-this.config.historyDepth)
121 | .slice(0, -1) // Remove last message since it's now in system prompt
122 | .map(msg => ({
123 | role: msg.role as 'user' | 'assistant',
124 | content: msg.content
125 | }));
126 |
127 | // Get model without tools to avoid naming issues
128 | const { model } = await this.llmService.getModelAndTools(false);
129 |
130 | // If no conversation history, use a prompt approach
131 | let streamOptions: any;
132 |
133 | if (conversationMessages.length === 0) {
134 | // Use prompt-based approach when no history
135 | streamOptions = {
136 | model,
137 | system: enhancedSystemPrompt,
138 | prompt: "What should I do next?",
139 | temperature: this.config.temperature || 0.7,
140 | maxTokens: 8000,
141 | };
142 | } else {
143 | // Use messages-based approach with history
144 | streamOptions = {
145 | model,
146 | system: enhancedSystemPrompt,
147 | messages: conversationMessages,
148 | temperature: this.config.temperature || 0.7,
149 | maxTokens: 8000,
150 | };
151 | }
152 |
153 | // Stream the response
154 | let resultText = '';
155 | try {
156 | console.log('[MotiveForce] Calling streamText with options:', {
157 | ...streamOptions,
158 | system: streamOptions.system?.substring(0, 100) + '...',
159 | messages: streamOptions.messages?.length || 0
160 | });
161 |
162 | const result = await streamText(streamOptions);
163 |
164 | for await (const chunk of result.textStream) {
165 | resultText += chunk;
166 | }
167 | } catch (streamError) {
168 | console.error('Streaming failed:', streamError);
169 | throw streamError;
170 | }
171 |
172 | // Clean up the response
173 | return this.cleanGeneratedQuery(resultText);
174 | } catch (error) {
175 | console.error('Error generating next query:', error);
176 | throw error;
177 | }
178 | }
179 |
180 | private cleanGeneratedQuery(query: string): string {
181 | return query
182 | .trim()
183 | .replace(/^("|'|`)|("|'|`)$/g, '')
184 | .replace(/^(Question|Command|Follow-up|Response|Query|Next):\s*/i, '')
185 | .replace(/^\[.*?\]\s*/, '') // Remove [Autopilot] or similar prefixes
186 | .trim();
187 | }
188 |
189 | updateConfig(config: Partial): void {
190 | this.config = { ...this.config, ...config };
191 |
192 | // Update LLM service if provider/model changed
193 | if (config.provider || config.model) {
194 | this.llmService = new LLMService({
195 | provider: config.provider || this.config.provider || 'google',
196 | model: config.model || this.config.model || 'gemini-2.5-flash-preview-05-20'
197 | });
198 | this.initialized = false;
199 | }
200 | }
201 |
202 | getConfig(): MotiveForceConfig {
203 | return { ...this.config };
204 | }
205 | }
206 |
207 | export function getMotiveForceService(config?: Partial): MotiveForceService {
208 | return MotiveForceService.getInstance(config);
209 | }
210 |
--------------------------------------------------------------------------------
/src/lib/neo4j-service.ts:
--------------------------------------------------------------------------------
1 | import neo4j from 'neo4j-driver';
2 |
3 | export class Neo4jService {
4 | private driver: any;
5 | private initialized: boolean = false;
6 |
7 | constructor() {
8 | // Configuration for Neo4j connection
9 | const uri = process.env.NEO4J_URI || 'bolt://localhost:7687';
10 | const user = process.env.NEO4J_USER || 'neo4j';
11 | const password = process.env.NEO4J_PASSWORD || 'password'; // Use a strong password in production
12 |
13 | this.driver = neo4j.driver(uri, neo4j.auth.basic(user, password));
14 | console.log('Neo4jService initialized with URI:', uri);
15 | }
16 |
17 | async initialize(): Promise {
18 | if (this.initialized) {
19 | console.log('Neo4jService already initialized.');
20 | return;
21 | }
22 | try {
23 | // Verify connectivity
24 | await this.driver.verifyConnectivity();
25 | this.initialized = true;
26 | console.log(' Neo4j connection established successfully.');
27 | } catch (error) {
28 | console.error(' Failed to connect to Neo4j:', error);
29 | throw error;
30 | }
31 | }
32 |
33 | async close(): Promise {
34 | if (this.driver) {
35 | await this.driver.close();
36 | this.initialized = false;
37 | console.log(' Neo4j connection closed.');
38 | }
39 | }
40 |
41 | // --- Basic CRUD Operations (to be expanded) ---
42 |
43 | async createNode(label: string, properties: Record): Promise {
44 | const session = this.driver.session();
45 | try {
46 | const result = await session.run(
47 | `CREATE (n:${label} $properties) RETURN n`,
48 | { properties }
49 | );
50 | return result.records[0].get('n').properties;
51 | } finally {
52 | await session.close();
53 | }
54 | }
55 |
56 | async createRelationship(
57 | fromNodeId: string,
58 | fromNodeLabel: string,
59 | toNodeId: string,
60 | toNodeLabel: string,
61 | relationshipType: string,
62 | properties: Record = {}
63 | ): Promise {
64 | const session = this.driver.session();
65 | try {
66 | const query = `
67 | MATCH (a:${fromNodeLabel}), (b:${toNodeLabel})
68 | WHERE a.id = $fromNodeId AND b.id = $toNodeId
69 | CREATE (a)-[r:${relationshipType} $properties]->(b)
70 | RETURN r
71 | `;
72 | const result = await session.run(query, { fromNodeId, toNodeId, properties });
73 | return result.records[0].get('r').properties;
74 | } finally {
75 | await session.close();
76 | }
77 | }
78 |
79 | async findNode(label: string, property: string, value: any): Promise {
80 | const session = this.driver.session();
81 | try {
82 | const result = await session.run(
83 | `MATCH (n:${label} {${property}: $value}) RETURN n LIMIT 1`,
84 | { value }
85 | );
86 | return result.records.length > 0 ? result.records[0].get('n').properties : null;
87 | } finally {
88 | await session.close();
89 | }
90 | }
91 |
92 | // Add more advanced query methods as needed for graph traversal and reasoning
93 | }
94 |
95 | // Export singleton instance
96 | let neo4jServiceInstance: Neo4jService | null = null;
97 |
98 | export function getNeo4jService(): Neo4jService {
99 | if (!neo4jServiceInstance) {
100 | neo4jServiceInstance = new Neo4jService();
101 | }
102 | return neo4jServiceInstance;
103 | }
104 |
--------------------------------------------------------------------------------
/src/lib/rag-config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * RAG configuration utilities for the lean MCP chat client
3 | */
4 |
5 | export interface RAGConfig {
6 | enabled: boolean;
7 | maxMemories: number;
8 | minSimilarity: number;
9 | includeSessionContext: boolean;
10 | chromaUrl: string;
11 | chromaCollection: string;
12 | googleApiKey?: string;
13 | summarization: {
14 | enabled: boolean;
15 | threshold: number;
16 | provider: 'google' | 'openai';
17 | };
18 | }
19 |
20 | export function getRAGConfig(): RAGConfig {
21 | return {
22 | enabled: process.env.RAG_ENABLED !== 'false',
23 | maxMemories: parseInt(process.env.RAG_MAX_MEMORIES || '5'),
24 | minSimilarity: parseFloat(process.env.RAG_MIN_SIMILARITY || '0.15'),
25 | includeSessionContext: process.env.RAG_INCLUDE_SESSION_CONTEXT === 'true',
26 | chromaUrl: process.env.CHROMA_URL || 'http://localhost:8000',
27 | chromaCollection: process.env.CHROMA_COLLECTION || 'mcp_chat_memories',
28 | googleApiKey: process.env.GOOGLE_API_KEY,
29 | summarization: {
30 | enabled: process.env.RAG_ENABLE_SUMMARIZATION !== 'false',
31 | threshold: parseInt(process.env.RAG_SUMMARIZATION_THRESHOLD || '3000'),
32 | provider: (process.env.RAG_SUMMARIZATION_PROVIDER as 'google' | 'openai') || 'google'
33 | }
34 | };
35 | }
36 |
37 | export function validateRAGConfig(config: RAGConfig): { valid: boolean; issues: string[] } {
38 | const issues: string[] = [];
39 |
40 | if (config.maxMemories < 1 || config.maxMemories > 10) {
41 | issues.push('maxMemories should be between 1 and 10');
42 | }
43 |
44 | if (config.minSimilarity < 0 || config.minSimilarity > 1) {
45 | issues.push('minSimilarity should be between 0 and 1');
46 | }
47 |
48 | if (!config.chromaUrl || !config.chromaUrl.startsWith('http')) {
49 | issues.push('chromaUrl should be a valid HTTP URL');
50 | }
51 |
52 | if (!config.chromaCollection || config.chromaCollection.trim().length === 0) {
53 | issues.push('chromaCollection cannot be empty');
54 | }
55 |
56 | if (!config.googleApiKey) {
57 | issues.push('googleApiKey is not set (fallback embeddings will be used)');
58 | }
59 |
60 | return {
61 | valid: issues.length === 0 || (issues.length === 1 && issues[0].includes('googleApiKey')),
62 | issues
63 | };
64 | }
65 |
66 | export function getRAGSystemPrompt(): string {
67 | return `You are an AI assistant with access to previous conversation history. When provided with context from previous conversations, use it to:
68 |
69 | 1. Provide more personalized and contextual responses
70 | 2. Reference previous discussions when relevant
71 | 3. Maintain conversation continuity
72 | 4. Avoid repeating information already discussed
73 |
74 | Always prioritize the current user message while leveraging relevant historical context to enhance your response.`;
75 | }
76 |
--------------------------------------------------------------------------------
/src/lib/retry.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Retry utility with exponential backoff
3 | */
4 |
5 | interface RetryOptions {
6 | maxAttempts?: number;
7 | initialDelay?: number;
8 | maxDelay?: number;
9 | backoffFactor?: number;
10 | shouldRetry?: (error: any, attemptNumber: number) => boolean;
11 | }
12 |
13 | const defaultOptions: Required = {
14 | maxAttempts: 3,
15 | initialDelay: 1000,
16 | maxDelay: 10000,
17 | backoffFactor: 2,
18 | shouldRetry: () => true,
19 | };
20 |
21 | export async function withRetry(
22 | fn: () => Promise,
23 | options: RetryOptions = {}
24 | ): Promise {
25 | const opts = { ...defaultOptions, ...options };
26 | let lastError: any;
27 |
28 | for (let attempt = 1; attempt <= opts.maxAttempts; attempt++) {
29 | try {
30 | return await fn();
31 | } catch (error) {
32 | lastError = error;
33 |
34 | if (attempt === opts.maxAttempts || !opts.shouldRetry(error, attempt)) {
35 | throw error;
36 | }
37 |
38 | const delay = Math.min(
39 | opts.initialDelay * Math.pow(opts.backoffFactor, attempt - 1),
40 | opts.maxDelay
41 | );
42 |
43 | await new Promise(resolve => setTimeout(resolve, delay));
44 | }
45 | }
46 |
47 | throw lastError;
48 | }
49 |
50 | // Helper to check if error is retryable based on common patterns
51 | export function isRetryableNetworkError(error: any): boolean {
52 | if (!error) return false;
53 |
54 | const errorStr = error.toString().toLowerCase();
55 | const retryablePatterns = [
56 | 'timeout',
57 | 'econnrefused',
58 | 'enotfound',
59 | 'etimedout',
60 | 'socket hang up',
61 | 'network error',
62 | '502',
63 | '503',
64 | '504',
65 | ];
66 |
67 | return retryablePatterns.some(pattern => errorStr.includes(pattern));
68 | }
69 |
--------------------------------------------------------------------------------
/src/lib/text-summarizer.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Standalone text summarization utility
3 | * Used by RAG system to reduce storage size while preserving key information
4 | */
5 |
6 | import { generateText } from 'ai';
7 | import { createGoogleGenerativeAI } from '@ai-sdk/google';
8 | import { createOpenAI } from '@ai-sdk/openai';
9 |
10 | export interface SummarizationOptions {
11 | maxLength?: number;
12 | preserveContext?: string;
13 | provider?: 'google' | 'openai';
14 | }
15 |
16 | export interface SummarizationResult {
17 | summary: string;
18 | originalLength: number;
19 | summaryLength: number;
20 | compressionRatio: number;
21 | wasSummarized: boolean;
22 | }
23 |
24 | /**
25 | * Summarize text while preserving key information
26 | * This is a standalone utility with no dependencies on our RAG/LLM services
27 | */
28 | export async function summarizeText(
29 | text: string,
30 | options: SummarizationOptions = {}
31 | ): Promise {
32 | const {
33 | maxLength = 500,
34 | preserveContext = '',
35 | provider = 'google'
36 | } = options;
37 |
38 | const originalLength = text.length;
39 |
40 | // If text is already short enough, return as-is
41 | if (originalLength <= maxLength) {
42 | return {
43 | summary: text,
44 | originalLength,
45 | summaryLength: originalLength,
46 | compressionRatio: 1.0,
47 | wasSummarized: false
48 | };
49 | }
50 |
51 | try {
52 | // Initialize model based on provider
53 | let model;
54 | if (provider === 'google' && process.env.GOOGLE_API_KEY) {
55 | const google = createGoogleGenerativeAI({
56 | apiKey: process.env.GOOGLE_API_KEY
57 | });
58 | model = google('gemini-1.5-flash');
59 | } else if (provider === 'openai' && process.env.OPENAI_API_KEY) {
60 | const openai = createOpenAI({
61 | apiKey: process.env.OPENAI_API_KEY
62 | });
63 | model = openai('gpt-3.5-turbo');
64 | } else {
65 | throw new Error(`No API key available for provider: ${provider}`);
66 | }
67 |
68 | const summaryPrompt = `You are a text summarizer that preserves key concrete information.
69 |
70 | CRITICAL REQUIREMENTS:
71 | - Preserve ALL dates, times, file paths, URLs, specific names
72 | - Preserve ALL technical details, error codes, version numbers
73 | - Preserve ALL specific user intentions and context
74 | - Keep factual information intact
75 | - Aim for ~${maxLength} characters but focus on completeness over length
76 |
77 | ${preserveContext ? `CONTEXT: ${preserveContext}` : ''}
78 |
79 | TEXT TO SUMMARIZE:
80 | ${text}
81 |
82 | SUMMARY:`;
83 |
84 | const result = await generateText({
85 | model,
86 | prompt: summaryPrompt,
87 | maxTokens: Math.ceil(maxLength / 3), // Rough token estimation
88 | temperature: 0.1, // Low temperature for consistent, factual summaries
89 | });
90 |
91 | const summary = result.text.trim();
92 |
93 | // Ensure summary is actually shorter than original
94 | if (summary.length >= originalLength * 0.8) {
95 | console.warn(' Summary not significantly shorter than original, using truncated original');
96 | const truncated = text.substring(0, maxLength) + '...';
97 | return {
98 | summary: truncated,
99 | originalLength,
100 | summaryLength: truncated.length,
101 | compressionRatio: truncated.length / originalLength,
102 | wasSummarized: true
103 | };
104 | }
105 |
106 | console.log(` Summarized ${originalLength} chars → ${summary.length} chars`);
107 | return {
108 | summary,
109 | originalLength,
110 | summaryLength: summary.length,
111 | compressionRatio: summary.length / originalLength,
112 | wasSummarized: true
113 | };
114 |
115 | } catch (error) {
116 | console.error(' Summarization failed:', error);
117 | // Fallback: truncate original text
118 | const truncated = text.substring(0, maxLength) + '...';
119 | return {
120 | summary: truncated,
121 | originalLength,
122 | summaryLength: truncated.length,
123 | compressionRatio: truncated.length / originalLength,
124 | wasSummarized: true
125 | };
126 | }
127 | }
128 |
129 | /**
130 | * Check if summarization should be applied based on configuration
131 | */
132 | export function shouldSummarize(
133 | text: string,
134 | config: {
135 | enabled: boolean;
136 | threshold: number;
137 | }
138 | ): boolean {
139 | return config.enabled && text.length > config.threshold;
140 | }
141 |
--------------------------------------------------------------------------------
/src/lib/tool-error-handler.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Tool Error Handler Utility
3 | * Handles errors for non-existent tools and converts them to proper tool results
4 | */
5 |
6 | /**
7 | * Creates a transform stream that converts NoSuchToolError errors into tool results
8 | * This prevents the conversation from crashing when an AI tries to use a non-existent tool
9 | */
10 | export function createToolErrorHandlerStream() {
11 | return new TransformStream({
12 | transform(chunk, controller) {
13 | // If this is an error chunk for a non-existent tool, convert it to a tool result
14 | if (chunk.type === 'error' &&
15 | (chunk.error?.name === 'AI_NoSuchToolError' ||
16 | chunk.error?.message?.includes('NoSuchToolError'))) {
17 |
18 | const toolName = chunk.error.toolName;
19 | const toolCallId = chunk.error.toolCallId;
20 |
21 | // Find the server name if present in the tool name
22 | const serverName = toolName.includes('_') ?
23 | toolName.split('_')[0] : 'unknown';
24 |
25 | // Create a response that informs the AI about the error but allows conversation to continue
26 | const toolResult = {
27 | type: 'tool-result',
28 | toolCallId: toolCallId,
29 | toolName: toolName,
30 | result: {
31 | error: true,
32 | message: `Tool "${toolName}" does not exist.`,
33 | details: `Please use one of the available tools instead.`,
34 | availableTools: chunk.error.availableTools || []
35 | }
36 | };
37 |
38 | // Send this modified chunk instead of the error
39 | controller.enqueue(toolResult);
40 | } else {
41 | // Pass through all other chunks normally
42 | controller.enqueue(chunk);
43 | }
44 | }
45 | });
46 | }
47 |
48 | /**
49 | * Middleware for handling AI tool errors
50 | * Use this in your API routes to wrap the stream handling
51 | */
52 | export function handleToolErrors(stream: any) {
53 | return stream.pipeThrough(createToolErrorHandlerStream());
54 | }
55 |
--------------------------------------------------------------------------------
/src/scripts/migrate-timestamps.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | /**
4 | * Migration script to convert ISO string timestamps to numeric timestamps in ChromaDB
5 | */
6 | import { getMemoryStore } from '../lib/memory-store';
7 |
8 | async function main() {
9 | console.log('Starting timestamp migration');
10 |
11 | const memoryStore = getMemoryStore();
12 | await memoryStore.initialize();
13 |
14 | try {
15 | await memoryStore.migrateTimestamps();
16 | console.log('Migration completed successfully');
17 | } catch (error) {
18 | console.error('Migration failed:', error);
19 | process.exit(1);
20 | }
21 | }
22 |
23 | main().catch(console.error);
24 |
--------------------------------------------------------------------------------
/src/scripts/run-kg-sync.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Knowledge Graph Synchronization Service
3 | * This script can run as a one-off sync or as a continuous background process
4 | *
5 | * Run modes:
6 | * - One-time sync: npx tsx src/scripts/run-kg-sync.ts
7 | * - Background service: npx tsx src/scripts/run-kg-sync.ts --watch
8 | * - Full resync: npx tsx src/scripts/run-kg-sync.ts --full-resync
9 | * - Process all queue items: npx tsx src/scripts/run-kg-sync.ts --process-all
10 | */
11 |
12 | // Load environment variables from .env files BEFORE importing any services
13 | import { config } from 'dotenv';
14 | import { join } from 'path';
15 |
16 | const envPath = join(process.cwd(), '.env');
17 | const envLocalPath = join(process.cwd(), '.env.local');
18 |
19 | config({ path: envPath });
20 | config({ path: envLocalPath });
21 |
22 | // Now import services after environment variables are loaded
23 | async function importServices() {
24 | const { default: knowledgeGraphSyncServiceInstance } = await import('../lib/knowledge-graph-sync-service');
25 | const { default: knowledgeGraphServiceInstanceNeo4j } = await import('../lib/knowledge-graph-service');
26 | const { kgSyncQueue } = await import('../lib/kg-sync-queue');
27 |
28 | return {
29 | knowledgeGraphSyncServiceInstance,
30 | knowledgeGraphServiceInstanceNeo4j,
31 | kgSyncQueue
32 | };
33 | }
34 |
35 | // Parse command line arguments
36 | const args = process.argv.slice(2);
37 | const watchMode = args.includes('--watch');
38 | const forceFullResync = args.includes('--full-resync');
39 | const processAll = args.includes('--process-all');
40 | const syncIntervalMs = 30000; // 30 seconds between sync operations in watch mode
41 |
42 | async function runSync() {
43 |
44 | // Import services after environment variables are loaded
45 | const {
46 | knowledgeGraphSyncServiceInstance: syncService,
47 | knowledgeGraphServiceInstanceNeo4j: neo4jService,
48 | kgSyncQueue
49 | } = await importServices();
50 |
51 | try {
52 | // Ensure Neo4j driver is connected
53 | await neo4jService.connect();
54 |
55 | // Initialize queue if not already
56 | await kgSyncQueue.initialize();
57 | const queueSize = await kgSyncQueue.getQueueSize();
58 |
59 | if (queueSize > 0) {
60 | console.log(` [KG Sync] Found ${queueSize} items in sync queue`);
61 |
62 | // Process queue items if there are any
63 | if (processAll) {
64 | const processedCount = await kgSyncQueue.processAll(async (request: any) => {
65 | // Run appropriate sync based on request type
66 | if (request.type === 'full') {
67 | await syncService.syncKnowledgeGraph({ forceFullResync: true });
68 | } else {
69 | await syncService.syncKnowledgeGraph({ forceFullResync: false });
70 | }
71 | });
72 |
73 | console.log(` [KG Sync] Processed ${processedCount} queue items`);
74 | } else {
75 | // Process just one item (oldest first)
76 | const processed = await kgSyncQueue.processNext(async (request: any) => {
77 | if (request.type === 'full') {
78 | await syncService.syncKnowledgeGraph({ forceFullResync: true });
79 | } else {
80 | await syncService.syncKnowledgeGraph({ forceFullResync: false });
81 | }
82 | });
83 |
84 | console.log(` [KG Sync] Processed ${processed ? 1 : 0} queue items`);
85 | }
86 | } else if (forceFullResync) {
87 | // If no queue items but full resync requested, run it directly
88 | await syncService.syncKnowledgeGraph({ forceFullResync: true });
89 | } else {
90 | // Regular incremental sync when no queue items exist
91 | await syncService.syncKnowledgeGraph({ forceFullResync: false });
92 | }
93 | } catch (error) {
94 | console.error(' [KG Sync] Error during synchronization:', error);
95 | process.exitCode = 1; // Indicate failure
96 | } finally {
97 | // Ensure Neo4j connection is closed (unless in watch mode)
98 | if (!watchMode) {
99 | try {
100 | await neo4jService.close();
101 | } catch (closeError) {
102 | console.error(' [KG Sync] Error closing Neo4j connection:', closeError);
103 | }
104 | }
105 | }
106 | }
107 |
108 | // Main execution flow
109 | async function main() {
110 | if (watchMode) {
111 | console.log(` [KG Sync] Starting in continuous watch mode (interval: ${syncIntervalMs}ms)`);
112 |
113 | // Run initial sync
114 | await runSync().catch(error => {
115 | console.error(' [KG Sync] Error in initial sync:', error);
116 | });
117 |
118 | // Set up interval for continuous operation
119 | setInterval(async () => {
120 | try {
121 | await runSync();
122 | } catch (error) {
123 | console.error(' [KG Sync] Error in scheduled sync:', error);
124 | }
125 | }, syncIntervalMs);
126 |
127 | // Handle graceful shutdown
128 | process.on('SIGINT', async () => {
129 | console.log(' [KG Sync] Shutting down gracefully...');
130 | try {
131 | const { knowledgeGraphServiceInstanceNeo4j } = await importServices();
132 | await knowledgeGraphServiceInstanceNeo4j.close();
133 | } catch (error) {
134 | console.error(' [KG Sync] Error during shutdown:', error);
135 | }
136 | process.exit(0);
137 | });
138 | } else {
139 | // One-time execution
140 | await runSync();
141 | console.log(' [KG Sync] One-time synchronization complete');
142 | }
143 | }
144 |
145 | main().catch(error => {
146 | console.error(' [KG Sync] Fatal error:', error);
147 | process.exitCode = 1;
148 | });
149 |
--------------------------------------------------------------------------------
/src/scripts/test-kg-end-to-end.ts:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 | import { join } from 'path';
3 |
4 | // Load environment variables first
5 | config({ path: join(process.cwd(), '.env') });
6 | config({ path: join(process.cwd(), '.env.local') });
7 |
8 | import { LLMService } from '../lib/llm-service';
9 | import knowledgeGraphService from '../lib/knowledge-graph-service';
10 | import knowledgeGraphSyncService from '../lib/knowledge-graph-sync-service';
11 | import { ChatHistoryDatabase } from '../lib/chat-history';
12 | import { ConsciousMemoryService } from '../types/memory';
13 | import { getConsciousMemoryService } from '../lib/conscious-memory';
14 | import { ChromaMemoryStore } from '../lib/memory-store';
15 |
16 | async function testEndToEnd() {
17 | console.log(' Testing Knowledge Graph End-to-End Flow\n');
18 |
19 | let llmService: LLMService | null = null;
20 | let consciousMemory: ConsciousMemoryService | null = null;
21 | let memoryStore: ChromaMemoryStore | null = null;
22 | const testSessionId = `test_session_${Date.now()}`;
23 |
24 | try {
25 | // Step 1: Initialize services
26 | console.log('1⃣ Initializing services...');
27 | llmService = new LLMService();
28 | await llmService.initialize();
29 |
30 | consciousMemory = getConsciousMemoryService();
31 | await consciousMemory.initialize();
32 |
33 | memoryStore = new ChromaMemoryStore();
34 | await memoryStore.initialize();
35 |
36 | await knowledgeGraphService.connect();
37 |
38 | console.log(' Services initialized\n');
39 |
40 | // Step 2: Test tool availability
41 | console.log('2⃣ Testing tool availability...');
42 | const tools = await llmService.getAvailableTools();
43 | const kgTools = Object.keys(tools).filter(t => t.includes('knowledge') || t.includes('graph'));
44 | console.log(`Found ${kgTools.length} knowledge graph tools:`, kgTools);
45 | console.log(' Tools verified\n');
46 |
47 | // Step 3: Create test data through chat history
48 | console.log('3⃣ Creating test chat data...');
49 | const chatHistory = ChatHistoryDatabase.getInstance();
50 |
51 | // Create session
52 | const session = chatHistory.createSession({
53 | id: testSessionId,
54 | title: 'KG End-to-End Test',
55 | messages: []
56 | });
57 |
58 | // Add test messages
59 | chatHistory.addMessage({
60 | id: `msg_1_${Date.now()}`,
61 | sessionId: testSessionId,
62 | role: 'user',
63 | content: 'John Smith works at OpenAI as a researcher. He is working on GPT-5.'
64 | });
65 |
66 | chatHistory.addMessage({
67 | id: `msg_2_${Date.now()}`,
68 | sessionId: testSessionId,
69 | role: 'assistant',
70 | content: 'I understand that John Smith is a researcher at OpenAI working on GPT-5. That must be exciting work!'
71 | });
72 |
73 | console.log(' Test chat data created\n');
74 |
75 | // Step 4: Store in conscious memory
76 | console.log('4⃣ Testing conscious memory storage...');
77 | const memoryId = await consciousMemory.saveMemory({
78 | content: 'John Smith is a researcher at OpenAI working on GPT-5 development',
79 | tags: ['test', 'person', 'organization'],
80 | importance: 8,
81 | context: 'Test context for KG integration'
82 | });
83 | console.log(` Memory saved with ID: ${memoryId}\n`);
84 |
85 | // Step 5: Store in RAG memory
86 | console.log('5⃣ Testing RAG memory storage...'); await memoryStore.storeMemory(
87 | 'John Smith leads the GPT-5 project at OpenAI',
88 | {
89 | sessionId: testSessionId,
90 | messageType: 'user',
91 | timestamp: Date.now(), // Use numeric timestamp
92 | textLength: 42
93 | }
94 | );
95 | console.log(' RAG memory stored\n');
96 |
97 | // Step 6: Run knowledge graph sync
98 | console.log('6⃣ Running knowledge graph sync...');
99 | const beforeStats = await knowledgeGraphService.getStatistics();
100 | console.log('Before sync:', beforeStats);
101 |
102 | await knowledgeGraphSyncService.syncKnowledgeGraph({ forceFullResync: true });
103 |
104 | const afterStats = await knowledgeGraphService.getStatistics();
105 | console.log('After sync:', afterStats);
106 | console.log(' Sync completed\n');
107 |
108 | // Step 7: Query the knowledge graph
109 | console.log('7⃣ Querying knowledge graph...');
110 |
111 | // Test query for Person nodes
112 | const personQuery = await knowledgeGraphService.runQuery(
113 | `MATCH (p:Person) WHERE p.name CONTAINS 'John' RETURN p`
114 | );
115 | console.log(`Found ${personQuery.length} Person nodes containing 'John'`);
116 |
117 | // Test query for relationships
118 | const relationshipQuery = await knowledgeGraphService.runQuery(
119 | `MATCH (p:Person)-[r:WORKS_AT]->(o:Organization) RETURN p.name as person, type(r) as relationship, o.name as organization`
120 | );
121 | console.log(`Found ${relationshipQuery.length} WORKS_AT relationships`);
122 |
123 | // Test query for memories
124 | const memoryQuery = await knowledgeGraphService.runQuery(
125 | `MATCH (m:ConsciousMemory) RETURN count(m) as count`
126 | );
127 | console.log(`Found ${memoryQuery[0]?.count || 0} ConsciousMemory nodes`);
128 |
129 | console.log(' Queries executed successfully\n');
130 |
131 | // Step 8: Test MCP tool calls
132 | console.log('8⃣ Testing MCP tool calls...');
133 |
134 | if (kgTools.includes('knowledge-graph.get_entity_details')) {
135 | // This would need to be adjusted based on actual entity IDs in the graph
136 | console.log('Knowledge graph tools available for testing');
137 | }
138 |
139 | // Cleanup test data
140 | console.log('\n Cleaning up test data...');
141 | chatHistory.deleteSession(testSessionId);
142 | await consciousMemory.deleteMemory(memoryId);
143 |
144 | console.log('\n End-to-end test completed successfully!');
145 |
146 | } catch (error) {
147 | console.error(' Test failed:', error);
148 | throw error;
149 | } finally {
150 | // Cleanup
151 | if (llmService) await llmService.cleanup();
152 | if (memoryStore) await memoryStore.cleanup();
153 | await knowledgeGraphService.close();
154 | }
155 | }
156 |
157 | // Run the test
158 | testEndToEnd().catch(console.error);
159 |
--------------------------------------------------------------------------------
/src/scripts/test-kg-sync.ts:
--------------------------------------------------------------------------------
1 | import { config } from 'dotenv';
2 | import { join } from 'path';
3 |
4 | // Load environment variables first
5 | config({ path: join(process.cwd(), '.env') });
6 | config({ path: join(process.cwd(), '.env.local') });
7 |
8 | import knowledgeGraphSyncService from '../lib/knowledge-graph-sync-service';
9 | import knowledgeGraphService from '../lib/knowledge-graph-service';
10 |
11 | async function testSync() {
12 | console.log('Testing Knowledge Graph Sync...\n');
13 |
14 | try {
15 | // Connect to Neo4j
16 | await knowledgeGraphService.connect();
17 | console.log(' Connected to Neo4j');
18 |
19 | // Get initial stats
20 | const beforeStats = await knowledgeGraphService.getStatistics();
21 | console.log(' Before sync:', beforeStats);
22 |
23 | // Run incremental sync
24 | console.log('\n Running incremental sync...');
25 | await knowledgeGraphSyncService.syncKnowledgeGraph({ forceFullResync: false });
26 |
27 | // Get after stats
28 | const afterStats = await knowledgeGraphService.getStatistics();
29 | console.log('\n After sync:', afterStats);
30 |
31 | console.log('\n Sync test completed successfully!');
32 |
33 | } catch (error) {
34 | console.error(' Sync test failed:', error);
35 | } finally {
36 | await knowledgeGraphService.close();
37 | }
38 | }
39 |
40 | testSync();
41 |
--------------------------------------------------------------------------------
/src/scripts/test-tool-calls.ts:
--------------------------------------------------------------------------------
1 | import { LLMService } from '../lib/llm-service';
2 | import { config } from 'dotenv';
3 | import { join } from 'path';
4 |
5 | // Load environment variables
6 | config({ path: join(process.cwd(), '.env') });
7 | config({ path: join(process.cwd(), '.env.local') });
8 |
9 | async function testToolCalls() {
10 | console.log('Testing Tool Call Integration...\n');
11 |
12 | const service = new LLMService();
13 | await service.initialize();
14 |
15 | try {
16 | // Get available tools
17 | const tools = await service.getAvailableTools();
18 | console.log(` Found ${Object.keys(tools).length} tools available`);
19 |
20 | // Test a simple tool call
21 | console.log('\n Testing conscious memory save...');
22 | const result = await service.callTool('conscious-memory', 'save_memory', {
23 | content: 'Test memory from tool call integration test',
24 | tags: ['test', 'integration'],
25 | importance: 5,
26 | context: 'Testing tool call handling'
27 | });
28 |
29 | console.log('Tool call result:', JSON.stringify(result, null, 2));
30 |
31 | // Test search to verify it was saved
32 | console.log('\n Searching for saved memory...');
33 | const searchResult = await service.callTool('conscious-memory', 'search_memories', {
34 | query: 'tool call integration test',
35 | limit: 5
36 | });
37 |
38 | console.log('Search result:', JSON.stringify(searchResult, null, 2));
39 |
40 | console.log('\n Tool call test completed successfully!');
41 |
42 | } catch (error) {
43 | console.error(' Tool call test failed:', error);
44 | } finally {
45 | await service.cleanup();
46 | }
47 | }
48 |
49 | testToolCalls();
50 |
--------------------------------------------------------------------------------
/src/tests/api-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Script to test the conscious memory API endpoints
3 | */
4 |
5 | async function testConsciousMemoryAPI() {
6 | const BASE_URL = 'http://localhost:3000';
7 |
8 | console.log(' Testing Conscious Memory API...');
9 |
10 | try {
11 | // Test 1: Health check
12 | console.log('\n1. Testing health check...');
13 | const healthResponse = await fetch(`${BASE_URL}/api/conscious-memory?action=health`);
14 | const healthData = await healthResponse.json();
15 | console.log('Health:', healthData);
16 |
17 | // Test 2: Save a memory
18 | console.log('\n2. Testing save memory...');
19 | const saveResponse = await fetch(`${BASE_URL}/api/conscious-memory`, {
20 | method: 'POST',
21 | headers: { 'Content-Type': 'application/json' },
22 | body: JSON.stringify({
23 | action: 'save',
24 | content: 'This is a test conscious memory about API testing',
25 | tags: ['test', 'api', 'validation'],
26 | importance: 8,
27 | source: 'explicit',
28 | context: 'Testing the conscious memory API endpoints'
29 | })
30 | });
31 | const saveData = await saveResponse.json();
32 | console.log('Save result:', saveData);
33 |
34 | // Test 3: Search memories
35 | console.log('\n3. Testing search memories...');
36 | const searchResponse = await fetch(`${BASE_URL}/api/conscious-memory`, {
37 | method: 'POST',
38 | headers: { 'Content-Type': 'application/json' },
39 | body: JSON.stringify({
40 | action: 'search',
41 | query: 'test API',
42 | options: { limit: 5 }
43 | })
44 | });
45 | const searchData = await searchResponse.json();
46 | console.log('Search results:', searchData);
47 |
48 | // Test 4: Get stats
49 | console.log('\n4. Testing get stats...');
50 | const statsResponse = await fetch(`${BASE_URL}/api/conscious-memory?action=stats`);
51 | const statsData = await statsResponse.json();
52 | console.log('Stats:', statsData);
53 |
54 | // Test 5: Get tags
55 | console.log('\n5. Testing get tags...');
56 | const tagsResponse = await fetch(`${BASE_URL}/api/conscious-memory?action=tags`);
57 | const tagsData = await tagsResponse.json();
58 | console.log('Tags:', tagsData);
59 |
60 | console.log('\n All API tests completed!');
61 |
62 | } catch (error) {
63 | console.error(' API test failed:', error);
64 | }
65 | }
66 |
67 | // Export for use in other scripts
68 | if (typeof window === 'undefined') {
69 | // Node.js environment
70 | testConsciousMemoryAPI();
71 | }
72 |
73 | export { testConsciousMemoryAPI };
74 |
--------------------------------------------------------------------------------
/src/tests/conscious-memory-test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Test script for conscious memory functionality
3 | */
4 |
5 | import { getConsciousMemoryService } from '../lib/conscious-memory.js';
6 |
7 | async function testConsciousMemory() {
8 | console.log(' Testing Conscious Memory System...');
9 |
10 | try {
11 | const memoryService = getConsciousMemoryService();
12 |
13 | // Test initialization
14 | console.log('1. Initializing...');
15 | await memoryService.initialize();
16 |
17 | // Test health check
18 | console.log('2. Health check...');
19 | const isHealthy = await memoryService.healthCheck();
20 | console.log(` Health: ${isHealthy ? ' Healthy' : ' Unhealthy'}`);
21 |
22 | if (!isHealthy) {
23 | console.log(' Memory system is not healthy, stopping test');
24 | return false;
25 | }
26 |
27 | // Test save memory
28 | console.log('3. Saving test memory...');
29 | const testId = await memoryService.saveMemory({
30 | content: 'This is a test conscious memory about testing the system',
31 | tags: ['test', 'system', 'validation'],
32 | importance: 8,
33 | source: 'explicit',
34 | context: 'Testing conscious memory functionality'
35 | });
36 | console.log(` Saved with ID: ${testId}`);
37 |
38 | // Test search
39 | console.log('4. Searching memories...');
40 | const searchResults = await memoryService.searchMemories('test system', {
41 | limit: 5
42 | });
43 | console.log(` Found ${searchResults.length} results`); searchResults.forEach((result: any) => {
44 | console.log(` - ${result.text.slice(0, 50)}... (score: ${result.score})`);
45 | });
46 |
47 | // Test tags
48 | console.log('5. Getting all tags...');
49 | const tags = await memoryService.getAllTags();
50 | console.log(` Tags: ${tags.join(', ')}`);
51 |
52 | // Test stats
53 | console.log('6. Getting stats...');
54 | const stats = await memoryService.getStats();
55 | console.log(` Total memories: ${stats.totalConsciousMemories}`);
56 | console.log(` Unique tags: ${stats.tagCount}`);
57 | console.log(` Average importance: ${stats.averageImportance}`);
58 | console.log(` Source breakdown:`, stats.sourceBreakdown);
59 |
60 | console.log(' All tests passed!');
61 | return true;
62 |
63 | } catch (error) {
64 | console.error(' Test failed:', error);
65 | return false;
66 | }
67 | }
68 |
69 | // Run the test if this file is executed directly
70 | if (require.main === module) {
71 | testConsciousMemory().then(success => {
72 | process.exit(success ? 0 : 1);
73 | });
74 | }
75 |
76 | export { testConsciousMemory };
77 |
--------------------------------------------------------------------------------
/src/tests/integration-test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Integration Test Suite
3 | * Tests core application functionality and component integration
4 | */
5 |
6 | const path = require('path')
7 | const fs = require('fs')
8 |
9 | const testResults = []
10 |
11 | function runTest(name, testFn, expectedResult = true) {
12 | try {
13 | const result = testFn()
14 | const passed = result === expectedResult
15 | testResults.push({
16 | name,
17 | passed,
18 | message: passed ? 'PASSED' : `FAILED - Expected: ${expectedResult}, Got: ${result}`
19 | })
20 | } catch (error) {
21 | testResults.push({
22 | name,
23 | passed: false,
24 | message: `FAILED - Error: ${error instanceof Error ? error.message : String(error)}`
25 | })
26 | }
27 | }
28 |
29 | function runIntegrationTests() {
30 | console.log(' Running Integration Tests...\n')
31 |
32 | // Test 1: Project structure
33 | runTest('Project structure is valid', () => {
34 | const requiredFiles = [
35 | 'package.json',
36 | 'next.config.js',
37 | 'tailwind.config.js',
38 | 'src/app/page.tsx',
39 | 'src/components/ChatInterface.tsx',
40 | 'src/lib/mcp-manager.ts'
41 | ]
42 |
43 | return requiredFiles.every(file =>
44 | fs.existsSync(path.join(process.cwd(), file))
45 | )
46 | })
47 |
48 | // Test 2: TypeScript configuration
49 | runTest('TypeScript config is valid', () => {
50 | return fs.existsSync('tsconfig.json')
51 | })
52 |
53 | // Test 3: Core components exist
54 | runTest('Core components exist', () => {
55 | const components = [
56 | 'src/components/ChatInterface.tsx',
57 | 'src/components/ChatMessage.tsx',
58 | 'src/components/ToolCallDisplay.tsx',
59 | 'src/components/ChatHistorySidebar.tsx'
60 | ]
61 |
62 | return components.every(component =>
63 | fs.existsSync(path.join(process.cwd(), component))
64 | )
65 | })
66 |
67 | // Test 4: API routes exist
68 | runTest('API routes exist', () => {
69 | const routes = [
70 | 'src/app/api/chat/route.ts',
71 | 'src/app/api/chat-history/route.ts'
72 | ]
73 |
74 | return routes.every(route =>
75 | fs.existsSync(path.join(process.cwd(), route))
76 | )
77 | })
78 |
79 | // Test 5: MCP configuration exists
80 | runTest('MCP configuration exists', () => {
81 | return fs.existsSync('config.json')
82 | })
83 |
84 | // Test 6: Database initialization
85 | runTest('Database structure is set up', () => {
86 | return fs.existsSync('src/lib/chat-history.ts')
87 | })
88 |
89 | // Test 7: Styling configuration
90 | runTest('Styling is configured', () => {
91 | return fs.existsSync('src/app/globals.css') &&
92 | fs.existsSync('postcss.config.js')
93 | })
94 |
95 | // Print results
96 | console.log(' Test Results:')
97 | console.log('================')
98 |
99 | let passedCount = 0
100 | testResults.forEach(result => {
101 | const icon = result.passed ? '' : ''
102 | console.log(`${icon} ${result.name}: ${result.message}`)
103 | if (result.passed) passedCount++
104 | })
105 |
106 | console.log(`\n Summary: ${passedCount}/${testResults.length} tests passed`)
107 |
108 | if (passedCount === testResults.length) {
109 | console.log(' All integration tests passed!')
110 | process.exit(0)
111 | } else {
112 | console.log(' Some tests failed. Please check the issues above.')
113 | process.exit(1)
114 | }
115 | }
116 |
117 | // Run the tests
118 | runIntegrationTests()
119 |
--------------------------------------------------------------------------------
/src/tests/integration-test.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Integration Test Suite
3 | * Tests core application functionality and component integration
4 | */
5 |
6 | const path = require('path')
7 | const fs = require('fs')
8 |
9 | const testResults: Array<{ name: string; passed: boolean; message: string }> = []
10 |
11 | function runTest(name: string, testFn: () => boolean, expectedResult = true) {
12 | try {
13 | const result = testFn()
14 | const passed = result === expectedResult
15 | testResults.push({
16 | name,
17 | passed,
18 | message: passed ? 'PASSED' : `FAILED - Expected: ${expectedResult}, Got: ${result}`
19 | })
20 | } catch (error) {
21 | testResults.push({
22 | name,
23 | passed: false,
24 | message: `FAILED - Error: ${error instanceof Error ? error.message : String(error)}`
25 | })
26 | }
27 | }
28 |
29 | function runIntegrationTests() {
30 | console.log(' Running Integration Tests...\n')
31 |
32 | // Test 1: Project structure
33 | runTest('Project structure is valid', () => {
34 | const requiredFiles = [
35 | 'package.json',
36 | 'next.config.js',
37 | 'tailwind.config.js',
38 | 'src/app/page.tsx',
39 | 'src/components/ChatInterface.tsx',
40 | 'src/lib/mcp-manager.ts'
41 | ]
42 |
43 | return requiredFiles.every(file =>
44 | fs.existsSync(path.join(process.cwd(), file))
45 | )
46 | })
47 |
48 | // Test 2: TypeScript configuration
49 | runTest('TypeScript config is valid', () => {
50 | return fs.existsSync('tsconfig.json')
51 | })
52 |
53 | // Test 3: Core components exist
54 | runTest('Core components exist', () => {
55 | const components = [
56 | 'src/components/ChatInterface.tsx',
57 | 'src/components/ChatMessage.tsx',
58 | 'src/components/ToolCallDisplay.tsx',
59 | 'src/components/ChatHistorySidebar.tsx'
60 | ]
61 |
62 | return components.every(component =>
63 | fs.existsSync(path.join(process.cwd(), component))
64 | )
65 | })
66 |
67 | // Test 4: API routes exist
68 | runTest('API routes exist', () => {
69 | const routes = [
70 | 'src/app/api/chat/route.ts',
71 | 'src/app/api/chat-history/route.ts'
72 | ]
73 |
74 | return routes.every(route =>
75 | fs.existsSync(path.join(process.cwd(), route))
76 | )
77 | })
78 |
79 | // Test 5: MCP configuration exists
80 | runTest('MCP configuration exists', () => {
81 | return fs.existsSync('config.json')
82 | })
83 |
84 | // Test 6: Database initialization
85 | runTest('Database structure is set up', () => {
86 | return fs.existsSync('src/lib/chat-history.ts')
87 | })
88 |
89 | // Test 7: Styling configuration
90 | runTest('Styling is configured', () => {
91 | return fs.existsSync('src/app/globals.css') &&
92 | fs.existsSync('postcss.config.js')
93 | })
94 |
95 | // Print results
96 | console.log(' Test Results:')
97 | console.log('================')
98 |
99 | let passedCount = 0
100 | testResults.forEach(result => {
101 | const icon = result.passed ? '' : ''
102 | console.log(`${icon} ${result.name}: ${result.message}`)
103 | if (result.passed) passedCount++
104 | })
105 |
106 | console.log(`\n Summary: ${passedCount}/${testResults.length} tests passed`)
107 |
108 | if (passedCount === testResults.length) {
109 | console.log(' All integration tests passed!')
110 | process.exit(0)
111 | } else {
112 | console.log(' Some tests failed. Please check the issues above.')
113 | process.exit(1)
114 | }
115 | }
116 |
117 | // Run the tests
118 | runIntegrationTests()
119 |
--------------------------------------------------------------------------------
/src/tests/kg-sync-queue-test.ts:
--------------------------------------------------------------------------------
1 | import { kgSyncQueue } from '../lib/kg-sync-queue';
2 |
3 | async function testQueue() {
4 | console.log(' Testing KG Sync Queue...');
5 |
6 | try {
7 | await kgSyncQueue.initialize();
8 | console.log(' Queue initialized');
9 |
10 | await kgSyncQueue.addSyncRequest('chat', 1);
11 | console.log(' Added chat sync request');
12 |
13 | const size = await kgSyncQueue.getQueueSize();
14 | console.log(' Queue size:', size);
15 |
16 | // Process one item
17 | const processed = await kgSyncQueue.processNext(async (request) => {
18 | console.log(' Processing request:', request.id);
19 | console.log(' Type:', request.type);
20 | console.log(' Priority:', request.priority);
21 | console.log(' Timestamp:', request.timestamp);
22 | // Simulate processing
23 | await new Promise(resolve => setTimeout(resolve, 100));
24 | });
25 |
26 | console.log(' Processed:', processed);
27 |
28 | const finalSize = await kgSyncQueue.getQueueSize();
29 | console.log(' Final queue size:', finalSize);
30 |
31 | console.log(' Test completed successfully!');
32 | } catch (error) {
33 | console.error(' Test failed:', error);
34 | }
35 | }
36 |
37 | testQueue();
38 |
--------------------------------------------------------------------------------
/src/tests/neo4j-advanced-deletion.test.js:
--------------------------------------------------------------------------------
1 | // Advanced deletion test script for Knowledge Graph Service
2 | // Tests session cascade deletion and batch operations
3 |
4 | const { spawn } = require('child_process');
5 | const path = require('path');
6 |
7 | // Set environment variables for the test
8 | process.env.NEO4J_URI = 'bolt://localhost:7687';
9 | process.env.NEO4J_USER = 'neo4j';
10 | process.env.NEO4J_PASSWORD = 'password123';
11 |
12 | async function testAdvancedDeletion() {
13 | console.log(' Testing Advanced Deletion Features...');
14 |
15 | try {
16 | const imported = await import('../lib/knowledge-graph-service.ts');
17 | const kgService = imported.default.default || imported.default;
18 |
19 | console.log(' Connecting to Neo4j...');
20 | await kgService.connect();
21 |
22 | console.log(' Creating test session structure...');
23 |
24 | // Create a session
25 | const session = {
26 | id: 'test-session-1',
27 | type: 'Session',
28 | properties: {
29 | title: 'Test Session for Deletion',
30 | createdAt: new Date().toISOString()
31 | }
32 | };
33 | await kgService.addNode(session);
34 |
35 | // Create messages in the session
36 | const message1 = {
37 | id: 'test-msg-1',
38 | type: 'Message',
39 | properties: {
40 | role: 'user',
41 | content: 'Hello, test message 1',
42 | createdAt: new Date().toISOString()
43 | }
44 | };
45 |
46 | const message2 = {
47 | id: 'test-msg-2',
48 | type: 'Message',
49 | properties: {
50 | role: 'assistant',
51 | content: 'Test response message 2',
52 | createdAt: new Date().toISOString()
53 | }
54 | };
55 |
56 | await kgService.addNode(message1);
57 | await kgService.addNode(message2);
58 |
59 | // Create relationships
60 | await kgService.addRelationship({
61 | sourceNodeId: 'test-session-1',
62 | targetNodeId: 'test-msg-1',
63 | type: 'HAS_MESSAGE',
64 | properties: { order: 1 }
65 | });
66 |
67 | await kgService.addRelationship({
68 | sourceNodeId: 'test-session-1',
69 | targetNodeId: 'test-msg-2',
70 | type: 'HAS_MESSAGE',
71 | properties: { order: 2 }
72 | });
73 |
74 | // Create some test tool invocations
75 | const toolInv = {
76 | id: 'test-tool-inv-1',
77 | type: 'ToolInvocation',
78 | properties: {
79 | toolName: 'test-tool',
80 | args: JSON.stringify({ param: 'value' }),
81 | result: JSON.stringify({ success: true })
82 | }
83 | };
84 |
85 | await kgService.addNode(toolInv);
86 | await kgService.addRelationship({
87 | sourceNodeId: 'test-msg-1',
88 | targetNodeId: 'test-tool-inv-1',
89 | type: 'INVOKES_TOOL',
90 | properties: {}
91 | });
92 |
93 | // Create some orphaned test nodes for cleanup testing
94 | const orphan1 = {
95 | id: 'orphan-1',
96 | type: 'TestOrphan',
97 | properties: { name: 'Orphaned Node 1' }
98 | };
99 |
100 | const orphan2 = {
101 | id: 'orphan-2',
102 | type: 'TestOrphan',
103 | properties: { name: 'Orphaned Node 2' }
104 | };
105 |
106 | await kgService.addNode(orphan1);
107 | await kgService.addNode(orphan2);
108 |
109 | console.log(' Test structure created successfully!');
110 |
111 | console.log(' Testing session cascade deletion...');
112 | const cascadeResult = await kgService.cascadeDeleteSession('test-session-1');
113 | console.log(' Cascade deletion result:', cascadeResult);
114 |
115 | console.log(' Testing orphaned node cleanup...');
116 | const cleanupResult = await kgService.cleanupOrphanedNodes(['Session', 'Message']);
117 | console.log(' Cleanup result:', cleanupResult);
118 |
119 | console.log(' Testing batch deletion by type...');
120 |
121 | // Create some more test nodes to batch delete
122 | const batchTestNodes = [
123 | { id: 'batch-1', type: 'BatchTest', properties: { category: 'test', value: 1 } },
124 | { id: 'batch-2', type: 'BatchTest', properties: { category: 'test', value: 2 } },
125 | { id: 'batch-3', type: 'BatchTest', properties: { category: 'prod', value: 3 } }
126 | ];
127 |
128 | for (const node of batchTestNodes) {
129 | await kgService.addNode(node);
130 | }
131 |
132 | // Delete only test category nodes
133 | const batchDeleteResult = await kgService.deleteNodesByType('BatchTest', { category: 'test' });
134 | console.log(' Batch deletion result (test category only):', batchDeleteResult);
135 |
136 | // Clean up remaining batch test nodes
137 | const remainingBatchResult = await kgService.deleteNodesByType('BatchTest');
138 | console.log(' Remaining batch nodes cleanup:', remainingBatchResult);
139 |
140 | console.log(' Final verification - checking for any remaining test data...');
141 | const remainingNodes = await kgService.runQuery(`
142 | MATCH (n)
143 | WHERE n.id STARTS WITH 'test-' OR n.id STARTS WITH 'batch-' OR n.id STARTS WITH 'orphan-'
144 | RETURN count(n) as remainingCount, collect(DISTINCT labels(n)[0]) as types
145 | `);
146 |
147 | console.log(' Final check result:', remainingNodes[0]);
148 |
149 | console.log(' Closing connection...');
150 | await kgService.close();
151 |
152 | console.log('\n All advanced deletion tests passed! The system properly handles:');
153 | console.log(' Session cascade deletion with all related data');
154 | console.log(' Orphaned node cleanup with type exclusions');
155 | console.log(' Batch deletion by type with property filters');
156 | console.log(' Comprehensive relationship and dependency management');
157 |
158 | } catch (error) {
159 | console.error(' Advanced deletion test failed:', error.message);
160 | console.error('Stack trace:', error.stack);
161 | process.exit(1);
162 | }
163 | }
164 |
165 | // Run the test
166 | testAdvancedDeletion();
167 |
--------------------------------------------------------------------------------
/src/tests/neo4j-integration.test.js:
--------------------------------------------------------------------------------
1 | // Quick test script to verify Neo4j integration
2 | // This will test the basic connectivity and functionality
3 |
4 | const { spawn } = require('child_process');
5 | const path = require('path');
6 |
7 | // Set environment variables for the test
8 | process.env.NEO4J_URI = 'bolt://localhost:7687';
9 | process.env.NEO4J_USER = 'neo4j';
10 | process.env.NEO4J_PASSWORD = 'password123';
11 |
12 | async function testNeo4jIntegration() {
13 | console.log(' Testing Neo4j Integration...'); try { // Import and test the knowledge graph service
14 | const imported = await import('../lib/knowledge-graph-service.ts');
15 | const kgService = imported.default.default || imported.default;
16 |
17 | console.log(' Testing Neo4j connection...');
18 | await kgService.connect();
19 | console.log(' Neo4j connection successful!');
20 |
21 | console.log(' Testing health check...');
22 | const isHealthy = await kgService.healthCheck();
23 | console.log(isHealthy ? ' Health check passed!' : ' Health check failed');
24 |
25 | console.log(' Testing basic query...');
26 | const result = await kgService.runQuery('RETURN "Hello Neo4j!" as greeting, datetime() as timestamp');
27 | console.log(' Query result:', result);
28 | console.log(' Testing node creation...');
29 | const testNode = {
30 | id: 'test-node-1',
31 | type: 'TestNode',
32 | properties: {
33 | name: 'Integration Test Node',
34 | description: 'Created during integration testing',
35 | timestamp: new Date().toISOString(), // Already a string, but good to test
36 | createdDate: new Date(), // This will test Date sanitization
37 | metadata: { complex: 'object', nested: { data: true } }, // Test object serialization
38 | tags: ['test', 'integration', new Date()], // Test array with mixed types
39 | score: 95.5,
40 | isActive: true,
41 | nullValue: null
42 | }
43 | };
44 |
45 | await kgService.addNode(testNode);
46 | console.log(' Test node created successfully!');
47 |
48 | console.log(' Testing relationship creation...');
49 | const testNode2 = {
50 | id: 'test-node-2',
51 | type: 'TestNode',
52 | properties: {
53 | name: 'Second Test Node',
54 | timestamp: new Date().toISOString()
55 | }
56 | };
57 |
58 | await kgService.addNode(testNode2);
59 |
60 | const testRelationship = {
61 | sourceNodeId: 'test-node-1',
62 | targetNodeId: 'test-node-2',
63 | type: 'RELATES_TO',
64 | properties: {
65 | strength: 0.8,
66 | createdAt: new Date(),
67 | metadata: { type: 'test_relationship' },
68 | tags: ['test', new Date()]
69 | }
70 | };
71 |
72 | await kgService.addRelationship(testRelationship);
73 | console.log(' Test relationship created successfully!');
74 |
75 | console.log(' Testing complex query parameters...');
76 | const complexQueryResult = await kgService.runQuery(
77 | `MATCH (n:TestNode {id: $nodeId})
78 | WHERE n.score > $minScore AND n.isActive = $isActive
79 | RETURN n, $queryTime as queryTime, $metadata as metadata`,
80 | {
81 | nodeId: 'test-node-1',
82 | minScore: 90,
83 | isActive: true,
84 | queryTime: new Date(),
85 | metadata: { query: 'complex test', nested: { data: [1, 2, 3] } }
86 | }
87 | );
88 | console.log(' Complex query with sanitized parameters successful!');
89 | console.log(' Result count:', complexQueryResult.length);
90 |
91 | console.log(' Verifying node exists...');
92 | const nodeQuery = await kgService.runQuery(
93 | 'MATCH (n:TestNode {id: $id}) RETURN n',
94 | { id: 'test-node-1' }
95 | );
96 | console.log(' Node verification:', nodeQuery.length > 0 ? 'Found!' : 'Not found'); console.log(' Testing enhanced deletion capabilities...');
97 |
98 | // Test dependency checking
99 | console.log(' Testing dependency checking...');
100 | const deps = await kgService.checkNodeDependencies('test-node-1');
101 | console.log(' Dependency check result:', deps);
102 |
103 | // Test relationship deletion
104 | console.log(' Testing relationship deletion...');
105 | const relDeleteResult = await kgService.deleteRelationship('test-node-1', 'test-node-2', 'RELATES_TO');
106 | console.log(' Relationship deletion result:', relDeleteResult);
107 |
108 | // Test safe node deletion (should work now that relationship is gone)
109 | console.log(' Testing safe node deletion...');
110 | const safeDeleteResult = await kgService.deleteNode('test-node-2', {
111 | nodeType: 'TestNode',
112 | skipDependencyCheck: false
113 | });
114 | console.log(' Safe deletion result:', safeDeleteResult);
115 |
116 | // Test node deletion with cascade
117 | console.log(' Testing cascade deletion...');
118 | const cascadeDeleteResult = await kgService.deleteNode('test-node-1', {
119 | nodeType: 'TestNode',
120 | cascadeDelete: true
121 | });
122 | console.log(' Cascade deletion result:', cascadeDeleteResult);
123 |
124 | console.log(' Enhanced deletion testing completed!');
125 |
126 | console.log(' Closing connection...');
127 | await kgService.close();
128 | console.log(' Connection closed.');
129 |
130 | console.log('\n All tests passed! Neo4j integration is working correctly.');
131 |
132 | } catch (error) {
133 | console.error(' Integration test failed:', error.message);
134 | console.error('Stack trace:', error.stack);
135 | process.exit(1);
136 | }
137 | }
138 |
139 | // Run the test
140 | testNeo4jIntegration();
141 |
--------------------------------------------------------------------------------
/src/tests/neo4j-sync-test.ts:
--------------------------------------------------------------------------------
1 | import knowledgeGraphSyncService from '../lib/knowledge-graph-sync-service';
2 | import knowledgeGraphService from '../lib/knowledge-graph-service';
3 |
4 | async function runSyncTest() {
5 | console.log(' Starting Neo4j Sync Test...\n');
6 |
7 | try {
8 | // Test 1: Connection health check
9 | console.log('Test 1: Checking Neo4j connection...');
10 | const isHealthy = await knowledgeGraphService.healthCheck();
11 | console.log(` Neo4j connection: ${isHealthy ? 'Healthy' : 'Failed'}\n`);
12 |
13 | // Test 2: Get initial statistics
14 | console.log('Test 2: Getting initial statistics...');
15 | await knowledgeGraphSyncService.logStartupStatistics();
16 |
17 | // Test 3: Run incremental sync
18 | console.log('\nTest 3: Running incremental sync...');
19 | await knowledgeGraphSyncService.syncKnowledgeGraph({ forceFullResync: false });
20 |
21 | // Test 4: Verify final statistics
22 | console.log('\nTest 4: Getting final statistics...');
23 | const finalStats = await knowledgeGraphService.getStatistics();
24 | console.log('Final statistics:', finalStats);
25 |
26 | // Test 5: Test batch operations
27 | console.log('\nTest 5: Testing batch operations...');
28 | const testNodes = Array.from({ length: 5 }, (_, i) => ({
29 | id: `test-node-${Date.now()}-${i}`,
30 | type: 'TestNode',
31 | properties: { name: `Test Node ${i}`, testRun: true },
32 | createdAt: new Date()
33 | }));
34 |
35 | const batchResult = await knowledgeGraphService.addNodesBatch(testNodes);
36 | console.log(`Batch insert result: ${batchResult.succeeded} succeeded, ${batchResult.failed} failed`);
37 |
38 | // Cleanup test nodes
39 | for (const node of testNodes) {
40 | await knowledgeGraphService.deleteNode(node.id);
41 | }
42 |
43 | console.log('\n All tests completed successfully!');
44 | } catch (error) {
45 | console.error(' Test failed:', error);
46 | } finally {
47 | await knowledgeGraphService.close();
48 | }
49 | }
50 |
51 | // Run the test
52 | if (require.main === module) {
53 | runSyncTest().catch(console.error);
54 | }
55 |
--------------------------------------------------------------------------------
/src/types/chat.ts:
--------------------------------------------------------------------------------
1 | export interface FileAttachment {
2 | id: string
3 | name: string
4 | type: string // MIME type
5 | size: number
6 | data: string // Base64 encoded file data
7 | uploadedAt: Date
8 | }
9 |
10 | export interface Message {
11 | role: 'user' | 'assistant'
12 | content: string
13 | timestamp: Date
14 | attachments?: FileAttachment[]
15 | }
16 |
17 | export interface ChatRequest {
18 | message: string
19 | attachments?: FileAttachment[]
20 | }
21 |
22 | export interface ToolCall {
23 | toolName: string
24 | args: Record
25 | result?: any
26 | }
27 |
28 | export interface ChatResponse {
29 | message: string
30 | timestamp: string
31 | }
--------------------------------------------------------------------------------
/src/types/knowledge-graph.ts:
--------------------------------------------------------------------------------
1 | export interface KgNode {
2 | id: string;
3 | type: string; // e.g., 'Person', 'Organization', 'Memory', 'Concept'
4 | properties: Record; // Flexible properties
5 | createdAt?: Date; // Optional, can be set by the service
6 | updatedAt?: Date; // Optional, can be set by the service
7 | }
8 |
9 | // You can add other related types here as they become necessary,
10 | // for example, for relationships:
11 | export interface KgRelationship {
12 | id?: string; // Optional, Neo4j can auto-generate
13 | type: string; // e.g., 'WORKS_FOR', 'RELATED_TO'
14 | sourceNodeId: string;
15 | targetNodeId: string;
16 | properties?: Record;
17 | createdAt?: Date;
18 | updatedAt?: Date;
19 | }
20 |
--------------------------------------------------------------------------------
/src/types/mcp.ts:
--------------------------------------------------------------------------------
1 | import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2 | import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
3 |
4 | export interface MCPServerConfig {
5 | name: string;
6 | url?: string;
7 | type: 'http' | 'stdio';
8 | command?: string;
9 | args?: string[];
10 | env?: { [key: string]: string };
11 | }
12 |
13 | export class MCPClient {
14 | private client: Client | null = null;
15 | private isConnected = false;
16 |
17 | constructor(private name: string, private serverUrl?: string) {}
18 |
19 | async connect(url?: string) {
20 | // Use environment variable as fallback if url is not provided
21 | const serverUrl = url || this.serverUrl || `http://localhost:${process.env.MCP_SERVER_PORT || 8081}`;
22 | const transport = new StreamableHTTPClientTransport(new URL(serverUrl));
23 | this.client = new Client({ name: this.name || 'LocalChatClient', version: '1.0.0' });
24 | await this.client.connect(transport);
25 | this.isConnected = true;
26 | }
27 |
28 | async callTool(name: string, args: any) {
29 | if (!this.client || !this.isConnected) {
30 | throw new Error('Client not connected');
31 | }
32 | return await this.client.callTool({ name, arguments: args });
33 | }
34 |
35 | async disconnect() {
36 | if (this.client && this.isConnected) {
37 | await this.client.close();
38 | this.isConnected = false;
39 | }
40 | }
41 |
42 | get connected(): boolean {
43 | return this.isConnected;
44 | }
45 | }
--------------------------------------------------------------------------------
/src/types/memory.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Type definitions for the RAG memory system
3 | */
4 |
5 | export interface Memory {
6 | id: string;
7 | text: string;
8 | embedding: number[];
9 | metadata: MemoryMetadata;
10 | timestamp: number; // Changed from string to number (epoch ms)
11 | }
12 |
13 | export interface MemoryMetadata {
14 | sessionId: string;
15 | timestamp: number; // Changed from string to number (epoch ms)
16 | messageType: 'user' | 'assistant';
17 | textLength: number;
18 | [key: string]: any; // Allow additional metadata
19 | }
20 |
21 | export interface MemoryRetrievalResult {
22 | id: string;
23 | text: string;
24 | score: number;
25 | metadata: MemoryMetadata;
26 | embedding?: number[];
27 | }
28 |
29 | export interface MemorySearchOptions {
30 | limit?: number;
31 | sessionId?: string;
32 | minScore?: number;
33 | messageType?: 'user' | 'assistant' | 'both';
34 | }
35 |
36 | export interface MemoryStoreConfig {
37 | chromaUrl?: string;
38 | chromaCollection?: string;
39 | defaultLimit?: number;
40 | defaultMinScore?: number;
41 | }
42 |
43 | export interface EmbeddingService {
44 | generateEmbedding(text: string): Promise;
45 | getDimensions(): number;
46 | isReady(): boolean;
47 | }
48 |
49 | export interface MemoryStore {
50 | initialize(): Promise;
51 | storeMemory(text: string, metadata: MemoryMetadata, id?: string): Promise;
52 | retrieveMemories(query: string, options?: MemorySearchOptions): Promise;
53 | getMemoryById(id: string): Promise;
54 | getMemoryCount(): Promise;
55 | healthCheck(): Promise;
56 | }
57 |
58 | // === Conscious Memory Types ===
59 |
60 | export interface ConsciousMemory {
61 | id: string;
62 | content: string;
63 | tags: string[];
64 | importance: number; // 1-10 scale
65 | source: 'explicit' | 'suggested' | 'derived';
66 | context?: string; // Surrounding conversation context
67 | metadata: ConsciousMemoryMetadata;
68 | createdAt: number; // Changed from string to number (epoch ms)
69 | updatedAt?: number; // Changed from string to number (epoch ms)
70 | }
71 |
72 | export interface ConsciousMemoryMetadata extends MemoryMetadata {
73 | memoryType: 'conscious';
74 | tags: string[];
75 | importance: number;
76 | source: 'explicit' | 'suggested' | 'derived';
77 | relatedMemoryIds?: string[];
78 | context?: string;
79 | }
80 |
81 | export interface ConsciousMemorySearchOptions extends MemorySearchOptions {
82 | tags?: string[];
83 | importanceMin?: number;
84 | importanceMax?: number;
85 | source?: 'explicit' | 'suggested' | 'derived';
86 | includeRelated?: boolean;
87 | consciousOnly?: boolean; // If true, only search explicit conscious memories
88 | startDate?: string; // ISO date string
89 | endDate?: string; // ISO date string
90 | page?: number; // Page number (1-based)
91 | pageSize?: number; // Results per page
92 | }
93 |
94 | export interface ConsciousMemorySearchResult extends MemoryRetrievalResult {
95 | tags: string[];
96 | importance: number;
97 | source: 'explicit' | 'suggested' | 'derived';
98 | context?: string;
99 | relatedMemoryIds?: string[];
100 | }
101 |
102 | // New interface for paginated results
103 | export interface PaginatedMemorySearchResult {
104 | results: ConsciousMemorySearchResult[];
105 | pagination: {
106 | page: number;
107 | pageSize: number;
108 | totalResults: number;
109 | totalPages: number;
110 | hasNext: boolean;
111 | hasPrevious: boolean;
112 | };
113 | timeRange: {
114 | startDate?: string;
115 | endDate?: string;
116 | actualStartDate?: string; // Earliest memory found
117 | actualEndDate?: string; // Latest memory found
118 | };
119 | }
120 |
121 | export interface MemorySaveRequest {
122 | content: string;
123 | tags?: string[];
124 | importance?: number;
125 | source?: 'explicit' | 'suggested' | 'derived';
126 | context?: string;
127 | sessionId?: string;
128 | relatedMemoryIds?: string[];
129 | }
130 |
131 | export interface MemoryUpdateRequest {
132 | id: string;
133 | content?: string;
134 | tags?: string[];
135 | importance?: number;
136 | context?: string;
137 | relatedMemoryIds?: string[];
138 | }
139 |
140 | export interface ConsciousMemoryStats {
141 | totalConsciousMemories: number;
142 | tagCount: number;
143 | averageImportance: number;
144 | sourceBreakdown: Record;
145 | }
146 |
147 | export interface ConsciousMemoryService {
148 | initialize(): Promise;
149 | saveMemory(request: MemorySaveRequest): Promise;
150 | searchMemories(query: string, options?: ConsciousMemorySearchOptions): Promise;
151 | searchMemoriesByTimeRange(query: string, options?: ConsciousMemorySearchOptions): Promise;
152 | updateMemory(request: MemoryUpdateRequest): Promise;
153 | deleteMemory(id: string): Promise;
154 | deleteMultipleMemories(ids: string[]): Promise;
155 | clearAllMemories(): Promise;
156 | getAllTags(): Promise;
157 | getRelatedMemories(id: string, limit?: number): Promise;
158 | getStats(): Promise;
159 | healthCheck(): Promise;
160 | testMemorySystem(): Promise;
161 | }
162 |
--------------------------------------------------------------------------------
/src/types/motive-force-graph.ts:
--------------------------------------------------------------------------------
1 | import { BaseMessage } from "@langchain/core/messages";
2 |
3 | // Core types for the LangGraph-powered MotiveForce system
4 |
5 | // NEW: Interface for Problem Resolution Details
6 | export interface ProblemResolutionPurpose {
7 | type: 'failure' | 'major_error' | 'loop';
8 | details: string;
9 | actionSuggestion: string;
10 | }
11 |
12 | export interface SubGoal {
13 | id: string;
14 | description: string;
15 | status: 'pending' | 'in_progress' | 'completed' | 'failed' | 'skipped';
16 | priority: number; // 1-10, higher is more important
17 | dependencies: string[]; // IDs of other subgoals this depends on
18 | estimatedDuration?: number; // in minutes
19 | actualDuration?: number; // in minutes
20 | startedAt?: Date;
21 | completedAt?: Date;
22 | failureReason?: string;
23 | tools?: string[]; // Tools required for this subgoal
24 | }
25 |
26 | export interface ExecutionStep {
27 | id: string;
28 | subgoalId: string;
29 | action: 'tool_call' | 'memory_search' | 'user_interaction' | 'analysis' | 'planning';
30 | description: string;
31 | toolName?: string;
32 | toolArgs?: Record;
33 | status: 'pending' | 'executing' | 'completed' | 'failed';
34 | result?: any;
35 | error?: string;
36 | executedAt?: Date;
37 | duration?: number; // in milliseconds
38 | }
39 |
40 | export interface ToolResult {
41 | stepId: string;
42 | toolName: string;
43 | args: Record;
44 | result: any;
45 | error?: string;
46 | duration: number; // in milliseconds
47 | timestamp: Date;
48 | success: boolean;
49 | }
50 |
51 | export interface Memory {
52 | id: string;
53 | content: string;
54 | type: 'conscious' | 'rag' | 'knowledge_graph';
55 | relevanceScore?: number;
56 | retrievedAt: Date;
57 | source?: string;
58 | }
59 |
60 | export interface Reflection {
61 | id: string;
62 | type: 'success' | 'failure' | 'insight' | 'strategy_adjustment';
63 | content: string;
64 | relatedSubgoalId?: string;
65 | relatedStepId?: string;
66 | timestamp: Date;
67 | impact: 'low' | 'medium' | 'high';
68 | actionable?: boolean;
69 | }
70 |
71 | export interface UserPreference {
72 | key: string;
73 | value: any;
74 | source: 'explicit' | 'inferred' | 'default';
75 | confidence: number; // 0-1
76 | lastUpdated: Date;
77 | }
78 |
79 | export interface SessionMetadata {
80 | sessionId: string;
81 | threadId: string;
82 | startedAt: Date;
83 | lastActiveAt: Date;
84 | totalSteps: number;
85 | completedSteps: number;
86 | failedSteps: number;
87 | totalDuration: number; // in milliseconds
88 | pausedDuration: number; // in milliseconds
89 | userInteractions: number;
90 | toolCalls: number;
91 | memoryRetrievals: number;
92 | }
93 |
94 | export interface MotiveForceGraphState {
95 | // Core conversation
96 | messages: BaseMessage[];
97 |
98 | // Purpose and planning
99 | currentPurpose: string;
100 | purposeType: 'research' | 'productivity' | 'learning' | 'creative' | 'analysis' | 'maintenance' | 'custom';
101 | subgoals: SubGoal[];
102 | executionPlan: ExecutionStep[];
103 |
104 | // Execution tracking
105 | currentStepId?: string;
106 | toolResults: ToolResult[];
107 | contextualMemories: Memory[];
108 |
109 | // Learning and adaptation
110 | reflections: Reflection[];
111 | userPreferences: UserPreference[];
112 |
113 | // Session management
114 | sessionMetadata: SessionMetadata;
115 |
116 | // Control flags
117 | emergencyStop: boolean;
118 | needsUserInput: boolean;
119 | waitingForUser: boolean;
120 | isPaused: boolean;
121 | motiveForceOff?: boolean; // NEW: Flag to indicate Motive Force is shutting down
122 |
123 | // Progress tracking
124 | overallProgress: number; // 0-100
125 | lastProgressUpdate: Date;
126 | blockers: string[];
127 |
128 | // Context and state for current workflow/detection
129 | workingMemory: {
130 | [key: string]: any; // Allow dynamic properties
131 | motiveForceQueryGenerated?: boolean;
132 | generatedQuery?: string;
133 | detectingProblems?: boolean; // Set to true when entering detection phase
134 | nextDetector?: MotiveForceRoute; // Next detection node to hit
135 | failureAddressed?: boolean; // Flag to indicate a failure was handled
136 | majorErrorAddressed?: boolean; // Flag to indicate a major error was handled
137 | loopAddressed?: boolean; // Flag to indicate a loop was handled
138 | motiveForceShutdown?: boolean; // Flag for graceful shutdown message
139 | problemResolutionPurpose?: ProblemResolutionPurpose; // NEW: Problem details for query generation
140 | triggerImmediateQueryGeneration?: boolean; // NEW: Flag to force immediate problem query
141 | nextActionIsInvestigativeTool?: { toolName: string; args: Record; }; // For detectFailureNode
142 | };
143 | persistentContext: Record; // Data that survives sessions
144 |
145 | // Error handling
146 | errorCount: number;
147 | lastError?: string;
148 | retryCount: number;
149 |
150 | // Performance metrics
151 | averageStepDuration: number;
152 | successRate: number;
153 | toolEfficiency: Record; // Tool name -> efficiency score
154 |
155 | // Problem Detection Report (NEW)
156 | lastDetectionReport?: {
157 | type: 'failure' | 'major_error' | 'loop';
158 | details: string;
159 | actionSuggestion: string; // Corrective action suggested by detector
160 | timestamp: Date;
161 | confidence?: number;
162 | };
163 |
164 | // For passing specific message subsets to detection nodes if not using direct slice/context building per node
165 | messagesForDetection?: BaseMessage[];
166 | detectionContextType?: 'single' | 'last2' | 'last3'; // To control context for detectors
167 | }
168 |
169 | // Node-specific state updates
170 | export interface PurposeAnalysisResult {
171 | purpose: string;
172 | purposeType: MotiveForceGraphState['purposeType'];
173 | complexity: 'simple' | 'moderate' | 'complex' | 'very_complex';
174 | estimatedDuration: number; // in minutes
175 | confidence: number; // 0-1
176 | clarificationsNeeded: string[];
177 | }
178 |
179 | export interface PlanGenerationResult {
180 | subgoals: SubGoal[];
181 | executionPlan: ExecutionStep[];
182 | riskAssessment: {
183 | level: 'low' | 'medium' | 'high';
184 | factors: string[];
185 | mitigations: string[];
186 | };
187 | resourceRequirements: {
188 | toolsNeeded: string[];
189 | memoryTypes: string[];
190 | estimatedTokens: number;
191 | estimatedTime: number;
192 | };
193 | }
194 |
195 | export interface ContextGatheringResult {
196 | memories: Memory[];
197 | relevantPreferences: UserPreference[];
198 | knowledgeGaps: string[];
199 | contextCompleteness: number; // 0-1
200 | }
201 |
202 | export interface ProgressMonitoringResult {
203 | overallProgress: number;
204 | completedSubgoals: number;
205 | failedSubgoals: number;
206 | blockers: string[];
207 | nextSteps: string[];
208 | shouldContinue: boolean;
209 | needsUserInput: boolean;
210 | adaptationsNeeded: string[];
211 | }
212 |
213 | export interface ReflectionResult {
214 | reflections: Reflection[];
215 | strategicInsights: string[];
216 | performanceMetrics: {
217 | efficiency: number;
218 | effectiveness: number;
219 | userSatisfaction?: number;
220 | };
221 | recommendedAdjustments: {
222 | planChanges: string[];
223 | toolUsageOptimizations: string[];
224 | timelineAdjustments: string[];
225 | };
226 | }
227 |
228 | // Routing decision types
229 | export type MotiveForceRoute =
230 | | 'purpose_analyzer'
231 | | 'plan_generator'
232 | | 'context_gatherer'
233 | | 'tool_orchestrator'
234 | | 'progress_monitor'
235 | | 'reflection_engine'
236 | | 'user_checkin'
237 | | 'query_generator'
238 | | 'prepare_detection_input'
239 | | 'detection_orchestrator'
240 | | 'detect_failure'
241 | | 'detect_major_error'
242 | | 'detect_loop'
243 | | 'handle_failure'
244 | | 'handle_major_error'
245 | | 'handle_loop'
246 | | '__end__';
247 |
248 | // Configuration types
249 | export interface MotiveForceGraphConfig {
250 | maxStepsPerSession: number;
251 | maxDurationMinutes: number;
252 | userCheckinInterval: number; // in steps
253 | errorThreshold: number;
254 | retryLimit: number;
255 | memoryRetrievalLimit: number;
256 | parallelToolExecution: boolean;
257 | aggressiveness: 'conservative' | 'balanced' | 'aggressive';
258 | purposeTypes: string[];
259 | enableLearning: boolean;
260 | enableUserCheckins: boolean;
261 | }
262 |
263 | // Event types for streaming
264 | export interface MotiveForceEvent {
265 | type: 'step_start' | 'step_complete' | 'step_error' | 'progress_update' | 'user_input_needed' | 'reflection' | 'plan_update';
266 | stepId?: string;
267 | nodeType?: string;
268 | data: any;
269 | timestamp: Date;
270 | }
271 |
272 | // Integration with existing MotiveForce types
273 | export interface LegacyMotiveForceConfig {
274 | enabled: boolean;
275 | delayBetweenTurns: number;
276 | maxConsecutiveTurns: number;
277 | temperature: number;
278 | historyDepth: number;
279 | useRag: boolean;
280 | useConsciousMemory: boolean;
281 | mode: 'aggressive' | 'balanced' | 'conservative';
282 | }
283 |
284 | // Migration utility types
285 | export interface MotiveForceMode {
286 | useGraph: boolean;
287 | legacyConfig?: LegacyMotiveForceConfig;
288 | graphConfig?: MotiveForceGraphConfig;
289 | }
290 |
--------------------------------------------------------------------------------
/src/types/motive-force.ts:
--------------------------------------------------------------------------------
1 | export interface MotiveForceConfig {
2 | enabled: boolean;
3 | delayBetweenTurns: number; // milliseconds
4 | maxConsecutiveTurns: number;
5 | temperature: number;
6 | historyDepth: number; // number of messages to include
7 | useRag: boolean;
8 | useConsciousMemory: boolean;
9 | mode: 'aggressive' | 'balanced' | 'conservative';
10 | provider?: any;
11 | model?: string;
12 | }
13 |
14 | export interface MotiveForceState {
15 | enabled: boolean;
16 | isGenerating: boolean;
17 | currentTurn: number;
18 | lastGeneratedAt?: Date;
19 | errorCount: number;
20 | sessionId?: string;
21 | }
22 |
23 | export interface MotiveForceMessage {
24 | id: string;
25 | sessionId: string;
26 | query: string;
27 | generatedAt: Date;
28 | turn: number;
29 | }
30 |
31 | export const DEFAULT_MOTIVE_FORCE_CONFIG: MotiveForceConfig = {
32 | enabled: false,
33 | delayBetweenTurns: 2000,
34 | maxConsecutiveTurns: 10,
35 | temperature: 0.8,
36 | historyDepth: 5,
37 | useRag: true,
38 | useConsciousMemory: true,
39 | mode: 'balanced'
40 | };
41 |
--------------------------------------------------------------------------------
/src/types/tool.ts:
--------------------------------------------------------------------------------
1 | export interface MCPTool {
2 | name: string
3 | mcpToolName: string
4 | description: string
5 | parameters: any
6 | execute: (args: any) => Promise
7 | }
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}',
5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}',
6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}',
7 | ],
8 | theme: {
9 | extend: {
10 | backgroundImage: {
11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
12 | 'gradient-conic':
13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
14 | },
15 | },
16 | },
17 | plugins: [],
18 | }
19 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": [
4 | "dom",
5 | "dom.iterable",
6 | "es6"
7 | ],
8 | "allowJs": true,
9 | "skipLibCheck": true,
10 | "strict": true,
11 | "noEmit": true,
12 | "esModuleInterop": true,
13 | "module": "esnext",
14 | "moduleResolution": "bundler",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "jsx": "preserve",
18 | "incremental": true,
19 | "plugins": [
20 | {
21 | "name": "next"
22 | }
23 | ],
24 | "baseUrl": ".",
25 | "paths": {
26 | "@/*": [
27 | "./src/*"
28 | ]
29 | },
30 | "target": "ES2017"
31 | },
32 | "include": [
33 | "next-env.d.ts",
34 | "**/*.ts",
35 | "**/*.tsx",
36 | ".next/types/**/*.ts"
37 | ],
38 | "exclude": [
39 | "node_modules"
40 | ]
41 | }
42 |
--------------------------------------------------------------------------------