nc1709 1.15.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. nc1709/__init__.py +13 -0
  2. nc1709/agent/__init__.py +36 -0
  3. nc1709/agent/core.py +505 -0
  4. nc1709/agent/mcp_bridge.py +245 -0
  5. nc1709/agent/permissions.py +298 -0
  6. nc1709/agent/tools/__init__.py +21 -0
  7. nc1709/agent/tools/base.py +440 -0
  8. nc1709/agent/tools/bash_tool.py +367 -0
  9. nc1709/agent/tools/file_tools.py +454 -0
  10. nc1709/agent/tools/notebook_tools.py +516 -0
  11. nc1709/agent/tools/search_tools.py +322 -0
  12. nc1709/agent/tools/task_tool.py +284 -0
  13. nc1709/agent/tools/web_tools.py +555 -0
  14. nc1709/agents/__init__.py +17 -0
  15. nc1709/agents/auto_fix.py +506 -0
  16. nc1709/agents/test_generator.py +507 -0
  17. nc1709/checkpoints.py +372 -0
  18. nc1709/cli.py +3380 -0
  19. nc1709/cli_ui.py +1080 -0
  20. nc1709/cognitive/__init__.py +149 -0
  21. nc1709/cognitive/anticipation.py +594 -0
  22. nc1709/cognitive/context_engine.py +1046 -0
  23. nc1709/cognitive/council.py +824 -0
  24. nc1709/cognitive/learning.py +761 -0
  25. nc1709/cognitive/router.py +583 -0
  26. nc1709/cognitive/system.py +519 -0
  27. nc1709/config.py +155 -0
  28. nc1709/custom_commands.py +300 -0
  29. nc1709/executor.py +333 -0
  30. nc1709/file_controller.py +354 -0
  31. nc1709/git_integration.py +308 -0
  32. nc1709/github_integration.py +477 -0
  33. nc1709/image_input.py +446 -0
  34. nc1709/linting.py +519 -0
  35. nc1709/llm_adapter.py +667 -0
  36. nc1709/logger.py +192 -0
  37. nc1709/mcp/__init__.py +18 -0
  38. nc1709/mcp/client.py +370 -0
  39. nc1709/mcp/manager.py +407 -0
  40. nc1709/mcp/protocol.py +210 -0
  41. nc1709/mcp/server.py +473 -0
  42. nc1709/memory/__init__.py +20 -0
  43. nc1709/memory/embeddings.py +325 -0
  44. nc1709/memory/indexer.py +474 -0
  45. nc1709/memory/sessions.py +432 -0
  46. nc1709/memory/vector_store.py +451 -0
  47. nc1709/models/__init__.py +86 -0
  48. nc1709/models/detector.py +377 -0
  49. nc1709/models/formats.py +315 -0
  50. nc1709/models/manager.py +438 -0
  51. nc1709/models/registry.py +497 -0
  52. nc1709/performance/__init__.py +343 -0
  53. nc1709/performance/cache.py +705 -0
  54. nc1709/performance/pipeline.py +611 -0
  55. nc1709/performance/tiering.py +543 -0
  56. nc1709/plan_mode.py +362 -0
  57. nc1709/plugins/__init__.py +17 -0
  58. nc1709/plugins/agents/__init__.py +18 -0
  59. nc1709/plugins/agents/django_agent.py +912 -0
  60. nc1709/plugins/agents/docker_agent.py +623 -0
  61. nc1709/plugins/agents/fastapi_agent.py +887 -0
  62. nc1709/plugins/agents/git_agent.py +731 -0
  63. nc1709/plugins/agents/nextjs_agent.py +867 -0
  64. nc1709/plugins/base.py +359 -0
  65. nc1709/plugins/manager.py +411 -0
  66. nc1709/plugins/registry.py +337 -0
  67. nc1709/progress.py +443 -0
  68. nc1709/prompts/__init__.py +22 -0
  69. nc1709/prompts/agent_system.py +180 -0
  70. nc1709/prompts/task_prompts.py +340 -0
  71. nc1709/prompts/unified_prompt.py +133 -0
  72. nc1709/reasoning_engine.py +541 -0
  73. nc1709/remote_client.py +266 -0
  74. nc1709/shell_completions.py +349 -0
  75. nc1709/slash_commands.py +649 -0
  76. nc1709/task_classifier.py +408 -0
  77. nc1709/version_check.py +177 -0
  78. nc1709/web/__init__.py +8 -0
  79. nc1709/web/server.py +950 -0
  80. nc1709/web/templates/index.html +1127 -0
  81. nc1709-1.15.4.dist-info/METADATA +858 -0
  82. nc1709-1.15.4.dist-info/RECORD +86 -0
  83. nc1709-1.15.4.dist-info/WHEEL +5 -0
  84. nc1709-1.15.4.dist-info/entry_points.txt +2 -0
  85. nc1709-1.15.4.dist-info/licenses/LICENSE +9 -0
  86. nc1709-1.15.4.dist-info/top_level.txt +1 -0
nc1709/web/server.py ADDED
@@ -0,0 +1,950 @@
1
+ """
2
+ NC1709 Web Dashboard Server
3
+ FastAPI-based local web server for the dashboard
4
+ """
5
+ import asyncio
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ from typing import Dict, Any, Optional, List
10
+ from datetime import datetime
11
+
12
+ from fastapi import FastAPI, HTTPException, WebSocket, WebSocketDisconnect, Header, Depends
13
+ from fastapi.staticfiles import StaticFiles
14
+ from fastapi.responses import HTMLResponse, FileResponse
15
+ from fastapi.middleware.cors import CORSMiddleware
16
+ from pydantic import BaseModel
17
+
18
+ # Get the directory where this file is located
19
+ WEB_DIR = Path(__file__).parent
20
+ STATIC_DIR = WEB_DIR / "static"
21
+ TEMPLATES_DIR = WEB_DIR / "templates"
22
+
23
+
24
+ class ChatMessage(BaseModel):
25
+ """Chat message model"""
26
+ role: str # "user" or "assistant"
27
+ content: str
28
+ timestamp: Optional[str] = None
29
+
30
+
31
+ class ChatRequest(BaseModel):
32
+ """Chat request model"""
33
+ message: str
34
+ session_id: Optional[str] = None
35
+
36
+
37
+ class PluginActionRequest(BaseModel):
38
+ """Plugin action request model"""
39
+ plugin: str
40
+ action: str
41
+ params: Optional[Dict[str, Any]] = None
42
+
43
+
44
+ class MCPToolRequest(BaseModel):
45
+ """MCP tool request model"""
46
+ tool: str
47
+ arguments: Optional[Dict[str, Any]] = None
48
+
49
+
50
+ class SearchRequest(BaseModel):
51
+ """Search request model"""
52
+ query: str
53
+ n_results: Optional[int] = 5
54
+
55
+
56
+ class RemoteLLMRequest(BaseModel):
57
+ """Remote LLM request model for API access"""
58
+ prompt: str
59
+ task_type: Optional[str] = "general"
60
+ system_prompt: Optional[str] = None
61
+ temperature: Optional[float] = 0.7
62
+ max_tokens: Optional[int] = None
63
+ stream: Optional[bool] = False
64
+
65
+
66
+ class AgentChatRequest(BaseModel):
67
+ """Agent chat request - for local tool execution architecture"""
68
+ messages: List[Dict[str, str]] # Conversation history
69
+ cwd: str # Client's current working directory
70
+ tools: Optional[List[str]] = None # List of available tools on client
71
+
72
+
73
+ class IndexCodeRequest(BaseModel):
74
+ """Request to index code in the server's vector database"""
75
+ user_id: str # Unique user/session identifier
76
+ files: List[Dict[str, str]] # List of {"path": "...", "content": "...", "language": "..."}
77
+ project_name: Optional[str] = None
78
+
79
+
80
+ class SearchCodeRequest(BaseModel):
81
+ """Request to search indexed code"""
82
+ user_id: str # User identifier to search within their indexed code
83
+ query: str
84
+ n_results: Optional[int] = 5
85
+ project_name: Optional[str] = None # Optional: filter by project
86
+
87
+
88
+ def create_app() -> FastAPI:
89
+ """Create the FastAPI application
90
+
91
+ Returns:
92
+ Configured FastAPI app
93
+ """
94
+ app = FastAPI(
95
+ title="NC1709 Dashboard",
96
+ description="Local web dashboard for NC1709 AI assistant",
97
+ version="1.0.0"
98
+ )
99
+
100
+ # CORS for local development
101
+ app.add_middleware(
102
+ CORSMiddleware,
103
+ allow_origins=["*"],
104
+ allow_credentials=True,
105
+ allow_methods=["*"],
106
+ allow_headers=["*"],
107
+ )
108
+
109
+ # Lazy-loaded components
110
+ _components = {}
111
+
112
+ def get_config():
113
+ if "config" not in _components:
114
+ from ..config import get_config
115
+ _components["config"] = get_config()
116
+ return _components["config"]
117
+
118
+ def get_session_manager():
119
+ if "session_manager" not in _components:
120
+ try:
121
+ from ..memory.sessions import SessionManager
122
+ _components["session_manager"] = SessionManager()
123
+ except ImportError:
124
+ _components["session_manager"] = None
125
+ return _components["session_manager"]
126
+
127
+ def get_project_indexer():
128
+ if "indexer" not in _components:
129
+ try:
130
+ from ..memory.indexer import ProjectIndexer
131
+ _components["indexer"] = ProjectIndexer(str(Path.cwd()))
132
+ except (ImportError, Exception):
133
+ _components["indexer"] = None
134
+ return _components["indexer"]
135
+
136
+ def get_plugin_manager():
137
+ if "plugin_manager" not in _components:
138
+ try:
139
+ from ..plugins import PluginManager
140
+ pm = PluginManager()
141
+ pm.discover_plugins()
142
+ pm.load_all()
143
+ _components["plugin_manager"] = pm
144
+ except ImportError:
145
+ _components["plugin_manager"] = None
146
+ return _components["plugin_manager"]
147
+
148
+ def get_mcp_manager():
149
+ if "mcp_manager" not in _components:
150
+ try:
151
+ from ..mcp import MCPManager
152
+ mm = MCPManager(name="nc1709", version="1.0.0")
153
+ mm.setup_default_tools()
154
+ _components["mcp_manager"] = mm
155
+ except ImportError:
156
+ _components["mcp_manager"] = None
157
+ return _components["mcp_manager"]
158
+
159
+ def get_global_vector_store():
160
+ """Get or create a global vector store for all users' code"""
161
+ if "global_vector_store" not in _components:
162
+ try:
163
+ from ..memory.vector_store import VectorStore
164
+ # Store in server's data directory
165
+ import os
166
+ data_dir = os.path.expanduser("~/.nc1709_server/vector_db")
167
+ os.makedirs(data_dir, exist_ok=True)
168
+ _components["global_vector_store"] = VectorStore(persist_directory=data_dir)
169
+ except ImportError:
170
+ _components["global_vector_store"] = None
171
+ return _components["global_vector_store"]
172
+
173
+ def get_code_chunker():
174
+ """Get code chunker for splitting code into indexable chunks"""
175
+ if "code_chunker" not in _components:
176
+ try:
177
+ from ..memory.embeddings import CodeChunker
178
+ _components["code_chunker"] = CodeChunker()
179
+ except ImportError:
180
+ _components["code_chunker"] = None
181
+ return _components["code_chunker"]
182
+
183
+ def get_reasoning_engine():
184
+ if "reasoning" not in _components:
185
+ try:
186
+ from ..reasoning_engine import ReasoningEngine
187
+ _components["reasoning"] = ReasoningEngine()
188
+ except ImportError:
189
+ _components["reasoning"] = None
190
+ return _components["reasoning"]
191
+
192
+ # =========================================================================
193
+ # Static files and main page
194
+ # =========================================================================
195
+
196
+ # Mount static files
197
+ if STATIC_DIR.exists():
198
+ app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static")
199
+
200
+ @app.get("/", response_class=HTMLResponse)
201
+ async def index():
202
+ """Serve the main dashboard page"""
203
+ index_file = TEMPLATES_DIR / "index.html"
204
+ if index_file.exists():
205
+ return FileResponse(index_file)
206
+ return HTMLResponse(content=get_fallback_html(), status_code=200)
207
+
208
+ # =========================================================================
209
+ # API Routes - System
210
+ # =========================================================================
211
+
212
+ @app.get("/api/status")
213
+ async def get_status():
214
+ """Get system status"""
215
+ config = get_config()
216
+
217
+ return {
218
+ "status": "ok",
219
+ "version": "1.0.0",
220
+ "project": str(Path.cwd()),
221
+ "memory_enabled": config.get("memory.enabled", False),
222
+ "timestamp": datetime.now().isoformat()
223
+ }
224
+
225
+ @app.get("/api/config")
226
+ async def get_configuration():
227
+ """Get current configuration"""
228
+ config = get_config()
229
+ return {
230
+ "config": config.config,
231
+ "config_path": str(config.config_path)
232
+ }
233
+
234
+ @app.post("/api/config")
235
+ async def update_config(updates: Dict[str, Any]):
236
+ """Update configuration"""
237
+ config = get_config()
238
+ for key, value in updates.items():
239
+ config.set(key, value)
240
+ return {"status": "ok", "updated": list(updates.keys())}
241
+
242
+ # =========================================================================
243
+ # API Routes - Chat
244
+ # =========================================================================
245
+
246
+ @app.post("/api/chat")
247
+ async def chat(request: ChatRequest):
248
+ """Send a chat message and get a response"""
249
+ reasoning = get_reasoning_engine()
250
+ if not reasoning:
251
+ raise HTTPException(status_code=503, detail="Reasoning engine not available")
252
+
253
+ context = {
254
+ "cwd": str(Path.cwd()),
255
+ "task_type": "general"
256
+ }
257
+
258
+ try:
259
+ response = reasoning.process_request(request.message, context)
260
+
261
+ # Save to session if available
262
+ session_mgr = get_session_manager()
263
+ if session_mgr and request.session_id:
264
+ session = session_mgr.load_session(request.session_id)
265
+ if session:
266
+ session_mgr.add_message(session, "user", request.message)
267
+ session_mgr.add_message(session, "assistant", response)
268
+ session_mgr.save_session(session)
269
+
270
+ return {
271
+ "response": response,
272
+ "timestamp": datetime.now().isoformat()
273
+ }
274
+ except Exception as e:
275
+ raise HTTPException(status_code=500, detail=str(e))
276
+
277
+ # =========================================================================
278
+ # API Routes - Sessions
279
+ # =========================================================================
280
+
281
+ @app.get("/api/sessions")
282
+ async def list_sessions():
283
+ """List all sessions"""
284
+ session_mgr = get_session_manager()
285
+ if not session_mgr:
286
+ return {"sessions": [], "error": "Session manager not available"}
287
+
288
+ sessions = session_mgr.list_sessions(limit=50)
289
+ return {"sessions": sessions}
290
+
291
+ @app.get("/api/sessions/{session_id}")
292
+ async def get_session(session_id: str):
293
+ """Get a specific session"""
294
+ session_mgr = get_session_manager()
295
+ if not session_mgr:
296
+ raise HTTPException(status_code=503, detail="Session manager not available")
297
+
298
+ session = session_mgr.load_session(session_id)
299
+ if not session:
300
+ raise HTTPException(status_code=404, detail="Session not found")
301
+
302
+ return {
303
+ "id": session.id,
304
+ "name": session.name,
305
+ "messages": [
306
+ {"role": m.role, "content": m.content, "timestamp": m.timestamp}
307
+ for m in session.messages
308
+ ],
309
+ "created_at": session.created_at,
310
+ "updated_at": session.updated_at
311
+ }
312
+
313
+ @app.post("/api/sessions")
314
+ async def create_session():
315
+ """Create a new session"""
316
+ session_mgr = get_session_manager()
317
+ if not session_mgr:
318
+ raise HTTPException(status_code=503, detail="Session manager not available")
319
+
320
+ session = session_mgr.start_session(project_path=str(Path.cwd()))
321
+ return {
322
+ "id": session.id,
323
+ "name": session.name,
324
+ "created_at": session.created_at
325
+ }
326
+
327
+ @app.delete("/api/sessions/{session_id}")
328
+ async def delete_session(session_id: str):
329
+ """Delete a session"""
330
+ session_mgr = get_session_manager()
331
+ if not session_mgr:
332
+ raise HTTPException(status_code=503, detail="Session manager not available")
333
+
334
+ success = session_mgr.delete_session(session_id)
335
+ if not success:
336
+ raise HTTPException(status_code=404, detail="Session not found")
337
+
338
+ return {"status": "ok", "deleted": session_id}
339
+
340
+ # =========================================================================
341
+ # API Routes - Search/Index
342
+ # =========================================================================
343
+
344
+ @app.get("/api/index/status")
345
+ async def get_index_status():
346
+ """Get project index status"""
347
+ try:
348
+ indexer = get_project_indexer()
349
+ if not indexer:
350
+ return {"indexed": False, "error": "Indexer not available"}
351
+
352
+ summary = indexer.get_project_summary()
353
+ return {
354
+ "indexed": summary["total_files"] > 0,
355
+ "total_files": summary["total_files"],
356
+ "total_chunks": summary["total_chunks"],
357
+ "languages": summary["languages"]
358
+ }
359
+ except Exception as e:
360
+ return {"indexed": False, "error": str(e)}
361
+
362
+ @app.post("/api/index")
363
+ async def index_project():
364
+ """Index the current project"""
365
+ indexer = get_project_indexer()
366
+ if not indexer:
367
+ raise HTTPException(status_code=503, detail="Indexer not available")
368
+
369
+ try:
370
+ stats = indexer.index_project(show_progress=False)
371
+ return {
372
+ "status": "ok",
373
+ "files_indexed": stats["files_indexed"],
374
+ "chunks_created": stats["chunks_created"],
375
+ "errors": len(stats.get("errors", []))
376
+ }
377
+ except Exception as e:
378
+ raise HTTPException(status_code=500, detail=str(e))
379
+
380
+ @app.post("/api/search")
381
+ async def search_code(request: SearchRequest):
382
+ """Search indexed code"""
383
+ try:
384
+ indexer = get_project_indexer()
385
+ if not indexer:
386
+ raise HTTPException(status_code=503, detail="Indexer not available")
387
+
388
+ results = indexer.search(request.query, n_results=request.n_results)
389
+
390
+ return {
391
+ "query": request.query,
392
+ "results": [
393
+ {
394
+ "content": r.get("content", ""),
395
+ "location": r.get("location", ""),
396
+ "language": r.get("language", ""),
397
+ "similarity": r.get("similarity", 0)
398
+ }
399
+ for r in results
400
+ ]
401
+ }
402
+ except HTTPException:
403
+ raise
404
+ except Exception as e:
405
+ raise HTTPException(status_code=503, detail=str(e))
406
+
407
+ # =========================================================================
408
+ # API Routes - Plugins
409
+ # =========================================================================
410
+
411
+ @app.get("/api/plugins")
412
+ async def list_plugins():
413
+ """List available plugins"""
414
+ pm = get_plugin_manager()
415
+ if not pm:
416
+ return {"plugins": [], "error": "Plugin manager not available"}
417
+
418
+ status = pm.get_status()
419
+ plugins = []
420
+
421
+ for name, info in status.items():
422
+ plugin = pm.get_plugin(name)
423
+ actions = list(plugin.actions.keys()) if plugin else []
424
+
425
+ plugins.append({
426
+ "name": name,
427
+ "version": info["version"],
428
+ "status": info["status"],
429
+ "builtin": info.get("builtin", False),
430
+ "actions": actions,
431
+ "error": info.get("error")
432
+ })
433
+
434
+ return {"plugins": plugins}
435
+
436
+ @app.post("/api/plugins/execute")
437
+ async def execute_plugin_action(request: PluginActionRequest):
438
+ """Execute a plugin action"""
439
+ pm = get_plugin_manager()
440
+ if not pm:
441
+ raise HTTPException(status_code=503, detail="Plugin manager not available")
442
+
443
+ plugin = pm.get_plugin(request.plugin)
444
+ if not plugin:
445
+ raise HTTPException(status_code=404, detail=f"Plugin not found: {request.plugin}")
446
+
447
+ if request.action not in plugin.actions:
448
+ raise HTTPException(status_code=404, detail=f"Action not found: {request.action}")
449
+
450
+ result = pm.execute_action(request.plugin, request.action, **(request.params or {}))
451
+
452
+ return {
453
+ "success": result.success,
454
+ "message": result.message,
455
+ "data": result.data,
456
+ "error": result.error
457
+ }
458
+
459
+ # =========================================================================
460
+ # API Routes - MCP
461
+ # =========================================================================
462
+
463
+ @app.get("/api/mcp/status")
464
+ async def get_mcp_status():
465
+ """Get MCP status"""
466
+ mm = get_mcp_manager()
467
+ if not mm:
468
+ return {"available": False, "error": "MCP not available"}
469
+
470
+ status = mm.get_status()
471
+ return {
472
+ "available": True,
473
+ "server": status["server"],
474
+ "client": status["client"]
475
+ }
476
+
477
+ @app.get("/api/mcp/tools")
478
+ async def list_mcp_tools():
479
+ """List MCP tools"""
480
+ mm = get_mcp_manager()
481
+ if not mm:
482
+ return {"tools": [], "error": "MCP not available"}
483
+
484
+ all_tools = mm.get_all_tools()
485
+
486
+ local_tools = [
487
+ {
488
+ "name": t.name,
489
+ "description": t.description,
490
+ "parameters": [
491
+ {"name": p.name, "type": p.type, "required": p.required}
492
+ for p in t.parameters
493
+ ]
494
+ }
495
+ for t in all_tools["local"]
496
+ ]
497
+
498
+ remote_tools = [
499
+ {"name": t.name, "description": t.description}
500
+ for t in all_tools["remote"]
501
+ ]
502
+
503
+ return {
504
+ "local": local_tools,
505
+ "remote": remote_tools
506
+ }
507
+
508
+ @app.post("/api/mcp/call")
509
+ async def call_mcp_tool(request: MCPToolRequest):
510
+ """Call an MCP tool"""
511
+ mm = get_mcp_manager()
512
+ if not mm:
513
+ raise HTTPException(status_code=503, detail="MCP not available")
514
+
515
+ result = await mm.call_tool(request.tool, request.arguments)
516
+
517
+ if "error" in result:
518
+ raise HTTPException(status_code=400, detail=result["error"])
519
+
520
+ return result
521
+
522
+ # =========================================================================
523
+ # API Routes - Remote LLM Access (for remote clients)
524
+ # =========================================================================
525
+
526
+ def verify_api_key(x_api_key: Optional[str] = Header(None)):
527
+ """Verify API key for remote access"""
528
+ config = get_config()
529
+ server_api_key = config.get("remote.api_key", None)
530
+
531
+ # If no API key is configured, allow access (open mode)
532
+ if not server_api_key:
533
+ return True
534
+
535
+ # If API key is configured, require it
536
+ if not x_api_key or x_api_key != server_api_key:
537
+ raise HTTPException(
538
+ status_code=401,
539
+ detail="Invalid or missing API key. Set X-API-Key header."
540
+ )
541
+ return True
542
+
543
+ @app.get("/api/remote/status")
544
+ async def remote_status(authorized: bool = Depends(verify_api_key)):
545
+ """Check remote API status and available models"""
546
+ config = get_config()
547
+ return {
548
+ "status": "ok",
549
+ "server": "nc1709",
550
+ "version": "1.4.0",
551
+ "models": config.get("models", {}),
552
+ "ollama_url": config.get("ollama.base_url", "http://localhost:11434"),
553
+ "auth_required": bool(config.get("remote.api_key")),
554
+ "timestamp": datetime.now().isoformat()
555
+ }
556
+
557
+ @app.post("/api/remote/complete")
558
+ async def remote_complete(
559
+ request: RemoteLLMRequest,
560
+ authorized: bool = Depends(verify_api_key)
561
+ ):
562
+ """Remote LLM completion endpoint - allows remote clients to use your LLMs"""
563
+ try:
564
+ from ..llm_adapter import LLMAdapter, TaskType
565
+
566
+ llm = LLMAdapter(skip_health_check=True)
567
+
568
+ # Map task type string to enum
569
+ task_type_map = {
570
+ "reasoning": TaskType.REASONING,
571
+ "coding": TaskType.CODING,
572
+ "tools": TaskType.TOOLS,
573
+ "general": TaskType.GENERAL,
574
+ "fast": TaskType.FAST
575
+ }
576
+ task_type = task_type_map.get(request.task_type, TaskType.GENERAL)
577
+
578
+ response = llm.complete(
579
+ prompt=request.prompt,
580
+ task_type=task_type,
581
+ system_prompt=request.system_prompt,
582
+ temperature=request.temperature,
583
+ max_tokens=request.max_tokens,
584
+ stream=False # Streaming not supported over HTTP yet
585
+ )
586
+
587
+ return {
588
+ "response": response,
589
+ "model": llm.get_model_info(task_type),
590
+ "timestamp": datetime.now().isoformat()
591
+ }
592
+ except Exception as e:
593
+ raise HTTPException(status_code=500, detail=str(e))
594
+
595
+ @app.post("/api/remote/chat")
596
+ async def remote_chat(
597
+ request: ChatRequest,
598
+ authorized: bool = Depends(verify_api_key)
599
+ ):
600
+ """Remote chat endpoint - uses full reasoning engine (legacy)"""
601
+ reasoning = get_reasoning_engine()
602
+ if not reasoning:
603
+ raise HTTPException(status_code=503, detail="Reasoning engine not available")
604
+
605
+ try:
606
+ response = reasoning.process_request(request.message)
607
+ return {
608
+ "response": response,
609
+ "timestamp": datetime.now().isoformat()
610
+ }
611
+ except Exception as e:
612
+ raise HTTPException(status_code=500, detail=str(e))
613
+
614
+ # =========================================================================
615
+ # API Routes - Server-Side Vector Database (Code Intelligence)
616
+ # =========================================================================
617
+
618
+ @app.post("/api/remote/index")
619
+ async def remote_index_code(
620
+ request: IndexCodeRequest,
621
+ authorized: bool = Depends(verify_api_key)
622
+ ):
623
+ """
624
+ Index user's code in the server's vector database.
625
+ This allows the server to learn from code patterns and provide better assistance.
626
+ """
627
+ vector_store = get_global_vector_store()
628
+ chunker = get_code_chunker()
629
+
630
+ if not vector_store or not chunker:
631
+ raise HTTPException(
632
+ status_code=503,
633
+ detail="Vector database not available. Install chromadb and sentence-transformers."
634
+ )
635
+
636
+ try:
637
+ indexed_count = 0
638
+ chunk_count = 0
639
+
640
+ for file_info in request.files:
641
+ file_path = file_info.get("path", "unknown")
642
+ content = file_info.get("content", "")
643
+ language = file_info.get("language", "text")
644
+
645
+ if not content:
646
+ continue
647
+
648
+ # Chunk the code
649
+ chunks = chunker.chunk_code(content, language=language)
650
+
651
+ # Add each chunk to vector store with user_id prefix for isolation
652
+ for i, chunk in enumerate(chunks):
653
+ doc_id = f"{request.user_id}:{file_path}:{i}"
654
+ metadata = {
655
+ "user_id": request.user_id,
656
+ "file_path": file_path,
657
+ "language": language,
658
+ "chunk_index": i,
659
+ "project_name": request.project_name or "default",
660
+ "indexed_at": datetime.now().isoformat()
661
+ }
662
+ vector_store.add(
663
+ documents=[chunk],
664
+ ids=[doc_id],
665
+ metadatas=[metadata]
666
+ )
667
+ chunk_count += 1
668
+
669
+ indexed_count += 1
670
+
671
+ return {
672
+ "status": "ok",
673
+ "files_indexed": indexed_count,
674
+ "chunks_created": chunk_count,
675
+ "user_id": request.user_id,
676
+ "timestamp": datetime.now().isoformat()
677
+ }
678
+
679
+ except Exception as e:
680
+ raise HTTPException(status_code=500, detail=str(e))
681
+
682
+ @app.post("/api/remote/search")
683
+ async def remote_search_code(
684
+ request: SearchCodeRequest,
685
+ authorized: bool = Depends(verify_api_key)
686
+ ):
687
+ """
688
+ Search user's indexed code using semantic similarity.
689
+ Only searches within the user's own indexed code.
690
+ """
691
+ vector_store = get_global_vector_store()
692
+
693
+ if not vector_store:
694
+ raise HTTPException(
695
+ status_code=503,
696
+ detail="Vector database not available"
697
+ )
698
+
699
+ try:
700
+ # Build filter for user's code only
701
+ where_filter = {"user_id": request.user_id}
702
+ if request.project_name:
703
+ where_filter["project_name"] = request.project_name
704
+
705
+ # Search
706
+ results = vector_store.query(
707
+ query_texts=[request.query],
708
+ n_results=request.n_results,
709
+ where=where_filter
710
+ )
711
+
712
+ # Format results
713
+ formatted_results = []
714
+ if results and results.get("documents"):
715
+ docs = results["documents"][0] if results["documents"] else []
716
+ metadatas = results["metadatas"][0] if results.get("metadatas") else []
717
+ distances = results["distances"][0] if results.get("distances") else []
718
+
719
+ for i, doc in enumerate(docs):
720
+ formatted_results.append({
721
+ "content": doc,
722
+ "file_path": metadatas[i].get("file_path", "unknown") if i < len(metadatas) else "unknown",
723
+ "language": metadatas[i].get("language", "text") if i < len(metadatas) else "text",
724
+ "similarity": 1 - (distances[i] if i < len(distances) else 0), # Convert distance to similarity
725
+ })
726
+
727
+ return {
728
+ "query": request.query,
729
+ "results": formatted_results,
730
+ "count": len(formatted_results),
731
+ "timestamp": datetime.now().isoformat()
732
+ }
733
+
734
+ except Exception as e:
735
+ raise HTTPException(status_code=500, detail=str(e))
736
+
737
+ @app.get("/api/remote/index/stats")
738
+ async def remote_index_stats(
739
+ user_id: str,
740
+ authorized: bool = Depends(verify_api_key)
741
+ ):
742
+ """Get indexing statistics for a user"""
743
+ vector_store = get_global_vector_store()
744
+
745
+ if not vector_store:
746
+ return {"indexed": False, "error": "Vector database not available"}
747
+
748
+ try:
749
+ # Get count of documents for this user
750
+ # Note: This is a simplified implementation
751
+ return {
752
+ "user_id": user_id,
753
+ "indexed": True,
754
+ "status": "active",
755
+ "timestamp": datetime.now().isoformat()
756
+ }
757
+ except Exception as e:
758
+ return {"indexed": False, "error": str(e)}
759
+
760
+ @app.post("/api/remote/agent")
761
+ async def remote_agent_chat(
762
+ request: AgentChatRequest,
763
+ authorized: bool = Depends(verify_api_key)
764
+ ):
765
+ """
766
+ Agent chat endpoint - returns LLM response with tool calls for LOCAL execution.
767
+
768
+ This is the correct architecture:
769
+ 1. Client sends conversation history + context
770
+ 2. Server runs LLM to generate response (may include tool calls)
771
+ 3. Server returns raw LLM response (does NOT execute tools)
772
+ 4. Client parses tool calls and executes them LOCALLY
773
+ 5. Client sends tool results back for next iteration
774
+ """
775
+ try:
776
+ from ..llm_adapter import LLMAdapter
777
+ from ..prompts.unified_prompt import get_unified_prompt
778
+
779
+ llm = LLMAdapter(skip_health_check=True)
780
+
781
+ # Build unified system prompt - no keyword detection needed
782
+ # The LLM understands user intent from natural language
783
+ system_prompt = get_unified_prompt(request.cwd)
784
+
785
+ # Build messages - prepend system prompt
786
+ messages = [{"role": "system", "content": system_prompt}] + request.messages
787
+
788
+ # Get LLM response (just thinking, no execution)
789
+ response = llm.chat(messages)
790
+
791
+ return {
792
+ "response": response,
793
+ "timestamp": datetime.now().isoformat(),
794
+ "type": "agent_response"
795
+ }
796
+ except Exception as e:
797
+ raise HTTPException(status_code=500, detail=str(e))
798
+
799
+ # =========================================================================
800
+ # WebSocket for real-time chat
801
+ # =========================================================================
802
+
803
+ @app.websocket("/ws/chat")
804
+ async def websocket_chat(websocket: WebSocket):
805
+ """WebSocket endpoint for real-time chat"""
806
+ await websocket.accept()
807
+
808
+ reasoning = get_reasoning_engine()
809
+
810
+ try:
811
+ while True:
812
+ data = await websocket.receive_text()
813
+ message = json.loads(data)
814
+
815
+ if message.get("type") == "chat":
816
+ user_msg = message.get("content", "")
817
+
818
+ # Send acknowledgment
819
+ await websocket.send_json({
820
+ "type": "ack",
821
+ "content": user_msg
822
+ })
823
+
824
+ if reasoning:
825
+ context = {"cwd": str(Path.cwd()), "task_type": "general"}
826
+ response = reasoning.process_request(user_msg, context)
827
+
828
+ await websocket.send_json({
829
+ "type": "response",
830
+ "content": response,
831
+ "timestamp": datetime.now().isoformat()
832
+ })
833
+ else:
834
+ await websocket.send_json({
835
+ "type": "error",
836
+ "content": "Reasoning engine not available"
837
+ })
838
+
839
+ except WebSocketDisconnect:
840
+ pass
841
+ except Exception as e:
842
+ await websocket.send_json({
843
+ "type": "error",
844
+ "content": str(e)
845
+ })
846
+
847
+ return app
848
+
849
+
850
+ def get_fallback_html() -> str:
851
+ """Get fallback HTML if template not found"""
852
+ return """
853
+ <!DOCTYPE html>
854
+ <html lang="en">
855
+ <head>
856
+ <meta charset="UTF-8">
857
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
858
+ <title>NC1709 Dashboard</title>
859
+ <style>
860
+ body {
861
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
862
+ background: #1a1a2e;
863
+ color: #eee;
864
+ margin: 0;
865
+ padding: 20px;
866
+ display: flex;
867
+ justify-content: center;
868
+ align-items: center;
869
+ min-height: 100vh;
870
+ }
871
+ .container {
872
+ text-align: center;
873
+ }
874
+ h1 { color: #00d9ff; }
875
+ p { color: #888; }
876
+ a { color: #00d9ff; }
877
+ </style>
878
+ </head>
879
+ <body>
880
+ <div class="container">
881
+ <h1>NC1709 Dashboard</h1>
882
+ <p>Dashboard is loading...</p>
883
+ <p>If this message persists, the frontend assets may not be installed.</p>
884
+ <p><a href="/api/status">Check API Status</a></p>
885
+ </div>
886
+ </body>
887
+ </html>
888
+ """
889
+
890
+
891
+ def run_server(host: str = "127.0.0.1", port: int = 8709, reload: bool = False, serve_remote: bool = False):
892
+ """Run the web server
893
+
894
+ Args:
895
+ host: Host to bind to
896
+ port: Port to bind to
897
+ reload: Enable auto-reload for development
898
+ serve_remote: If True, bind to 0.0.0.0 for remote access
899
+ """
900
+ import uvicorn
901
+
902
+ # If serving remote, bind to all interfaces
903
+ if serve_remote:
904
+ host = "0.0.0.0"
905
+
906
+ if serve_remote:
907
+ print(f"""
908
+ ╔═══════════════════════════════════════════════════════════╗
909
+ ║ ║
910
+ ║ NC1709 Remote Server ║
911
+ ║ ║
912
+ ║ 🌐 API Server running on port {port} ║
913
+ ║ 📁 Project: {str(Path.cwd())[:40]}
914
+ ║ ║
915
+ ║ Remote clients can connect using: ║
916
+ ║ NC1709_API_URL=http://YOUR_IP:{port} ║
917
+ ║ ║
918
+ ║ For public access, use a tunnel like ngrok: ║
919
+ ║ ngrok http {port} ║
920
+ ║ ║
921
+ ║ Press Ctrl+C to stop ║
922
+ ║ ║
923
+ ╚═══════════════════════════════════════════════════════════╝
924
+ """)
925
+ else:
926
+ print(f"""
927
+ ╔═══════════════════════════════════════════════════════════╗
928
+ ║ ║
929
+ ║ NC1709 Web Dashboard ║
930
+ ║ ║
931
+ ║ 🌐 Running at: http://{host}:{port} ║
932
+ ║ 📁 Project: {str(Path.cwd())[:40]}
933
+ ║ ║
934
+ ║ Press Ctrl+C to stop ║
935
+ ║ ║
936
+ ╚═══════════════════════════════════════════════════════════╝
937
+ """)
938
+
939
+ uvicorn.run(
940
+ "nc1709.web.server:create_app",
941
+ host=host,
942
+ port=port,
943
+ reload=reload,
944
+ factory=True,
945
+ log_level="info"
946
+ )
947
+
948
+
949
+ if __name__ == "__main__":
950
+ run_server()