diff --git a/.agent/workflows/tdd_feature_impl.md b/.agent/workflows/tdd_feature_impl.md new file mode 100644 index 0000000..2b1ac6a --- /dev/null +++ b/.agent/workflows/tdd_feature_impl.md @@ -0,0 +1,47 @@ +--- +description: [tdd_feature_impl]: How to perform a close-loop check pattern utilizing Production DB locally for Test Driven Development (TDD) +--- + +This workflow ensures AI agents implement robust, context-safe features backed by real production data, strictly avoiding regressions. + +# Execution Pattern + +// turbo +1. **Sync Production DB to Local**: Execute the pre-configured script to copy the production SQLite database (`192.168.68.113`) to your local workspace. +```bash +chmod +x /app/scripts/sync_prod_db.sh && bash /app/scripts/sync_prod_db.sh +``` + +2. **Start Local Backend**: Ensure the local backend server is running in the background. Note: Verify if it's already running using `ps aux | grep uvicorn`, otherwise start it. +```bash +cd /app/ai-hub && uvicorn app.main:app --host 0.0.0.0 --port 8000 & +``` + +3. **Define the Integration Test**: Based on the `to-do` task, write an explicit integration test locally (e.g., in `/tmp/test_feature.py`). Use python `requests` targeting `http://localhost:8000` to simulate an end-to-end client API call testing the *desired* functionality that doesn't exist yet. The test MUST fail initially (Red step of Red-Green-Refactor). + +4. **Pause & Switch Sessions (CRITICAL)**: Because iterating on complex implementations causes extreme token bloating and AI context amnesia, you must PAUSE here. Explicitly provide the user with a copy-pasteable prompt template to carry into the new session: + +> *"I have synced the production DB and staged the integration test (`/tmp/...`). To prevent cross-impact and memory pollution during execution, **please switch to a new chat session** and copy-paste this prompt:"* +> +> ```text +> Hi! We are working on the Cortex AI-Hub backend (FastAPI + SQLAlchemy) and following a strict TDD close-loop workflow. +> +> Please execute **[Task Name]** as defined in the execution plan here: @/app/docs/... +> +> 1. I have already synced the production SQLite database locally. +> 2. An integration test has been staged at `[Test File Path]` which hits `http://localhost:8000/...`. +> 3. Start the local development server in the background: +> `cd /app/ai-hub && uvicorn app.main:app --host 0.0.0.0 --port 8000 &` +> 4. Review the requirements from the execution plan. +> 5. [Insert specific files to modify here...] +> +> You are not finished until you can successfully execute `python3 [Test File Path]` against the local server and it passes all tests automatically. Iterate on your code until it works! +> ``` + +5. **Start Implementation (In New Session)**: Once respawned in the new session, begin architectural coding. + +6. **Validate & Loop**: Every time you modify the codebase and believe you are "finished", immediately execute the integration test you defined in Step 3. +```bash +python3 /tmp/test_feature.py +``` +If it fails, fix the code natively. You cannot declare the task completed to the user until this script runs cleanly against the local server containing the production DB dump. diff --git a/ai-hub/app/api/routes/agents.py b/ai-hub/app/api/routes/agents.py new file mode 100644 index 0000000..1281b78 --- /dev/null +++ b/ai-hub/app/api/routes/agents.py @@ -0,0 +1,244 @@ +from fastapi import APIRouter, Depends, HTTPException, BackgroundTasks +from typing import List +from sqlalchemy.orm import Session +from app.api.dependencies import ServiceContainer, get_db +from app.api import schemas +from app.db.models.agent import AgentTemplate, AgentInstance, AgentTrigger +from app.db.models import Message +from app.api.schemas import ( + AgentTemplateCreate, AgentTemplateResponse, + AgentInstanceCreate, AgentInstanceResponse, AgentInstanceStatusUpdate +) +import uuid +import json +from app.core.orchestration.agent_loop import AgentExecutor + +from sqlalchemy.orm import joinedload + +def create_agents_router(services: ServiceContainer) -> APIRouter: + router = APIRouter() + + @router.get("", response_model=List[AgentInstanceResponse]) + def get_agents(db: Session = Depends(get_db)): + return db.query(AgentInstance).options(joinedload(AgentInstance.template)).all() + + @router.post("/templates", response_model=AgentTemplateResponse) + def create_template(request: AgentTemplateCreate, db: Session = Depends(get_db)): + template = AgentTemplate(**request.model_dump()) + db.add(template) + db.commit() + db.refresh(template) + return template + + @router.post("/instances", response_model=AgentInstanceResponse) + def create_instance(request: AgentInstanceCreate, db: Session = Depends(get_db)): + # Verify template exists + template = db.query(AgentTemplate).filter(AgentTemplate.id == request.template_id).first() + if not template: + raise HTTPException(status_code=404, detail="Template not found") + + instance = AgentInstance(**request.model_dump()) + db.add(instance) + db.commit() + db.refresh(instance) + return instance + + @router.patch("/{id}/status", response_model=AgentInstanceResponse) + def update_status(id: str, request: AgentInstanceStatusUpdate, db: Session = Depends(get_db)): + instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() + if not instance: + raise HTTPException(status_code=404, detail="Instance not found") + + instance.status = request.status + db.commit() + db.refresh(instance) + return instance + + @router.patch("/{id}/config", response_model=AgentInstanceResponse) + def update_config(id: str, request: schemas.AgentConfigUpdate, db: Session = Depends(get_db)): + from app.db.models.session import Session as SessionModel + + instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() + if not instance: + raise HTTPException(status_code=404, detail="Instance not found") + + template = db.query(AgentTemplate).filter(AgentTemplate.id == instance.template_id).first() + + if request.name is not None and template: + template.name = request.name + if request.system_prompt is not None and template: + template.system_prompt_path = request.system_prompt + if request.max_loop_iterations is not None and template: + template.max_loop_iterations = request.max_loop_iterations + + if request.mesh_node_id is not None: + instance.mesh_node_id = request.mesh_node_id + + # Update the Session overriding prompt so the running loop picks it up instantly! + if instance.session_id: + session = db.query(SessionModel).filter(SessionModel.id == instance.session_id).first() + if session: + if request.system_prompt is not None: + session.system_prompt_override = request.system_prompt + if hasattr(request, 'provider_name') and request.provider_name is not None: + session.provider_name = request.provider_name + if request.mesh_node_id is not None: + session.attached_node_ids = [request.mesh_node_id] if request.mesh_node_id else [] + + db.commit() + db.refresh(instance) + return instance + + @router.post("/{id}/webhook", status_code=202) + def webhook_receiver(id: str, payload: dict, background_tasks: BackgroundTasks, token: str = None, db: Session = Depends(get_db)): + # Validate instance + instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() + if not instance: + raise HTTPException(status_code=404, detail="Instance not found") + + # Pass webhook event directly to the Agent Executor to process + prompt = f"Webhook Event: {json.dumps(payload)}" + background_tasks.add_task(AgentExecutor.run, instance.id, prompt, services.rag_service, services.user_service) + return {"message": "Accepted"} + + @router.get("/{id}/triggers", response_model=List[schemas.AgentTriggerResponse]) + def get_agent_triggers(id: str, db: Session = Depends(get_db)): + instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() + if not instance: + raise HTTPException(status_code=404, detail="Instance not found") + return db.query(AgentTrigger).filter(AgentTrigger.instance_id == id).all() + + @router.post("/{id}/triggers", response_model=schemas.AgentTriggerResponse) + def create_agent_trigger(id: str, request: schemas.AgentTriggerCreate, db: Session = Depends(get_db)): + trigger = AgentTrigger(**request.model_dump()) + trigger.instance_id = id # Ensure it maps safely + + if trigger.trigger_type == "webhook" and not trigger.webhook_secret: + import secrets + trigger.webhook_secret = secrets.token_hex(16) + + db.add(trigger) + db.commit() + db.refresh(trigger) + return trigger + + @router.delete("/triggers/{trigger_id}") + def delete_agent_trigger(trigger_id: str, db: Session = Depends(get_db)): + trigger = db.query(AgentTrigger).filter(AgentTrigger.id == trigger_id).first() + if not trigger: + raise HTTPException(status_code=404, detail="Trigger not found") + db.delete(trigger) + db.commit() + return {"message": "Trigger deleted successfully"} + + + @router.get("/{id}/telemetry") + def get_telemetry(id: str, db: Session = Depends(get_db)): + instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() + if not instance: + raise HTTPException(status_code=404, detail="Instance not found") + # For MVP/Area 3, return mock telemetry data (e.g. baseline or from cgroup) + # Real cgroup-based metrics will come in Phase 2 + return { + "cpu_usage": 2.5, + "memory_usage": 512, + "network_tx": 120, + "network_rx": 450 + } + + @router.get("/{id}/dependencies") + def get_dependencies(id: str, db: Session = Depends(get_db)): + instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() + if not instance: + raise HTTPException(status_code=404, detail="Instance not found") + return { + "dependencies": [], + "edges": [] + } + + @router.post("/deploy") + def deploy_agent( + request: schemas.DeployAgentRequest, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db) + ): + """ + One-click agent deployment (Design Doc CUJ 1). + Atomically creates: Template → Session → Instance → Locks Session → Injects initial prompt → Starts loop. + """ + from app.db import models as db_models + + # 1. Create Template + template = AgentTemplate( + name=request.name, + description=request.description, + system_prompt_path=request.system_prompt, + max_loop_iterations=request.max_loop_iterations + ) + db.add(template) + db.flush() + + # Resolve default provider mapping if user didn't select one + resolved_provider = request.provider_name + if not resolved_provider: + sys_prefs = services.user_service.get_system_settings(db) + resolved_provider = sys_prefs.get('llm', {}).get('default_provider', 'gemini') + + # 2. Create a locked Session for the agent + new_session = db_models.Session( + user_id="agent-system", + provider_name=resolved_provider, + feature_name="agent_harness", + is_locked=True, + system_prompt_override=request.system_prompt + ) + db.add(new_session) + db.flush() + + # 3. Create AgentInstance + workspace_jail = f"/tmp/cortex/agent_{template.id[:8]}/" + instance = AgentInstance( + template_id=template.id, + session_id=new_session.id, + mesh_node_id=request.mesh_node_id, + status="idle", + current_workspace_jail=workspace_jail + ) + db.add(instance) + db.flush() + + # 4. Kick off agent loop if initial prompt was provided + # (Message insertion is handled automatically by the RAG service execution) + if request.initial_prompt: + instance.status = "active" + db.commit() + + async def run_wrapper(): + await AgentExecutor.run(instance.id, request.initial_prompt, services.rag_service, services.user_service) + + background_tasks.add_task(run_wrapper) + else: + db.commit() + + return { + "template_id": template.id, + "template_name": template.name, + "instance_id": instance.id, + "session_id": new_session.id, + "status": instance.status, + "workspace_jail": workspace_jail, + "message": f"Agent '{request.name}' deployed successfully" + } + @router.delete("/{id}") + def delete_agent(id: str, db: Session = Depends(get_db)): + from app.db.models.agent import AgentInstance + instance = db.query(AgentInstance).filter(AgentInstance.id == id).first() + if not instance: + raise HTTPException(status_code=404, detail="Agent not found") + + # Stop the agent loop if it was active by deleting it (the loop will hit a None instance and return) + db.delete(instance) + db.commit() + return {"message": "Agent deleted successfully"} + + return router diff --git a/ai-hub/app/api/routes/api.py b/ai-hub/app/api/routes/api.py index 3e27520..7270512 100644 --- a/ai-hub/app/api/routes/api.py +++ b/ai-hub/app/api/routes/api.py @@ -31,5 +31,8 @@ router.include_router(create_skills_router(services)) router.include_router(create_agent_update_router()) router.include_router(create_admin_router(), prefix="/admin") + + from .agents import create_agents_router + router.include_router(create_agents_router(services), prefix="/agents", tags=["Agents"]) return router \ No newline at end of file diff --git a/ai-hub/app/api/routes/sessions.py b/ai-hub/app/api/routes/sessions.py index 1d6504b..e4ab25c 100644 --- a/ai-hub/app/api/routes/sessions.py +++ b/ai-hub/app/api/routes/sessions.py @@ -109,6 +109,9 @@ session = db.query(models.Session).filter(models.Session.id == session_id).first() if not session: raise HTTPException(status_code=404, detail="Session not found.") + + if session.is_locked: + raise HTTPException(status_code=403, detail="Cannot clear history of a locked session. Unlock it first.") deleted = db.query(models.Message).filter(models.Message.session_id == session_id).delete() db.commit() @@ -198,6 +201,22 @@ if session_update.tts_provider_name is not None: session.tts_provider_name = session_update.tts_provider_name + if session_update.restrict_skills is not None: + session.restrict_skills = session_update.restrict_skills + + if session_update.allowed_skill_names is not None: + session.allowed_skill_names = session_update.allowed_skill_names + + if session_update.allowed_skill_ids is not None: + skills = db.query(models.Skill).filter(models.Skill.id.in_(session_update.allowed_skill_ids)).all() + session.skills = skills + + if session_update.system_prompt_override is not None: + session.system_prompt_override = session_update.system_prompt_override + + if session_update.is_locked is not None: + session.is_locked = session_update.is_locked + db.commit() db.refresh(session) return session @@ -212,6 +231,9 @@ session = db.query(models.Session).filter(models.Session.id == session_id).first() if not session: raise HTTPException(status_code=404, detail="Session not found.") + + if session.is_locked: + raise HTTPException(status_code=403, detail="Cannot delete a locked session. Unlock it first to delete.") session.is_archived = True sync_workspace_id = session.sync_workspace_id @@ -255,7 +277,8 @@ sessions = db.query(models.Session).filter( models.Session.user_id == user_id, models.Session.feature_name == feature_name, - models.Session.is_archived == False + models.Session.is_archived == False, + models.Session.is_locked == False ).all() workspaces_to_purge = [] diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py index a82a82d..eb27e50 100644 --- a/ai-hub/app/api/schemas.py +++ b/ai-hub/app/api/schemas.py @@ -217,6 +217,11 @@ provider_name: Optional[str] = None stt_provider_name: Optional[str] = None tts_provider_name: Optional[str] = None + restrict_skills: Optional[bool] = None + allowed_skill_ids: Optional[List[int]] = None + allowed_skill_names: Optional[List[str]] = None + system_prompt_override: Optional[str] = None + is_locked: Optional[bool] = None class Session(BaseModel): """Defines the shape of a session object returned by the API.""" @@ -233,6 +238,12 @@ attached_node_ids: Optional[List[str]] = Field(default_factory=list) node_sync_status: Optional[dict] = Field(default_factory=dict) sync_config: Optional[dict] = Field(default_factory=dict) + + restrict_skills: bool = False + allowed_skill_names: Optional[List[str]] = Field(default_factory=list) + system_prompt_override: Optional[str] = None + is_locked: bool = False + model_config = ConfigDict(from_attributes=True) # --- M3: Session Node Attachment Schemas --- @@ -502,3 +513,74 @@ # Keep backward-compat alias AgentNodeSummary = AgentNodeUserView + +# --------------------------------------------------------------------------- +# Agent Schemas +# --------------------------------------------------------------------------- + +class AgentTemplateBase(BaseModel): + name: str + description: Optional[str] = None + system_prompt_path: Optional[str] = None + max_loop_iterations: int = 20 + +class AgentTemplateCreate(AgentTemplateBase): + pass + +class AgentTemplateResponse(AgentTemplateBase): + id: str + model_config = ConfigDict(from_attributes=True) + + +class AgentInstanceBase(BaseModel): + template_id: str + session_id: Optional[int] = None + mesh_node_id: Optional[str] = None + status: str = "idle" + current_workspace_jail: Optional[str] = None + +class AgentInstanceCreate(AgentInstanceBase): + pass + +class AgentInstanceStatusUpdate(BaseModel): + status: str + +class AgentInstanceResponse(AgentInstanceBase): + id: str + last_heartbeat: Optional[datetime] = None + template: Optional[AgentTemplateResponse] = None + model_config = ConfigDict(from_attributes=True) + + +class AgentTriggerBase(BaseModel): + instance_id: str + trigger_type: str + cron_expression: Optional[str] = None + webhook_secret: Optional[str] = None + webhook_mapping_schema: Optional[dict] = None + +class AgentTriggerCreate(AgentTriggerBase): + pass + +class AgentTriggerResponse(AgentTriggerBase): + id: str + model_config = ConfigDict(from_attributes=True) + + +class DeployAgentRequest(BaseModel): + """One-click deployment: creates template + session + instance atomically.""" + name: str + description: Optional[str] = None + system_prompt: Optional[str] = None + mesh_node_id: Optional[str] = None + max_loop_iterations: int = 20 + initial_prompt: Optional[str] = None # First message to kick off the loop + provider_name: Optional[str] = None + +class AgentConfigUpdate(BaseModel): + """Day 2 Agent Configuration edits""" + name: Optional[str] = None + system_prompt: Optional[str] = None + max_loop_iterations: Optional[int] = None + mesh_node_id: Optional[str] = None + provider_name: Optional[str] = None diff --git a/ai-hub/app/core/orchestration/agent_loop.py b/ai-hub/app/core/orchestration/agent_loop.py new file mode 100644 index 0000000..5e7d94d --- /dev/null +++ b/ai-hub/app/core/orchestration/agent_loop.py @@ -0,0 +1,88 @@ +import asyncio +import time +from datetime import datetime +from sqlalchemy.orm import Session +from tenacity import retry, wait_exponential, stop_after_attempt +import json + +from app.db.session import SessionLocal +from app.db.models.agent import AgentInstance, AgentTemplate +from app.db.models import Message + +class AgentExecutor: + @staticmethod + async def run(agent_id: str, prompt: str, rag_service, user_service): + """Asynchronous execution loop for the agent.""" + # Create a fresh DB session for the background task + db: Session = SessionLocal() + try: + instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() + if not instance or not prompt: + return + + # Acquire Lease + instance.last_heartbeat = datetime.utcnow() + instance.status = "active" + db.commit() + + template = db.query(AgentTemplate).filter(AgentTemplate.id == instance.template_id).first() + if not template: + instance.status = "error_suspended" + db.commit() + return + + max_iterations = template.max_loop_iterations or 20 + session_id = instance.session_id + + # Load session to check configured assigned provider + from app.db.models.session import Session as SessionModel + agent_session = db.query(SessionModel).filter(SessionModel.id == session_id).first() + + provider_name = getattr(agent_session, "provider_name", None) + + # If not explicitly defined on session, fallback to smartest available + if not provider_name and user_service: + provider_name = "gemini" + sys_prefs = user_service.get_system_settings(db) + providers = sys_prefs.get("llm", {}).get("providers", {}) + for best_choice in ["deepseek", "gemini", "openai", "anthropic"]: + if best_choice in providers and providers[best_choice].get("api_key"): + provider_name = best_choice + break + + print(f"[AgentExecutor] Starting run for {agent_id} with provider '{provider_name}'. Prompt length: {len(prompt)}") + + # Iterate the RAG architecture to solve the prompt + try: + # We consume the generator completely to let it execute all tools and generate reasoning + async for event in rag_service.chat_with_rag( + db=db, + session_id=session_id, + prompt=prompt, + provider_name=provider_name, + load_faiss_retriever=False, + user_service=user_service + ): + # We could log events here if needed + pass + + # Execution complete + instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() + if instance.status == "active": + instance.status = "idle" # Completed work + db.commit() + + except Exception as e: + print(f"[AgentExecutor] RAG execution failed for {agent_id}: {e}") + instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() + instance.status = "error_suspended" + db.commit() + + except Exception as e: + print(f"[AgentExecutor] Unhandled loop error: {e}") + instance = db.query(AgentInstance).filter(AgentInstance.id == agent_id).first() + if instance: + instance.status = "error_suspended" + db.commit() + finally: + db.close() diff --git a/ai-hub/app/core/orchestration/architect.py b/ai-hub/app/core/orchestration/architect.py index 9fb2f64..8afcf4b 100644 --- a/ai-hub/app/core/orchestration/architect.py +++ b/ai-hub/app/core/orchestration/architect.py @@ -33,13 +33,14 @@ sync_workspace_id: Optional[str] = None, session_id: Optional[int] = None, feature_name: str = "chat", - prompt_slug: str = "rag-pipeline" + prompt_slug: str = "rag-pipeline", + session_override: Optional[str] = None ): # 1. Initialize Context & Messages messages = self.memory.prepare_initial_messages( question, context_chunks, history, feature_name, mesh_context, sync_workspace_id, db=db, user_id=user_id, prompt_service=prompt_service, prompt_slug=prompt_slug, - tools=tools + tools=tools, session_override=session_override ) # DEBUG: Log the total prompt size to detect bloated contexts @@ -75,7 +76,7 @@ if safety.check_cancellation(): yield {"type": "reasoning", "content": "\n> **🛑 User Interruption:** Terminating loop.\n"} return - messages = self.memory.compress_history(messages) + messages = await self.memory.compress_history(messages, llm_provider) # B. Turn Start Heartbeat self._update_turn_marker(messages, turn) diff --git a/ai-hub/app/core/orchestration/memory.py b/ai-hub/app/core/orchestration/memory.py index d1ddf10..517e958 100644 --- a/ai-hub/app/core/orchestration/memory.py +++ b/ai-hub/app/core/orchestration/memory.py @@ -16,7 +16,8 @@ feature_name: str, mesh_context: str, sync_workspace_id: Optional[str], db: Optional[Session] = None, user_id: Optional[str] = None, prompt_service: Any = None, prompt_slug: str = "rag-pipeline", - tools: List[Dict[str, Any]] = None) -> List[Dict[str, str]]: + tools: List[Dict[str, Any]] = None, + session_override: Optional[str] = None) -> List[Dict[str, str]]: from .profiles import get_profile profile = get_profile(feature_name) @@ -31,6 +32,10 @@ db_prompt = prompt_service.get_prompt_by_slug(db, prompt_slug, user_id) if db_prompt: template = db_prompt.content + + # Session Specific Override takes maximum priority + if session_override and session_override.strip(): + template = session_override # Augmented mesh info mesh_info = mesh_context @@ -77,12 +82,12 @@ {"role": "user", "content": question} ] - def compress_history(self, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - """Summarizes middle turns to preserve context window.""" + async def compress_history(self, messages: List[Dict[str, Any]], llm_provider=None) -> List[Dict[str, Any]]: + """Summarizes middle turns using the LLM to preserve context window while preventing amnesia.""" if len(messages) < 60: return messages - logging.info(f"[ContextManager] Memory overflow ({len(messages)}). Compressing middle history...") + logging.info(f"[ContextManager] Memory overflow ({len(messages)}). Compressing middle history via LLM...") system_msg = messages[0] pivot_idx = len(messages) - 20 while pivot_idx > 1 and messages[pivot_idx].get("role") != "user": @@ -106,9 +111,32 @@ summary_text = "\n".join(summary_lines) + # If we have an LLM provider, do a fast, dense compression + if llm_provider: + try: + prompt = f"Please read the following timeline of chronological actions and compress them into a dense, highly factual summary paragraph. Retain critical paths, file names, errors, and system state outcomes. Omit pleasantries and reasoning fluff.\n\n{summary_text}" + + comp_res = await llm_provider.acompletion( + messages=[{"role": "user", "content": prompt}], + max_tokens=500, + temperature=0.0, + stream=False + ) + if comp_res and comp_res.choices: + try: + llm_summary = comp_res.choices[0].message.content + except AttributeError: + # Fallback for weird dictionary responses + llm_summary = comp_res.choices[0].get("message", {}).get("content") + + if llm_summary: + summary_text = llm_summary.strip() + except Exception as e: + logging.warning(f"LLM Summary compression failed, falling back to python truncation: {e}") + history_msg = { "role": "user", - "content": f"[SYSTEM: Historical Context Summary]\n\n{summary_text}\n\n[End Summary.]" + "content": f"[SYSTEM: Historical Context Timeline Condensed into Summary]\n\n{summary_text}\n\n[End Summary.]" } ack_msg = { "role": "assistant", diff --git a/ai-hub/app/core/services/rag.py b/ai-hub/app/core/services/rag.py index b7c01b1..84d4d08 100644 --- a/ai-hub/app/core/services/rag.py +++ b/ai-hub/app/core/services/rag.py @@ -95,7 +95,7 @@ tools = [] if self.tool_service: - tools = self.tool_service.get_available_tools(db, session.user_id, feature=session.feature_name) + tools = self.tool_service.get_available_tools(db, session.user_id, feature=session.feature_name, session_id=session.id) profile = get_profile(session.feature_name) mesh_context = "" @@ -194,7 +194,8 @@ sync_workspace_id = session.sync_workspace_id, session_id = session_id, feature_name = session.feature_name, - prompt_slug = profile.default_prompt_slug + prompt_slug = profile.default_prompt_slug, + session_override = session.system_prompt_override ): if event["type"] == "content": full_answer += event["content"] diff --git a/ai-hub/app/core/services/tool.py b/ai-hub/app/core/services/tool.py index bdd6276..a347cf8 100644 --- a/ai-hub/app/core/services/tool.py +++ b/ai-hub/app/core/services/tool.py @@ -22,16 +22,28 @@ self._local_skills = {s.name: s for s in local_skills} tool_registry.load_plugins() - def get_available_tools(self, db: Session, user_id: str, feature: str = None) -> List[Dict[str, Any]]: + def get_available_tools(self, db: Session, user_id: str, feature: str = None, session_id: int = None) -> List[Dict[str, Any]]: """ Retrieves all tools the user is authorized to use, optionally filtered by feature. """ + allowed_skill_names = None + if session_id and db: + session_obj = db.query(models.Session).filter(models.Session.id == session_id).first() + if session_obj and getattr(session_obj, "restrict_skills", False): + allowed_skill_names = set() + if session_obj.allowed_skill_names: + allowed_skill_names.update(session_obj.allowed_skill_names) + if getattr(session_obj, "skills", None): + allowed_skill_names.update(s.name for s in session_obj.skills) + # 1. Fetch system/local skills and filter by feature if requested local_skills = self._local_skills.values() if feature: local_skills = [s for s in local_skills if feature in getattr(s, "features", ["chat"])] tools = [s.to_tool_definition() for s in local_skills] + if allowed_skill_names is not None: + tools = [t for t in tools if t["function"]["name"] in allowed_skill_names] # 2. Add FS-defined skills (System skills or user-owned) from app.core.skills.fs_loader import fs_loader @@ -51,6 +63,8 @@ fs_skill["files"] = [_DictObj(f) for f in fs_skill.get("files", [])] db_skills.append(_DictObj(fs_skill)) + if allowed_skill_names is not None: + db_skills = [ds for ds in db_skills if ds.name in allowed_skill_names] import litellm max_md_len = 1000 diff --git a/ai-hub/app/db/__init__.py b/ai-hub/app/db/__init__.py index e69de29..b03d5b4 100644 --- a/ai-hub/app/db/__init__.py +++ b/ai-hub/app/db/__init__.py @@ -0,0 +1 @@ +from .models.agent import AgentTemplate, AgentInstance, AgentTrigger \ No newline at end of file diff --git a/ai-hub/app/db/migrate.py b/ai-hub/app/db/migrate.py index ac6fb85..69f34fa 100644 --- a/ai-hub/app/db/migrate.py +++ b/ai-hub/app/db/migrate.py @@ -52,6 +52,10 @@ ("node_sync_status", "TEXT"), ("sync_config", "TEXT"), ("is_cancelled", "INTEGER DEFAULT 0"), + ("restrict_skills", "BOOLEAN DEFAULT 0"), + ("allowed_skill_names","TEXT"), + ("system_prompt_override","TEXT"), + ("is_locked", "BOOLEAN DEFAULT 0"), ] for col_name, col_type in session_required_columns: if col_name not in session_columns: @@ -160,6 +164,22 @@ except Exception as e: logger.error(f"Failed to create 'skill_group_access': {e}") + # Create session_skills table if it doesn't exist + if not inspector.has_table("session_skills"): + logger.info("Creating table 'session_skills'...") + try: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS session_skills ( + session_id INTEGER NOT NULL REFERENCES sessions(id) ON DELETE CASCADE, + skill_id INTEGER NOT NULL REFERENCES skills(id) ON DELETE CASCADE, + PRIMARY KEY (session_id, skill_id) + ) + """)) + conn.commit() + logger.info("Table 'session_skills' created.") + except Exception as e: + logger.error(f"Failed to create 'session_skills': {e}") + # Create skill_files table if it doesn't exist if not inspector.has_table("skill_files"): logger.info("Creating table 'skill_files'...") @@ -214,6 +234,61 @@ except Exception as e: logger.error(f"Failed to drop column '{col_name}' from 'skills'. SQLite might not support drop column: {e}") + # --- Area 1: Agent Infrastructure Tables --- + if not inspector.has_table("agent_templates"): + logger.info("Creating table 'agent_templates'...") + try: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS agent_templates ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + system_prompt_path TEXT, + max_loop_iterations INTEGER DEFAULT 20 + ) + """)) + conn.commit() + logger.info("Table 'agent_templates' created.") + except Exception as e: + logger.error(f"Failed to create 'agent_templates': {e}") + + if not inspector.has_table("agent_instances"): + logger.info("Creating table 'agent_instances'...") + try: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS agent_instances ( + id TEXT PRIMARY KEY, + template_id TEXT NOT NULL REFERENCES agent_templates(id), + session_id INTEGER REFERENCES sessions(id), + mesh_node_id TEXT, + status TEXT DEFAULT 'idle', + current_workspace_jail TEXT, + last_heartbeat DATETIME DEFAULT CURRENT_TIMESTAMP + ) + """)) + conn.commit() + logger.info("Table 'agent_instances' created.") + except Exception as e: + logger.error(f"Failed to create 'agent_instances': {e}") + + if not inspector.has_table("agent_triggers"): + logger.info("Creating table 'agent_triggers'...") + try: + conn.execute(text(""" + CREATE TABLE IF NOT EXISTS agent_triggers ( + id TEXT PRIMARY KEY, + instance_id TEXT NOT NULL REFERENCES agent_instances(id), + trigger_type TEXT NOT NULL, + cron_expression TEXT, + webhook_secret TEXT, + webhook_mapping_schema JSON + ) + """)) + conn.commit() + logger.info("Table 'agent_triggers' created.") + except Exception as e: + logger.error(f"Failed to create 'agent_triggers': {e}") + logger.info("Database migrations complete.") diff --git a/ai-hub/app/db/models/__init__.py b/ai-hub/app/db/models/__init__.py index 9850d37..a0692a7 100644 --- a/ai-hub/app/db/models/__init__.py +++ b/ai-hub/app/db/models/__init__.py @@ -1,3 +1,4 @@ +from app.db.database import Base from .user import User, Group from .session import Session, Message from .document import Document, VectorMetadata diff --git a/ai-hub/app/db/models/agent.py b/ai-hub/app/db/models/agent.py new file mode 100644 index 0000000..60c652c --- /dev/null +++ b/ai-hub/app/db/models/agent.py @@ -0,0 +1,42 @@ +from sqlalchemy import Column, String, Integer, ForeignKey, DateTime, Enum, JSON, Boolean +from sqlalchemy.orm import relationship +import uuid +import datetime +from app.db.database import Base + +class AgentTemplate(Base): + __tablename__ = 'agent_templates' + + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + name = Column(String, nullable=False) + description = Column(String, nullable=True) + system_prompt_path = Column(String, nullable=True) + max_loop_iterations = Column(Integer, default=20) + + instances = relationship("AgentInstance", back_populates="template") + +class AgentInstance(Base): + __tablename__ = 'agent_instances' + + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + template_id = Column(String, ForeignKey('agent_templates.id'), nullable=False) + session_id = Column(Integer, ForeignKey('sessions.id'), nullable=True) + mesh_node_id = Column(String, nullable=True) # Just use string or connect to agent_nodes.node_id if needed + status = Column(String, default='idle') # Enum: active, idle, listening, error_suspended + current_workspace_jail = Column(String, nullable=True) + last_heartbeat = Column(DateTime, default=datetime.datetime.utcnow) + + template = relationship("AgentTemplate", back_populates="instances") + triggers = relationship("AgentTrigger", back_populates="instance") + +class AgentTrigger(Base): + __tablename__ = 'agent_triggers' + + id = Column(String, primary_key=True, default=lambda: str(uuid.uuid4())) + instance_id = Column(String, ForeignKey('agent_instances.id'), nullable=False) + trigger_type = Column(String, nullable=False) # Enum: webhook, cron, manual + cron_expression = Column(String, nullable=True) + webhook_secret = Column(String, nullable=True) + webhook_mapping_schema = Column(JSON, nullable=True) + + instance = relationship("AgentInstance", back_populates="triggers") diff --git a/ai-hub/app/db/models/session.py b/ai-hub/app/db/models/session.py index 9e34798..4915f95 100644 --- a/ai-hub/app/db/models/session.py +++ b/ai-hub/app/db/models/session.py @@ -1,8 +1,13 @@ from datetime import datetime -from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON +from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, Boolean, JSON, Table from sqlalchemy.orm import relationship from ..database import Base +session_skills = Table('session_skills', Base.metadata, + Column('session_id', Integer, ForeignKey('sessions.id', ondelete="CASCADE"), primary_key=True), + Column('skill_id', Integer, ForeignKey('skills.id', ondelete="CASCADE"), primary_key=True) +) + class Session(Base): __tablename__ = 'sessions' @@ -21,8 +26,14 @@ attached_node_ids = Column(JSON, default=[], nullable=True) node_sync_status = Column(JSON, default={}, nullable=True) sync_config = Column(JSON, default={}, nullable=True) + + restrict_skills = Column(Boolean, default=False, nullable=False) + allowed_skill_names = Column(JSON, default=[], nullable=True) + system_prompt_override = Column(Text, nullable=True) + is_locked = Column(Boolean, default=False, nullable=False) messages = relationship("Message", back_populates="session", cascade="all, delete-orphan") + skills = relationship("Skill", secondary=session_skills, backref="sessions") user = relationship("User", back_populates="sessions") def __repr__(self): diff --git a/ai-hub/docs/api_reference/models.md b/ai-hub/docs/api_reference/models.md index 75c310b..f8067ae 100644 --- a/ai-hub/docs/api_reference/models.md +++ b/ai-hub/docs/api_reference/models.md @@ -509,6 +509,10 @@ | `attached_node_ids` | `anyOf` | | | `node_sync_status` | `anyOf` | | | `sync_config` | `anyOf` | | +| `restrict_skills` | `boolean` | | +| `allowed_skill_names` | `anyOf` | | +| `system_prompt_override` | `anyOf` | | +| `is_locked` | `boolean` | | ## `SessionCreate` @@ -559,6 +563,11 @@ | `provider_name` | `anyOf` | | | `stt_provider_name` | `anyOf` | | | `tts_provider_name` | `anyOf` | | +| `restrict_skills` | `anyOf` | | +| `allowed_skill_ids` | `anyOf` | | +| `allowed_skill_names` | `anyOf` | | +| `system_prompt_override` | `anyOf` | | +| `is_locked` | `anyOf` | | ## `SkillConfig` diff --git a/ai-hub/uvicorn.log b/ai-hub/uvicorn.log new file mode 100644 index 0000000..3e9d165 --- /dev/null +++ b/ai-hub/uvicorn.log @@ -0,0 +1,282 @@ +nohup: ignoring input +INFO:app.core.tools.registry:Registered dynamic tool plugin: 'browser_automation_agent' +INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_file_explorer' +INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_inspect_drift' +INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_sync_control' +INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_terminal_control' +INFO:app.core.tools.registry:Registered dynamic tool plugin: 'mesh_wait_tasks' +INFO:app.core.tools.registry:Registered dynamic tool plugin: 'read_skill_artifact' +INFO: Started server process [79083] +INFO: Waiting for application startup. +INFO:app.db.migrate:Starting database migrations... +INFO:app.db.migrate:Column 'audio_path' already exists in 'messages'. +INFO:app.db.migrate:Column 'model_response_time' already exists in 'messages'. +INFO:app.db.migrate:Column 'token_count' already exists in 'messages'. +INFO:app.db.migrate:Column 'reasoning_content' already exists in 'messages'. +INFO:app.db.migrate:Column 'stt_provider_name' already exists in 'sessions'. +INFO:app.db.migrate:Column 'tts_provider_name' already exists in 'sessions'. +INFO:app.db.migrate:Column 'sync_workspace_id' already exists in 'sessions'. +INFO:app.db.migrate:Column 'attached_node_ids' already exists in 'sessions'. +INFO:app.db.migrate:Column 'node_sync_status' already exists in 'sessions'. +INFO:app.db.migrate:Column 'sync_config' already exists in 'sessions'. +INFO:app.db.migrate:Column 'is_cancelled' already exists in 'sessions'. +INFO:app.db.migrate:Column 'restrict_skills' already exists in 'sessions'. +INFO:app.db.migrate:Column 'allowed_skill_names' already exists in 'sessions'. +INFO:app.db.migrate:Column 'system_prompt_override' already exists in 'sessions'. +INFO:app.db.migrate:Column 'is_locked' already exists in 'sessions'. +INFO:app.db.migrate:Database migrations complete. +INFO:app.core.services.node_registry:[NodeRegistry] Reset all DB node statuses to 'offline'. +ERROR:app.app:[M6] Failed to start gRPC server: No module named 'grpc_reflection' +INFO:app.core.skills.bootstrap:Checking for system skills bootstrapping... +INFO:app.core.skills.bootstrap:System skills bootstrap completed. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) +✅ Loading configuration from app/config.yaml +Application startup... +--- ⚙️ Application Configuration --- + - ALLOW_OIDC_LOGIN: False + - ALLOW_PASSWORD_LOGIN: *** + - DATABASE_URL: sqlite:///./data/ai_hub.db + - DATA_DIR: /app/data + - DB_MODE: sqlite + - DEEPSEEK_API_KEY: sk-a...6bf2 + - DEEPSEEK_MODEL_NAME: deepseek-chat + - EMBEDDING_API_KEY: AIza...sKuI + - EMBEDDING_DIMENSION: 768 + - EMBEDDING_MODEL_NAME: models/text-embedding-004 + - EMBEDDING_PROVIDER: google_gemini + - FAISS_INDEX_PATH: data/faiss_index.bin + - GEMINI_API_KEY: AIza...sKuI + - GEMINI_MODEL_NAME: gemini-1.5-flash + - GRPC_CERT_PATH: None + - GRPC_EXTERNAL_ENDPOINT: None + - GRPC_KEY_PATH: Not Set + - GRPC_TLS_ENABLED: False + - LLM_PROVIDERS: {'deepseek': {'api_key': 'sk-a1b3b85a32a942c3b80e06566ef46bf2'}, 'gemini': {'api_key': 'AIzaSyBn5HYiZ8yKmNL0ambyz4Aspr5lKw1sKuI'}, 'openai': {'api_key': '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'}} + - LOG_LEVEL: DEBUG + - OIDC_CLIENT_ID: cortex-server + - OIDC_CLIENT_SECRET: aYc2...leZI + - OIDC_ENABLED: False + - OIDC_REDIRECT_URI: http://localhost:8001/users/login/callback + - OIDC_SERVER_URL: https://auth.jerxie.com + - OPENAI_API_KEY: sk-p...h34A + - PROJECT_NAME: Cortex Hub + - SECRET_KEY: aYc2...leZI + - SKILLS_DIR: /app/data/skills + - STT_API_KEY: AIza...sKuI + - STT_MODEL_NAME: gemini-2.5-flash + - STT_PROVIDER: google_gemini + - STT_PROVIDERS: {} + - SUPER_ADMINS: ['axieyangb@gmail.com'] + - TTS_API_KEY: AIza...sKuI + - TTS_MODEL_NAME: gemini-2.5-flash-preview-tts + - TTS_PROVIDER: google_gemini + - TTS_PROVIDERS: {} + - TTS_VOICE_NAME: Kore + - VERSION: 1.0.0 +------------------------------------ +Creating database tables... +INFO: 127.0.0.1:37766 - "POST /sessions/ HTTP/1.1" 200 OK +INFO: 127.0.0.1:37782 - "POST /api/v1/agents/templates HTTP/1.1" 200 OK +INFO: 127.0.0.1:37784 - "POST /api/v1/agents/instances HTTP/1.1" 200 OK +INFO: 127.0.0.1:37790 - "POST /api/v1/agents/b8bad024-ca14-42a5-baf4-01e9be120f54/webhook HTTP/1.1" 202 Accepted +INFO: 127.0.0.1:37804 - "GET /api/v1/agents HTTP/1.1" 200 OK +[AgentExecutor] Agent b8bad024-ca14-42a5-baf4-01e9be120f54 suspended: max iterations reached. + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + [📁🧹] Running Mirror Cleanup. Active Sessions: 11 + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + +INFO: 127.0.0.1:38568 - "POST /sessions/ HTTP/1.1" 200 OK +INFO: 127.0.0.1:38576 - "POST /api/v1/agents/templates HTTP/1.1" 200 OK +INFO: 127.0.0.1:38578 - "POST /api/v1/agents/instances HTTP/1.1" 200 OK +INFO: 127.0.0.1:38588 - "GET /api/v1/agents/ac074e05-874e-4173-ab35-20d0edcabc77/telemetry HTTP/1.1" 404 Not Found + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + +INFO: 127.0.0.1:53798 - "POST /sessions/ HTTP/1.1" 200 OK +INFO: 127.0.0.1:53800 - "POST /api/v1/agents/templates HTTP/1.1" 200 OK +INFO: 127.0.0.1:53806 - "POST /api/v1/agents/instances HTTP/1.1" 200 OK +INFO: 127.0.0.1:53814 - "GET /api/v1/agents/bf04a4e8-4099-41fd-b71e-96742eafb55f/telemetry HTTP/1.1" 404 Not Found + +================================================== +📡 CORTEX MESH DASHBOARD | 0 Nodes Online +-------------------------------------------------- + No nodes currently connected. +================================================== + diff --git a/curl_setup_agent.sh b/curl_setup_agent.sh new file mode 100644 index 0000000..d086c2b --- /dev/null +++ b/curl_setup_agent.sh @@ -0,0 +1,45 @@ +#!/bin/bash +USER_ID="9a333ccd-9c3f-432f-a030-7b1e1284a436" +API="https://ai.jerxie.com/api/v1" + +echo "Creating template..." +curl -s -X POST "$API/agents/templates/" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "demo-telemetry-agent", + "name": "Live Telemetry Demo", + "system_prompt_template": "Just a test.", + "allowed_skills": [], + "resource_limits": {"max_memory_mb": 512, "max_cpu_percent": 80} + }' + +echo -e "\nCreating session..." +RESP=$(curl -s -X POST "$API/sessions/" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "'"$USER_ID"'", + "feature_name": "agent_dashboard" + }') +echo "$RESP" + +# Since jq might not be available, extract string matching "id": 123 +SESSION_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | cut -d':' -f2 | head -n 1) + +if [ -z "$SESSION_ID" ]; then + echo "Failed to extract session ID." + exit 1 +fi + +echo -e "\nCreating instance..." +curl -s -X POST "$API/agents/instances/" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "template_id": "demo-telemetry-agent", + "session_id": '"$SESSION_ID"', + "mesh_node_id": "test-node-1", + "status": "active", + "current_workspace_jail": "/tmp/cortex-sync" + }' diff --git a/curl_setup_agent_fix.sh b/curl_setup_agent_fix.sh new file mode 100644 index 0000000..c72e260 --- /dev/null +++ b/curl_setup_agent_fix.sh @@ -0,0 +1,44 @@ +#!/bin/bash +USER_ID="9a333ccd-9c3f-432f-a030-7b1e1284a436" +API="https://ai.jerxie.com/api/v1" + +echo "Creating template..." +curl -s -X POST "$API/agents/templates" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "id": "demo-telemetry-agent", + "name": "Live Telemetry Demo", + "system_prompt_template": "Just a test.", + "allowed_skills": [], + "resource_limits": {"max_memory_mb": 512, "max_cpu_percent": 80} + }' + +echo -e "\nCreating session..." +RESP=$(curl -s -X POST "$API/sessions/" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "'"$USER_ID"'", + "feature_name": "agent_dashboard" + }') +echo "$RESP" + +SESSION_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | cut -d':' -f2 | head -n 1) + +if [ -z "$SESSION_ID" ]; then + echo "Failed to extract session ID." + exit 1 +fi + +echo -e "\nCreating instance..." +curl -s -X POST "$API/agents/instances" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "template_id": "demo-telemetry-agent", + "session_id": '"$SESSION_ID"', + "mesh_node_id": "test-node-1", + "status": "active", + "current_workspace_jail": "/tmp/cortex-sync" + }' diff --git a/curl_setup_agent_fix2.sh b/curl_setup_agent_fix2.sh new file mode 100644 index 0000000..ef66e49 --- /dev/null +++ b/curl_setup_agent_fix2.sh @@ -0,0 +1,37 @@ +#!/bin/bash +USER_ID="9a333ccd-9c3f-432f-a030-7b1e1284a436" +API="https://ai.jerxie.com/api/v1" + +echo "Creating template..." +TPL_RESP=$(curl -s -X POST "$API/agents/templates" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "Live Telemetry Demo" + }') +echo "$TPL_RESP" +TPL_ID=$(echo "$TPL_RESP" | grep -o '"id":"[^"]*' | cut -d'"' -f4 | head -n 1) + +echo -e "\nCreating session..." +RESP=$(curl -s -X POST "$API/sessions/" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "user_id": "'"$USER_ID"'", + "feature_name": "agent_dashboard" + }') +echo "$RESP" + +SESSION_ID=$(echo "$RESP" | grep -o '"id":[0-9]*' | cut -d':' -f2 | head -n 1) + +echo -e "\nCreating instance..." +curl -s -X POST "$API/agents/instances" \ + -H "X-User-ID: $USER_ID" \ + -H "Content-Type: application/json" \ + -d '{ + "template_id": "'"$TPL_ID"'", + "session_id": '"$SESSION_ID"', + "mesh_node_id": "test-node-1", + "status": "active", + "current_workspace_jail": "/tmp/cortex-sync" + }' diff --git a/deployment/jerxie-prod/docker-compose.production.yml b/deployment/jerxie-prod/docker-compose.production.yml index ed3330c..93a389e 100644 --- a/deployment/jerxie-prod/docker-compose.production.yml +++ b/deployment/jerxie-prod/docker-compose.production.yml @@ -5,6 +5,8 @@ services: ai-hub: + env_file: + - .env environment: - HUB_PUBLIC_URL=https://ai.jerxie.com - HUB_GRPC_ENDPOINT=ai.jerxie.com:443 diff --git a/envoy_listener_fix.yaml b/envoy_listener_fix.yaml new file mode 100644 index 0000000..aa5bdcf --- /dev/null +++ b/envoy_listener_fix.yaml @@ -0,0 +1,59 @@ +'@type': type.googleapis.com/envoy.config.listener.v3.Listener +address: + socketAddress: + address: 0.0.0.0 + portValue: 10001 +filterChains: + - filterChainMatch: + serverNames: + - ai.jerxie.com + filters: + - name: envoy.filters.network.http_connection_manager + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager + httpFilters: + - name: envoy.filters.http.router + typedConfig: + '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router + routeConfig: + name: ai_unified_service + virtualHosts: + - domains: + - ai.jerxie.com + - ai.jerxie.com:443 + name: ai_service + routes: + - match: + prefix: /agent. + route: + autoHostRewrite: false + cluster: _ai_agent_orchestrator + maxStreamDuration: + grpcTimeoutHeaderMax: 0s + timeout: 0s + - match: + prefix: / + route: + cluster: _ai_unified_server + timeout: 0s + statPrefix: ingress_http + upgradeConfigs: + - upgradeType: websocket + transportSocket: + name: envoy.transport_sockets.tls + typedConfig: + '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext + commonTlsContext: + alpnProtocols: + - h2 + - http/1.1 + tlsCertificateSdsSecretConfigs: + - name: ai_jerxie_com + sdsConfig: + apiConfigSource: + apiType: GRPC + grpcServices: + - envoyGrpc: + clusterName: xds_cluster + transportApiVersion: V3 + resourceApiVersion: V3 diff --git a/envoy_request.json b/envoy_request.json new file mode 100644 index 0000000..3fc3c15 --- /dev/null +++ b/envoy_request.json @@ -0,0 +1 @@ +{"name": "https_listener", "yaml": "'@type': type.googleapis.com/envoy.config.listener.v3.Listener\naddress:\n socketAddress:\n address: 0.0.0.0\n portValue: 10001\nfilterChains:\n - filterChainMatch:\n serverNames:\n - pcb.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n name: pcb_service\n virtualHosts:\n - domains:\n - pcb.jerxie.com\n name: pcb_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _pcb_server\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: pcb_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - monitor.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - monitor.jerxie.com\n name: monitor_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _monitor_server\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: monitor_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - ai.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n name: ai_unified_service\n virtualHosts:\n - domains:\n - ai.jerxie.com\n - ai.jerxie.com:443\n name: ai_service\n routes:\n - match:\n prefix: /agent.\n route:\n autoHostRewrite: false\n cluster: _ai_agent_orchestrator\n maxStreamDuration:\n grpcTimeoutHeaderMax: 0s\n timeout: 0s\n - match:\n prefix: /\n route:\n cluster: _ai_unified_server\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n alpnProtocols:\n - h2\n - http/1.1\n tlsCertificateSdsSecretConfigs:\n - name: ai_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - container.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - container.jerxie.com\n name: container_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _portainer_ui\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: container_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - password.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - password.jerxie.com\n name: password_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _bitwarden_service\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: password_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - docker.jerxie.com\n - docker.local\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - docker.jerxie.com\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _docker_registry\n timeout: 0s\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: docker_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - video.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - video.jerxie.com\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_video\n timeout: 0s\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: video_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - audio.jerxie.com\n - audio.local\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - audio.jerxie.com\n - audio.local\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_audio\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: audio_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - gitbucket.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - gitbucket.jerxie.com\n name: gitbucket_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _git_bucket\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: gitbucket_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - photo.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - photo.jerxie.com\n name: photo_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_photo\n timeout: 0s\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: photo_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - note.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - note.jerxie.com\n name: note_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_note\n statPrefix: ingress_http\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: note_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - home.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n mergeSlashes: true\n normalizePath: true\n requestTimeout: 300s\n routeConfig:\n virtualHosts:\n - domains:\n - home.jerxie.com\n name: home_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _homeassistant_service\n statPrefix: ingress_http\n streamIdleTimeout: 300s\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: home_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - auth.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - auth.jerxie.com\n name: auth_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _auth_server\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: auth_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - nas\n - nas.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n maxRequestHeadersKb: 96\n routeConfig:\n virtualHosts:\n - domains:\n - nas.jerxie.com\n - nas:10001\n name: docker_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _nas_service\n timeout: 0s\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: nas_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - code.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2\n name: oidc_oauth2_config_code-server\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication\n name: oidc_jwt_authn_config\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua\n name: oidc_authz_lua\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - code.jerxie.com\n name: code_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _code_server\n statPrefix: ingress_http\n upgradeConfigs:\n - upgradeType: websocket\n name: code_server_filter_chain\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: code_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\n - filterChainMatch:\n serverNames:\n - envoy.jerxie.com\n filters:\n - name: envoy.filters.network.http_connection_manager\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager\n httpFilters:\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.oauth2.v3.OAuth2\n name: oidc_oauth2_config_envoy-server\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication\n name: oidc_jwt_authn_config\n - configDiscovery:\n configSource:\n ads: {}\n resourceApiVersion: V3\n typeUrls:\n - type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua\n name: oidc_authz_lua\n - name: envoy.filters.http.router\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.http.router.v3.Router\n routeConfig:\n virtualHosts:\n - domains:\n - envoy.jerxie.com\n name: envoy_service\n routes:\n - match:\n prefix: /\n route:\n cluster: _envoy_server\n statPrefix: ingress_http_envoy\n upgradeConfigs:\n - upgradeType: websocket\n name: envoy_server_filter_chain\n transportSocket:\n name: envoy.transport_sockets.tls\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext\n commonTlsContext:\n tlsCertificateSdsSecretConfigs:\n - name: envoy_jerxie_com\n sdsConfig:\n apiConfigSource:\n apiType: GRPC\n grpcServices:\n - envoyGrpc:\n clusterName: xds_cluster\n transportApiVersion: V3\n resourceApiVersion: V3\nlistenerFilters:\n - name: envoy.filters.listener.tls_inspector\n typedConfig:\n '@type': type.googleapis.com/envoy.extensions.filters.listener.tls_inspector.v3.TlsInspector\nname: https_listener\n", "upsert": true} diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 9adf07f..cc54a13 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -22,6 +22,8 @@ "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-scripts": "5.0.1", + "reactflow": "^11.11.4", + "recharts": "^3.8.0", "web-vitals": "^2.1.4" }, "devDependencies": { @@ -3396,6 +3398,136 @@ } } }, + "node_modules/@reactflow/background": { + "version": "11.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/background/-/background-11.3.14.tgz", + "integrity": "sha512-Gewd7blEVT5Lh6jqrvOgd4G6Qk17eGKQfsDXgyRSqM+CTwDqRldG2LsWN4sNeno6sbqVIC2fZ+rAUBFA9ZEUDA==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/controls": { + "version": "11.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/controls/-/controls-11.2.14.tgz", + "integrity": "sha512-MiJp5VldFD7FrqaBNIrQ85dxChrG6ivuZ+dcFhPQUwOK3HfYgX2RHdBua+gx+40p5Vw5It3dVNp/my4Z3jF0dw==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/core": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/@reactflow/core/-/core-11.11.4.tgz", + "integrity": "sha512-H4vODklsjAq3AMq6Np4LE12i1I4Ta9PrDHuBR9GmL8uzTt2l2jh4CiQbEMpvMDcp7xi4be0hgXj+Ysodde/i7Q==", + "dependencies": { + "@types/d3": "^7.4.0", + "@types/d3-drag": "^3.0.1", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/minimap": { + "version": "11.7.14", + "resolved": "https://registry.npmjs.org/@reactflow/minimap/-/minimap-11.7.14.tgz", + "integrity": "sha512-mpwLKKrEAofgFJdkhwR5UQ1JYWlcAAL/ZU/bctBkuNTT1yqV+y0buoNVImsRehVYhJwffSWeSHaBR5/GJjlCSQ==", + "dependencies": { + "@reactflow/core": "11.11.4", + "@types/d3-selection": "^3.0.3", + "@types/d3-zoom": "^3.0.1", + "classcat": "^5.0.3", + "d3-selection": "^3.0.0", + "d3-zoom": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-resizer": { + "version": "2.2.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-resizer/-/node-resizer-2.2.14.tgz", + "integrity": "sha512-fwqnks83jUlYr6OHcdFEedumWKChTHRGw/kbCxj0oqBd+ekfs+SIp4ddyNU0pdx96JIm5iNFS0oNrmEiJbbSaA==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.4", + "d3-drag": "^3.0.0", + "d3-selection": "^3.0.0", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reactflow/node-toolbar": { + "version": "1.3.14", + "resolved": "https://registry.npmjs.org/@reactflow/node-toolbar/-/node-toolbar-1.3.14.tgz", + "integrity": "sha512-rbynXQnH/xFNu4P9H+hVqlEUafDCkEoCy0Dg9mG22Sg+rY/0ck6KkrAQrYrTgXusd+cEJOMK0uOOFCK2/5rSGQ==", + "dependencies": { + "@reactflow/core": "11.11.4", + "classcat": "^5.0.3", + "zustand": "^4.4.1" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -3511,6 +3643,16 @@ "@sinonjs/commons": "^1.7.0" } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -4291,6 +4433,228 @@ "@types/node": "*" } }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -4369,6 +4733,11 @@ "@types/send": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -4596,6 +4965,11 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -6397,6 +6771,11 @@ "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "license": "MIT" }, + "node_modules/classcat": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/classcat/-/classcat-5.0.5.tgz", + "integrity": "sha512-JhZUT7JFcQy/EzW605k/ktHtncoo9vnyW/2GspNYwFlN1C/WmjuV/xtS04e9SOkL2sTdw0VAZ2UGCcQ9lR6p6w==" + }, "node_modules/clean-css": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", @@ -6429,6 +6808,14 @@ "wrap-ansi": "^7.0.0" } }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -7174,6 +7561,177 @@ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "peer": true }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "dependencies": { + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" + } + }, + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -7268,6 +7826,11 @@ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", "license": "MIT" }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, "node_modules/decode-named-character-reference": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", @@ -7979,6 +8542,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==" + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -10224,6 +10792,14 @@ "node": ">= 0.4" } }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -15835,6 +16411,28 @@ "react": ">=18" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", @@ -15966,6 +16564,23 @@ "node": ">=14.0.0" } }, + "node_modules/reactflow": { + "version": "11.11.4", + "resolved": "https://registry.npmjs.org/reactflow/-/reactflow-11.11.4.tgz", + "integrity": "sha512-70FOtJkUWH3BAOsN+LU9lCrKoKbtOPnz2uq0CV2PLdNSwxTXOhCbsZr50GmZ+Rtw3jx8Uv7/vBFtCGixLfd4Og==", + "dependencies": { + "@reactflow/background": "11.3.14", + "@reactflow/controls": "11.2.14", + "@reactflow/core": "11.11.4", + "@reactflow/minimap": "11.7.14", + "@reactflow/node-resizer": "2.2.14", + "@reactflow/node-toolbar": "1.3.14" + }, + "peerDependencies": { + "react": ">=17", + "react-dom": ">=17" + } + }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -16001,6 +16616,46 @@ "node": ">=8.10.0" } }, + "node_modules/recharts": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.0.tgz", + "integrity": "sha512-Z/m38DX3L73ExO4Tpc9/iZWHmHnlzWG4njQbxsF5aSjwqmHNDDIm0rdEBArkwsBvR8U6EirlEHiQNYWCVh9sGQ==", + "dependencies": { + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts/node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==" + }, + "node_modules/recharts/node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, "node_modules/recursive-readdir": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", @@ -16026,6 +16681,19 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -16222,6 +16890,11 @@ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "license": "MIT" }, + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==" + }, "node_modules/resolve": { "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", @@ -18153,6 +18826,11 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -18706,6 +19384,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -18806,6 +19492,27 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/victory-vendor": { + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, "node_modules/w3c-hr-time": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", @@ -19746,6 +20453,33 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zustand": { + "version": "4.5.7", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz", + "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==", + "dependencies": { + "use-sync-external-store": "^1.2.2" + }, + "engines": { + "node": ">=12.7.0" + }, + "peerDependencies": { + "@types/react": ">=16.8", + "immer": ">=9.0.6", + "react": ">=16.8" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + } + } + }, "node_modules/zwitch": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", diff --git a/frontend/package.json b/frontend/package.json index 154e417..fdc4a42 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,8 @@ "react-icons": "^5.5.0", "react-markdown": "^10.1.0", "react-scripts": "5.0.1", + "reactflow": "^11.11.4", + "recharts": "^3.8.0", "web-vitals": "^2.1.4" }, "scripts": { diff --git a/frontend/src/App.js b/frontend/src/App.js index b02cf78..be947d0 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -9,6 +9,7 @@ import { ProfilePage } from "./features/profile"; import { NodesPage } from "./features/nodes"; import { SkillsPage } from "./features/skills"; +import { AgentHarnessPage, AgentDrillDown } from "./features/agents"; import { getUserStatus, logout, getUserProfile } from "./services/apiService"; const Icon = ({ path, onClick, className }) => ( @@ -34,7 +35,7 @@ const [userId, setUserId] = useState(null); const [userProfile, setUserProfile] = useState(null); - const authenticatedPages = ["voice-chat", "swarm-control", "settings", "profile", "nodes", "skills"]; + const authenticatedPages = ["voice-chat", "swarm-control", "settings", "profile", "nodes", "skills", "agents-harness", "agents-drilldown"]; const pageToPath = { "home": "/", "voice-chat": "/voice", @@ -43,23 +44,29 @@ "profile": "/profile", "nodes": "/nodes", "skills": "/skills", - "login": "/login" + "login": "/login", + "agents-harness": "/agents" }; - const pathToPage = Object.fromEntries(Object.entries(pageToPath).map(([pk, pv]) => [pv, pk])); + + const getPageFromPath = (path) => { + if (path === "/agents") return "agents-harness"; + if (path.startsWith("/agents/drilldown/")) return "agents-drilldown"; + const pathToPage = Object.fromEntries(Object.entries(pageToPath).map(([pk, pv]) => [pv, pk])); + return pathToPage[path] || "home"; + }; // Sync state with URL on mount and handle popstate useEffect(() => { const handlePopState = () => { const path = window.location.pathname; - const page = pathToPage[path] || "home"; - setCurrentPage(page); + setCurrentPage(getPageFromPath(path)); }; window.addEventListener("popstate", handlePopState); // Initial sync const initialPath = window.location.pathname; - const initialPage = pathToPage[initialPath] || "home"; + const initialPage = getPageFromPath(initialPath); if (initialPage !== currentPage) { setCurrentPage(initialPage); } @@ -141,13 +148,22 @@ } }; - const handleNavigate = (page) => { - if (authenticatedPages.includes(page) && !isLoggedIn) { + const handleNavigate = (pageOrPath) => { + let targetPage = pageOrPath; + let targetPath = pageToPath[pageOrPath]; + + // If it's a raw path like /agents/drilldown/123 + if (pageOrPath.startsWith('/')) { + targetPath = pageOrPath; + targetPage = getPageFromPath(pageOrPath); + } + + if (authenticatedPages.includes(targetPage) && !isLoggedIn) { setCurrentPage("login"); window.history.pushState({}, "", pageToPath["login"]); } else { - setCurrentPage(page); - window.history.pushState({}, "", pageToPath[page] || "/"); + setCurrentPage(targetPage); + window.history.pushState({}, "", targetPath || "/"); } }; @@ -176,6 +192,11 @@ return ; case "skills": return ; + case "agents-harness": + return ; + case "agents-drilldown": + const drilldownId = window.location.pathname.split('/').pop(); + return ; case "login": return ; default: diff --git a/frontend/src/features/agents/components/AgentDrillDown.js b/frontend/src/features/agents/components/AgentDrillDown.js new file mode 100644 index 0000000..68c9898 --- /dev/null +++ b/frontend/src/features/agents/components/AgentDrillDown.js @@ -0,0 +1,429 @@ +import React, { useState, useEffect } from 'react'; +import ChatWindow from '../../chat/components/ChatWindow'; +import FileSystemNavigator from '../../../shared/components/FileSystemNavigator'; +import { getAgents, getSessionMessages, fetchWithAuth, updateAgentConfig, getUserConfig, clearSessionHistory, getSessionTokenStatus, getAgentTriggers, createAgentTrigger, deleteAgentTrigger, getUserAccessibleNodes } from '../../../services/apiService'; + +export default function AgentDrillDown({ agentId, onNavigate }) { + const [agent, setAgent] = useState(null); + const [chatHistory, setChatHistory] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [overrideText, setOverrideText] = useState(""); + + // Day 2 Configurations + const [activeTab, setActiveTab] = useState('config'); // workspace or config + const [editConfig, setEditConfig] = useState(null); + const [saving, setSaving] = useState(false); + const [userConfig, setUserConfig] = useState(null); + const [tokenUsage, setTokenUsage] = useState({ token_count: 0, token_limit: 0, percentage: 0 }); + const [clearing, setClearing] = useState(false); + const [triggers, setTriggers] = useState([]); + const [newTriggerType, setNewTriggerType] = useState('webhook'); + const [newCronValue, setNewCronValue] = useState('0 * * * *'); + const [creatingTrigger, setCreatingTrigger] = useState(false); + const [modalConfig, setModalConfig] = useState(null); + const [nodes, setNodes] = useState([]); + + useEffect(() => { + const loadConf = async () => { + try { + const conf = await getUserConfig(); + setUserConfig(conf); + } catch (e) {} + try { + const nList = await getUserAccessibleNodes(); + setNodes(nList); + } catch (e) {} + }; + loadConf(); + }, []); + + const fetchData = async () => { + try { + // Find agent + const allAgents = await getAgents(); + const found = allAgents.find(a => a.id === agentId); + if (!found) throw new Error("Agent not found"); + setAgent(found); + + // Populate form only on first load using the agent context + setEditConfig(prev => prev || { + name: found.template?.name || "", + system_prompt: found.template?.system_prompt_path || "", + max_loop_iterations: found.template?.max_loop_iterations || 20, + mesh_node_id: found.mesh_node_id || "", + provider_name: "" + }); + + // Fetch chat history if session exists + if (found.session_id) { + const historyResp = await getSessionMessages(found.session_id); + const formatted = (historyResp.messages || []).map(m => ({ + text: m.content, + isUser: m.sender === 'user', + reasoning: m.reasoning_content, + status: null, + sender: m.sender, + timestamp: m.created_at, + id: m.id, + tool_calls: m.tool_calls + })); + setChatHistory(formatted); + + try { + const usage = await getSessionTokenStatus(found.session_id); + setTokenUsage(usage); + } catch(e) {} + + try { + const tList = await getAgentTriggers(agentId); + setTriggers(tList); + } catch(e) {} + } + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchData(); + const interval = setInterval(fetchData, 3000); + return () => clearInterval(interval); + }, [agentId]); + + const handleInjectOverride = async (e) => { + e.preventDefault(); + if (!overrideText.trim() || !agent?.session_id) return; + + try { + await fetchWithAuth(`/agents/${agentId}/webhook`, { + method: "POST", + body: { override_prompt: overrideText } + }); + setOverrideText(""); + fetchData(); + } catch (err) { + setModalConfig({ title: 'Injection Failed', message: err.message, type: 'error' }); + } + }; + + const handleSaveConfig = async () => { + try { + setSaving(true); + const payload = { + name: editConfig.name, + system_prompt: editConfig.system_prompt, + max_loop_iterations: parseInt(editConfig.max_loop_iterations, 10) || 20, + mesh_node_id: editConfig.mesh_node_id || null + }; + if (editConfig.provider_name) { + payload.provider_name = editConfig.provider_name; + } + await updateAgentConfig(agentId, payload); + fetchData(); + setModalConfig({ title: 'Success', message: 'Configuration Saved Successfully!', type: 'success' }); + } catch (err) { + setModalConfig({ title: 'Save Failed', message: err.message, type: 'error' }); + } finally { + setSaving(false); + } + }; + + const handleAddTrigger = async () => { + try { + setCreatingTrigger(true); + const payload = { trigger_type: newTriggerType }; + if (newTriggerType === 'cron') payload.cron_expression = newCronValue; + await createAgentTrigger(agentId, payload); + fetchData(); + } catch (err) { + setModalConfig({ title: 'Trigger Failed', message: err.message, type: 'error' }); + } finally { + setCreatingTrigger(false); + } + }; + + const handleDeleteTrigger = async (triggerId) => { + try { + await deleteAgentTrigger(triggerId); + fetchData(); + } catch (err) { + setModalConfig({ title: 'Delete Failed', message: err.message, type: 'error' }); + } + }; + + if (loading && !agent) return ( +
+
+
+ ); + + if (error) return ( +
+ Error loading Agent Drilldown: {error} +
+ ); + + return ( +
+ + {/* Minimal Header */} +
+
+ +
+

{agent?.id?.split('-')[0]} Dashboard

+
+ + {agent?.status === 'active' && } + + + Status: {agent?.status} +
+
+
+
+ Node: {nodes.find(n => n.id === agent?.mesh_node_id)?.name || agent?.mesh_node_id || 'unassigned'} + Jail: {agent?.current_workspace_jail || '/tmp'} +
+
+ + {/* Main Content Area - 50/50 Split */} +
+ + {/* Left Pane: Chat Tracker */} +
+
+ +
+ Live Thought Process +
+
+ +
+ {agent?.session_id ? ( + + ) : ( +
+ No session bounds established for this agent. +
+ )} +
+ + {/* Inject Prompt Override */} +
+
+ setOverrideText(e.target.value)} + placeholder="Steer agent execution loop..." + className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-xl py-3 pl-4 pr-12 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 transition-all font-mono" + /> + +
+
+
+ + {/* Right Pane: Multi-Tab Container */} +
+ {/* Tab Header */} +
+ + +
+ + {/* Tab Body */} +
+ {activeTab === 'workspace' && ( + agent ? ( + + ) : ( +
+ Initializing workspace bridge... +
+ ) + )} + + {activeTab === 'config' && ( +
+ {/* Token & History Utilities */} +
+
+ Session Context Window +
+
+
+
+ {Math.round(tokenUsage.percentage)}% +
+
+
+ +
+
+ Agent Name + setEditConfig({...editConfig, name: e.target.value})} className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2.5 px-3 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 shadow-sm" placeholder="e.g. Documentation Assistant" /> +
+
+ Active LLM Provider + +
+
+
+ Target Mesh Node + +
+
+ Max Iterations + setEditConfig({...editConfig, max_loop_iterations: e.target.value})} className="w-full bg-white dark:bg-gray-950 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2.5 px-3 focus:outline-none focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100 shadow-sm" /> +
+
+
+ + {/* Execution Triggers Box */} +
+ Execution Triggers +
+ + {/* List existing triggers */} + {triggers.length > 0 && ( +
+ {triggers.map(t => ( +
+
+ {t.trigger_type} ID: {t.id.split('-')[0]} + + {t.trigger_type === 'cron' ? `Schedule: ${t.cron_expression}` : `Secret: ${t.webhook_secret}`} + +
+ +
+ ))} +
+ )} + + {/* Add new trigger form */} +
+
+ Type + +
+ + {newTriggerType === 'cron' && ( +
+ CRON Expr + setNewCronValue(e.target.value)} + className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 text-sm rounded-md py-2 px-3 font-mono focus:outline-none" + /> +
+ )} + + +
+
+
+ +
+ Core System Instruction Prompt (Modifiable Live) + +

Changes to the system prompt will be immediately picked up by the agent loop on its next invocation turn.

+
+
+ +
+
+ )} +
+
+ +
+ + {/* Modal Overlay Component */} + {modalConfig && ( +
+
+
+

{modalConfig.title}

+

{modalConfig.message}

+
+ +
+
+
+ )} +
+ ); +} diff --git a/frontend/src/features/agents/components/AgentHarnessPage.js b/frontend/src/features/agents/components/AgentHarnessPage.js new file mode 100644 index 0000000..a790bf8 --- /dev/null +++ b/frontend/src/features/agents/components/AgentHarnessPage.js @@ -0,0 +1,459 @@ +import React, { useState, useEffect } from 'react'; +import { getAgents, getAgentTelemetry, updateAgentStatus, deployAgent, deleteAgent, getUserConfig, getUserAccessibleNodes } from '../../../services/apiService'; +import { AreaChart, Area, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; + +// Polling interval in ms +const POLLING_INTERVAL = 5000; + +export default function AgentHarnessPage({ onNavigate }) { + const [agents, setAgents] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [showDeploy, setShowDeploy] = useState(false); + + const fetchAgents = async () => { + try { + const data = await getAgents(); + setAgents(data); + setError(null); + } catch (err) { + setError(err.message); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchAgents(); + const interval = setInterval(fetchAgents, POLLING_INTERVAL); + return () => clearInterval(interval); + }, []); + + return ( +
+
+ {/* Header Section */} +
+
+
+

+ Agent Dashboard +

+

Cortex System Orchestrator

+
+ {/* Deploy New Agent Button */} + +
+ + {error && ( +
+ {error} + +
+ )} + + {loading && agents.length === 0 ? ( +
+
+
+ Loading Agents... +
+
+ ) : ( +
+ {agents.map(agent => ( + + ))} + {agents.length === 0 && ( +
+ No active agents found in the system. +
+ )} +
+ )} + + {/* Deploy New Agent Modal */} + {showDeploy && ( + setShowDeploy(false)} + onDeployed={() => { + setShowDeploy(false); + fetchAgents(); + }} + /> + )} +
+
+ ); +} + +// ───────────────────────────────────────────────────────────── +// Deploy Agent Modal +// ───────────────────────────────────────────────────────────── +const DeployAgentModal = ({ onClose, onDeployed }) => { + const [form, setForm] = useState({ + name: '', + description: '', + system_prompt: '', + mesh_node_id: '', + max_loop_iterations: 20, + initial_prompt: '', + provider_name: '' // Dynamic LLM selection + }); + const [deploying, setDeploying] = useState(false); + const [result, setResult] = useState(null); + const [deployError, setDeployError] = useState(null); + const [userConfig, setUserConfig] = useState(null); + const [nodes, setNodes] = useState([]); + + useEffect(() => { + const loadConfig = async () => { + try { + const conf = await getUserConfig(); + setUserConfig(conf); + + const providers = Object.keys(conf?.effective?.llm?.providers || {}); + const defaultProvider = conf?.effective?.llm?.active_provider || (providers.length > 0 ? providers[0] : ''); + + setForm(f => ({ ...f, provider_name: defaultProvider })); + } catch (e) { + console.warn("Failed to load user config for provider setup", e); + } + try { + const fetchedNodes = await getUserAccessibleNodes(); + setNodes(fetchedNodes); + } catch (e) { + console.warn("Failed to load nodes", e); + } + }; + loadConfig(); + }, []); + + const handleDeploy = async (e) => { + e.preventDefault(); + if (!form.name.trim()) return; + setDeploying(true); + setDeployError(null); + try { + const res = await deployAgent({ + ...form, + mesh_node_id: form.mesh_node_id || null, + system_prompt: form.system_prompt || null, + initial_prompt: form.initial_prompt || null, + description: form.description || null, + provider_name: form.provider_name || null + }); + setResult(res); + setTimeout(() => onDeployed(), 1500); + } catch (err) { + setDeployError(err.message); + } finally { + setDeploying(false); + } + }; + + const update = (key, val) => setForm(prev => ({ ...prev, [key]: val })); + + return ( +
+
+
+
+
+

Deploy Agent

+

Configure a new autonomous instance to connect to the mesh.

+
+ +
+ + {result ? ( +
+
🚀
+

{result.message}

+

ID: {result.instance_id}

+
+ ) : ( +
+ {/* Name & Provider */} +
+
+ + update('name', e.target.value)} + placeholder="e.g. System Monitor, QA Tester" + className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100" + required + /> +
+
+ + +
+
+ + {/* Description */} +
+ + update('description', e.target.value)} + placeholder="Brief description of what this agent does" + className="w-full bg-gray-50 dark:bg-gray-900 border border-gray-300 dark:border-gray-700 rounded-lg px-4 py-2.5 text-sm focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 text-gray-900 dark:text-gray-100" + /> +
+ + {/* System Prompt */} +
+ +