diff --git a/.azure/PRE-DEPLOYMENT-CHECKLIST.md b/.azure/PRE-DEPLOYMENT-CHECKLIST.md index f9689336c..5b4085d5d 100644 --- a/.azure/PRE-DEPLOYMENT-CHECKLIST.md +++ b/.azure/PRE-DEPLOYMENT-CHECKLIST.md @@ -180,7 +180,7 @@ ## ✅ Sign-Off | Role | Name | Date | Signature | -|------|------|------|-----------| +| --- | --- | --- | --- | | Release Lead | — | 2026-06-01 | ✓ | | Architecture | — | 2026-06-01 | ✓ | | QA | — | 2026-06-01 | ✓ | diff --git a/.azure/PRODUCTION-RUNBOOK.md b/.azure/PRODUCTION-RUNBOOK.md index 5011c2b6d..5eb3ff41b 100644 --- a/.azure/PRODUCTION-RUNBOOK.md +++ b/.azure/PRODUCTION-RUNBOOK.md @@ -417,7 +417,7 @@ az monitor metrics alert create \ ## Contact & Escalation | Role | Contact | Availability | -|------|---------|--------------| +| --- | --- | --- | | On-Call Engineer | #oncall Slack | 24/7 | | Platform Lead | — | Business hours | | Architecture | — | Business hours + on-call | diff --git a/.github/hooks/scripts/requirements_security_gate.py b/.github/hooks/scripts/requirements_security_gate.py index 286cb130b..3ca5ff948 100644 --- a/.github/hooks/scripts/requirements_security_gate.py +++ b/.github/hooks/scripts/requirements_security_gate.py @@ -27,7 +27,10 @@ import subprocess import sys import tempfile -import tomllib +try: + import tomllib +except ModuleNotFoundError: # Python < 3.11 + import tomli as tomllib from typing import Any _REQ_PATTERN = re.compile(r"requirements[^/]*\.txt$|pyproject\.toml$", re.IGNORECASE) diff --git a/.github/workflows/agi-prune-cron.yml b/.github/workflows/agi-prune-cron.yml index 26845e49e..b5ebf3643 100644 --- a/.github/workflows/agi-prune-cron.yml +++ b/.github/workflows/agi-prune-cron.yml @@ -69,10 +69,10 @@ jobs: DRY=${PRUNE_DRY_RUN:-true} ARGS=() if [ -n "${QAI_AGI_PERSIST_DB:-}" ]; then - ARGS+=("--sqlite" "${QAI_AGI_PERSIST_DB}") + ARGS="$ARGS --sqlite ${QAI_AGI_PERSIST_DB} --keep-rows ${KEEP}" fi if [ -n "${QAI_AGI_PERSIST_PATH:-}" ]; then - ARGS+=("--jsonl" "${QAI_AGI_PERSIST_PATH}") + ARGS="$ARGS --jsonl ${QAI_AGI_PERSIST_PATH} --keep-last ${KEEP}" fi if [ -z "${QAI_AGI_PERSIST_DB:-}" ] && [ -z "${QAI_AGI_PERSIST_PATH:-}" ]; then echo "No persistence target configured (QAI_AGI_PERSIST_DB or QAI_AGI_PERSIST_PATH). Skipping." diff --git a/.github/workflows/agi-smoke.yml b/.github/workflows/agi-smoke.yml index e9f4f429c..90f765ff6 100644 --- a/.github/workflows/agi-smoke.yml +++ b/.github/workflows/agi-smoke.yml @@ -2,7 +2,7 @@ name: AGI smoke tests on: pull_request: - branches: [ main ] + branches: [main] workflow_dispatch: permissions: @@ -12,8 +12,19 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" HARDEN_RUNNER_SHA: 0634a2670c59f64b4a01f0f96f84700a4088b9f0 # v2.12.2 CHECKOUT_SHA: 11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - SETUP_PYTHON_SHA: a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + SETUP_PYTHON_SHA: a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 UPLOAD_ARTIFACT_SHA: 65c4c4a1ddee5b72f698fdd19549f0f0fb45cf08 # v4.6.0 + AGI_SMOKE_TESTS: >- + tests/test_agi_smoke.py + tests/test_local_agi_sse_integration.py + tests/test_agi_persistence_endpoint.py + tests/test_agi_persistence_auth.py + tests/test_agi_prune.py + tests/test_lmstudio_agi_integration.py + tests/test_lmstudio_mcp_agi_tools.py + tests/test_agi_stream_utils_js.py + tests/test_function_app_endpoints.py::TestChatWebAssets + tests/test_function_app_endpoints.py::TestAgiEndpoints concurrency: group: agi-smoke-${{ github.event.pull_request.number || github.ref }} @@ -49,20 +60,10 @@ jobs: timeout-minutes: 15 run: | set -euo pipefail - pytest --collect-only \ - tests/test_agi_smoke.py \ - tests/test_local_agi_sse_integration.py \ - tests/test_agi_persistence_endpoint.py \ - tests/test_agi_persistence_auth.py \ - tests/test_agi_prune.py \ - -q - pytest \ - tests/test_agi_smoke.py \ - tests/test_local_agi_sse_integration.py \ - tests/test_agi_persistence_endpoint.py \ - tests/test_agi_persistence_auth.py \ - tests/test_agi_prune.py \ - -v --tb=short --timeout=10 --maxfail=1 \ + read -r -a _smoke_tests <<< "${AGI_SMOKE_TESTS}" + pytest --collect-only "${_smoke_tests[@]}" -q + pytest "${_smoke_tests[@]}" \ + -v --tb=short --timeout=30 --maxfail=1 \ --junitxml=agi-smoke-junit.xml - name: Upload AGI smoke artifacts diff --git a/.github/workflows/api-health-smoke.yml b/.github/workflows/api-health-smoke.yml index 9241f53f2..f52db591a 100644 --- a/.github/workflows/api-health-smoke.yml +++ b/.github/workflows/api-health-smoke.yml @@ -173,18 +173,16 @@ jobs: echo "| strict smoke output | \`data_out/integration_smoke/status-strict.json\` |" } >> "$GITHUB_STEP_SUMMARY" else - echo "| integration_smoke.py --strict-endpoints | ❌ failed |" >> "$GITHUB_STEP_SUMMARY" + echo "| integration_smoke.py --strict-endpoints | ⊘ skipped (non-scheduled) |" fi - else - echo "| integration_smoke.py --strict-endpoints | ⊘ skipped (non-scheduled) |" >> "$GITHUB_STEP_SUMMARY" - fi - # contract_tests - if [ "${{ steps.contract_tests.outcome }}" = "success" ]; then - echo "| ci_orchestrator --integration-contract-tests | ✅ passed |" >> "$GITHUB_STEP_SUMMARY" - else - echo "| ci_orchestrator --integration-contract-tests | ❌ failed |" >> "$GITHUB_STEP_SUMMARY" - fi + # contract_tests + if [ "${{ steps.contract_tests.outcome }}" = "success" ]; then + echo "| ci_orchestrator --integration-contract-tests | ✅ passed |" + else + echo "| ci_orchestrator --integration-contract-tests | ❌ failed |" + fi + } >> "$GITHUB_STEP_SUMMARY" # Evaluate overall status and fail if any required test failed if [ "${{ steps.integration_smoke.outcome }}" != "success" ] || [ "${{ steps.contract_tests.outcome }}" != "success" ]; then @@ -202,5 +200,7 @@ jobs: exit 1 fi - echo "" >> "$GITHUB_STEP_SUMMARY" - echo "### ✅ All API health checks passed." >> "$GITHUB_STEP_SUMMARY" + { + echo "" + echo "### ✅ All API health checks passed." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/aria-bot-tests.yml b/.github/workflows/aria-bot-tests.yml index bdb05c0f3..126a179b1 100644 --- a/.github/workflows/aria-bot-tests.yml +++ b/.github/workflows/aria-bot-tests.yml @@ -6,6 +6,9 @@ on: pull_request: branches: ["main", "master"] +permissions: + contents: read + jobs: test: name: Run tests @@ -13,22 +16,25 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + persist-credentials: false - name: Set up Python uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | + set -euo pipefail python -m pip install --upgrade pip if [ -f requirements.txt ]; then python -m pip install -r requirements.txt - else - python -m pip install pytest fi + python -m pip install pytest - name: Run pytest run: | - python -m pytest -q tests/test_aria_bot.py + set -euo pipefail + python -m pytest tests -q --maxfail=5 diff --git a/.github/workflows/aria-tests.yml b/.github/workflows/aria-tests.yml index 74b033f75..2bff2f5b7 100644 --- a/.github/workflows/aria-tests.yml +++ b/.github/workflows/aria-tests.yml @@ -114,7 +114,8 @@ jobs: uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: egress-policy: audit - disable-sudo: true + # Playwright --with-deps installs browser system packages via apt (sudo). + disable-sudo: false - name: Checkout code uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 @@ -237,7 +238,7 @@ jobs: libxfixes3 \ libxrandr2 \ libgbm1 \ - libasound2 + libasound2t64 - name: Install Python dependencies run: | @@ -315,7 +316,7 @@ jobs: ports: - 4444:4444 - 5900:5900 - options: --shm-size=2gb + options: --shm-size=2gb --add-host=host.docker.internal:host-gateway steps: - name: Harden runner uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 @@ -350,6 +351,7 @@ jobs: echo $! > /tmp/aria_server.pid env: PYTHONPATH: ${{ github.workspace }} + ARIA_HOST: 0.0.0.0 - name: Wait for server run: | @@ -392,7 +394,7 @@ jobs: pytest tests/test_ui_selenium.py -v --tb=short env: PYTHONPATH: ${{ github.workspace }} - ARIA_SERVER_URL: http://localhost:8080 + ARIA_SERVER_URL: http://host.docker.internal:8080 SELENIUM_REMOTE_URL: http://localhost:4444/wd/hub - name: Stop Aria server diff --git a/.github/workflows/auto-assign-reviewers.yml b/.github/workflows/auto-assign-reviewers.yml index fb49f1cc3..a6ce297d6 100644 --- a/.github/workflows/auto-assign-reviewers.yml +++ b/.github/workflows/auto-assign-reviewers.yml @@ -43,9 +43,6 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} script: | - // Required helpers - const core = require('@actions/core'); - const pr = context.payload.pull_request; if (!pr) { core.info('No pull request context found; exiting.'); diff --git a/.github/workflows/autonomous-evolver.yml b/.github/workflows/autonomous-evolver.yml index 4140176f8..b6068abd3 100644 --- a/.github/workflows/autonomous-evolver.yml +++ b/.github/workflows/autonomous-evolver.yml @@ -48,7 +48,7 @@ jobs: - name: Validate numeric inputs run: | set -euo pipefail - + validate_threshold() { local value=$1 local name=$2 @@ -67,7 +67,7 @@ jobs: exit 1 fi } - + validate_threshold "${{ inputs.fitness_threshold }}" "fitness_threshold" validate_threshold "${{ inputs.stability_threshold }}" "stability_threshold" echo "✓ All inputs validated successfully" @@ -110,20 +110,20 @@ jobs: // Parse and validate inputs const fitness = Math.random() * 100; const stability = Math.random() * 100; - + const parsedFitnessThreshold = Number(process.env.FITNESS_THRESHOLD); const parsedStabilityThreshold = Number(process.env.STABILITY_THRESHOLD); - + if (!Number.isFinite(parsedFitnessThreshold) || parsedFitnessThreshold < 0 || parsedFitnessThreshold > 100) { throw new Error(`Invalid FITNESS_THRESHOLD: ${process.env.FITNESS_THRESHOLD}. Must be a number between 0 and 100.`); } if (!Number.isFinite(parsedStabilityThreshold) || parsedStabilityThreshold < 0 || parsedStabilityThreshold > 100) { throw new Error(`Invalid STABILITY_THRESHOLD: ${process.env.STABILITY_THRESHOLD}. Must be a number between 0 and 100.`); } - + const fitnessThreshold = parsedFitnessThreshold; const stabilityThreshold = parsedStabilityThreshold; - + const forceEvolutionStr = (process.env.FORCE_EVOLUTION || 'false').toLowerCase().trim(); if (!['true', 'false'].includes(forceEvolutionStr)) { throw new Error(`Invalid FORCE_EVOLUTION: ${process.env.FORCE_EVOLUTION}. Must be 'true' or 'false'.`); @@ -150,7 +150,7 @@ jobs: out.push(`fitness=${fitness.toFixed(4)}`); out.push(`stability=${stability.toFixed(4)}`); out.push(`reason=${reason}`); - + fs.appendFileSync(githubOutput, out.join('\n') + '\n'); console.log('✓ Evaluation outputs written successfully'); } catch (error) { diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 0b5ba69dd..f8a706c36 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -311,6 +311,7 @@ jobs: permissions: contents: write pull-requests: write + workflows: write steps: - name: Harden runner @@ -329,19 +330,21 @@ jobs: REF_NAME: ${{ github.ref_name }} run: | set -euo pipefail - ref_value="" - repo_value="" - can_push_value="false" - if [ "$EVENT_NAME" = "pull_request" ]; then - ref_value="$PR_HEAD_REF" - repo_value="$PR_HEAD_REPO" - if [ "$PR_HEAD_REPO" = "$REPOSITORY" ]; then - can_push_value="true" + { + if [ "$EVENT_NAME" = "pull_request" ]; then + echo "ref=$PR_HEAD_REF" + echo "repo=$PR_HEAD_REPO" + if [ "$PR_HEAD_REPO" = "$REPOSITORY" ]; then + echo "can_push=true" + else + echo "can_push=false" + fi + else + echo "ref=$REF_NAME" + echo "repo=$REPOSITORY" + echo "can_push=false" fi - else - ref_value="$REF_NAME" - repo_value="$REPOSITORY" - fi + } >> "$GITHUB_OUTPUT" { echo "ref=$ref_value" diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 295db9690..9cbf58f5d 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -227,7 +227,7 @@ jobs: else echo "MISSING: $f" >&3 fi - done + done >> "$REPORT" echo >&3 diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index bbc147e95..aa6314d84 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -85,7 +85,8 @@ jobs: uses: step-security/harden-runner@0634a2670c59f64b4a01f0f96f84700a4088b9f0 with: egress-policy: audit - disable-sudo: true + # Playwright --with-deps installs browser system packages via apt (sudo). + disable-sudo: false - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 with: persist-credentials: false diff --git a/.github/workflows/integration-contract-gate.yml b/.github/workflows/integration-contract-gate.yml index 30cd03f7e..93c50223b 100644 --- a/.github/workflows/integration-contract-gate.yml +++ b/.github/workflows/integration-contract-gate.yml @@ -157,7 +157,7 @@ jobs: - name: Start local Functions host (for endpoint checks) id: start-func - if: runner.os == 'Linux' + if: runner.os == 'Linux' && steps.mode.outputs.strict == 'true' env: INTEGRATION_AI_STATUS_ENDPOINT: ${{ env.INTEGRATION_AI_STATUS_ENDPOINT }} RETRY_COUNT: ${{ env.RETRY_COUNT }} @@ -183,7 +183,7 @@ jobs: echo "AI status endpoint reachable: ${INTEGRATION_AI_STATUS_ENDPOINT}" exit 0 fi - ((++i)) + i=$((i + 1)) echo "Attempt ${i}/${RETRY_COUNT} — endpoint not ready yet; sleeping ${RETRY_INTERVAL}s" sleep "${RETRY_INTERVAL}" done diff --git a/SECURITY.md b/SECURITY.md index 2ff7161df..5e0820d36 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -24,13 +24,13 @@ Thank you for helping keep **Aria** and its users safe. This policy explains how Security fixes are provided only for versions listed below. If you’re running an unsupported release, please [upgrade](https://github.com/Bryan-Roe/Aria/releases) before reporting a vulnerability. -| Version | Supported | Notes | -| --------------- | ------------------ | --------------------------------------------- | -| latest (`main`) | :white_check_mark: | Active development branch | -| 5.1.x | :white_check_mark: | Current stable release | -| 5.0.x | :x: | End of life | -| 4.0.x | :white_check_mark: | Long-term support (critical fixes only) | -| < 4.0 | :x: | Unsupported — please upgrade | +| Version | Supported | Notes | +| --- | --- | --- | +| latest (`main`) | :white_check_mark: | Active development branch | +| 5.1.x | :white_check_mark: | Current stable release | +| 5.0.x | :x: | End of life | +| 4.0.x | :white_check_mark: | Long-term support (critical fixes only) | +| < 4.0 | :x: | Unsupported — please upgrade | Unsure which version you’re using? Review the [release notes](https://github.com/Bryan-Roe/Aria/releases). @@ -68,12 +68,12 @@ To help us confirm, triage, and fix the issue faster, please include: We strive to handle reports using a coordinated process and keep reporters informed. -| Stage | Target Response Time | -| ------------------------ | ----------------------------------------------------- | -| Initial acknowledgment | Within **3 business days** | -| Triage & assessment | Within **7 business days** | -| Progress updates | At least every **7 days** until resolution | -| Fix/disclosure | Typically within **90 days**, depending on severity | +| Stage | Target Response Time | +| --- | --- | +| Initial acknowledgment | Within **3 business days** | +| Triage & assessment | Within **7 business days** | +| Progress updates | At least every **7 days** until resolution | +| Fix/disclosure | Typically within **90 days**, depending on severity | After triage, you’ll be notified if the report is accepted, declined, or needs more info. Accepted reports proceed to remediation and coordinated disclosure. diff --git a/SMOKE_TESTS_AGI_ENDPOINTS.md b/SMOKE_TESTS_AGI_ENDPOINTS.md index d9a9cf9c0..3a3857648 100644 --- a/SMOKE_TESTS_AGI_ENDPOINTS.md +++ b/SMOKE_TESTS_AGI_ENDPOINTS.md @@ -87,9 +87,9 @@ The `/api/agi/status` endpoint response includes: ## Troubleshooting -| Issue | Solution | -| ------------------------------------ | --------------------------------------------------------------- | -| `curl: (7) Failed to connect` | Ensure `func host start` is running on port 7071 | -| `agent_tools` field missing | Check `agi_provider._AGENT_REGISTRY` is accessible | -| Tool names not sorted | Verify `sorted(set(...))` logic in `function_app.py` agi_status | -| Non-string tools in list | Check `agi_provider.py` tool configuration structure | +| Issue | Solution | +| --- | --- | +| `curl: (7) Failed to connect` | Ensure `func host start` is running on port 7071 | +| `agent_tools` field missing | Check `agi_provider._AGENT_REGISTRY` is accessible | +| Tool names not sorted | Verify `sorted(set(...))` logic in `function_app.py` agi_status | +| Non-string tools in list | Check `agi_provider.py` tool configuration structure | diff --git a/ai-projects/chat-cli/src/agi_provider.py b/ai-projects/chat-cli/src/agi_provider.py index 3f6b8a938..bc69b2852 100644 --- a/ai-projects/chat-cli/src/agi_provider.py +++ b/ai-projects/chat-cli/src/agi_provider.py @@ -349,6 +349,19 @@ def complete( ], "description": "Produces meta-learning insights and retrospective analysis from cycle history", }, + "infrastructure-specialist": { + "domains": ["infrastructure"], + "intents": ["explanation", "question", "coding", "creation", "debugging"], + "provider": "agi", + "confidence_boost": 0.18, + "subtask_templates": [ + "Map the deployment or runtime topology involved", + "Identify configuration, networking, and auth constraints", + "Propose the smallest safe infrastructure change", + "List verification steps and rollback criteria", + ], + "description": "DevOps, deployment, CI/CD, and cloud infrastructure specialist", + }, "general": { "domains": [], "intents": [], @@ -725,7 +738,7 @@ def _analyze_query(self, query: str) -> Dict[str, Any]: ``creation``, ``question``, ``general``. Domain categories: - ``quantum``, ``aria``, ``ai``, ``technical``, ``general``. + ``quantum``, ``aria``, ``ai``, ``technical``, ``infrastructure``, ``general``. Parameters ---------- @@ -831,6 +844,26 @@ def _analyze_query(self, query: str) -> Dict[str, Any]: ] ): domain = "ai" + elif any( + w in query_lower + for w in [ + "deploy", + "deployment", + "kubernetes", + "k8s", + "docker", + "terraform", + "bicep", + "pipeline", + "ci/cd", + "github actions", + "azure functions", + "infrastructure", + "devops", + "hosting", + ] + ): + domain = "infrastructure" elif any( w in query_lower for w in [ @@ -1129,6 +1162,10 @@ def _chain_of_thought(self, query: str, analysis: Dict[str, Any], messages: List thoughts.append("AI/ML context: distinguish training, inference, and evaluation concerns.") elif domain == "technical": thoughts.append("Technical context: prefer concrete, runnable code examples.") + elif domain == "infrastructure": + thoughts.append( + "Infrastructure context: map runtime topology, auth, and rollback before proposing changes." + ) # Surface which specialist will handle this query. selected_agent = analysis.get("selected_agent") @@ -1300,6 +1337,13 @@ def _build_agi_system_prompt(self, analysis: Dict[str, Any], reasoning_chain: Li "Recommend concrete, actionable behavioural adjustments.", "Close with a one-sentence executive summary of the overall insight.", ] + elif selected_agent == "infrastructure-specialist": + lines += [ + "You are acting as the Infrastructure Specialist.", + "Map deployment topology, CI/CD stages, secrets handling, and rollback paths.", + "Prefer the smallest safe change with explicit verification steps.", + "Call out environment-specific constraints (Azure, GitHub Actions, containers).", + ] else: # General / fallback — keep existing aria domain guidance if domain == "aria": @@ -1379,6 +1423,7 @@ def _generate_response( "debate-specialist": 0.6, "hypothesis-specialist": 0.5, "reflection-specialist": 0.4, + "infrastructure-specialist": 0.25, } original_temp = getattr(provider, "temperature", None) if selected_agent in _AGENT_TEMPERATURES: @@ -1779,7 +1824,8 @@ def _ensure_parent_dir(path: str) -> None: os.makedirs(parent, exist_ok=True) # Optionally attach a persistence backend if configured via environment. - persist_enabled = os.getenv("QAI_AGI_PERSIST", "").lower() in ("1", "true", "yes") + persist_env = os.getenv("QAI_AGI_PERSIST", "true") + persist_enabled = persist_env.lower() in ("1", "true", "yes") jsonl_path = os.getenv("QAI_AGI_PERSIST_PATH") sqlite_path = os.getenv("QAI_AGI_PERSIST_DB") or os.getenv("QAI_AGI_PERSIST_SQLITE") if sqlite_path: diff --git a/ai-projects/lmstudio-mcp/agi_mcp_tools.py b/ai-projects/lmstudio-mcp/agi_mcp_tools.py new file mode 100644 index 000000000..8fa9e9d05 --- /dev/null +++ b/ai-projects/lmstudio-mcp/agi_mcp_tools.py @@ -0,0 +1,135 @@ +"""AGI provider helpers exposed as LM Studio MCP tools.""" + +from __future__ import annotations + +import sys +from pathlib import Path +from typing import Any + + +def _repo_root() -> Path: + return Path(__file__).resolve().parents[2] + + +def _load_agi_factory(): + root = _repo_root() + root_str = str(root) + if root_str not in sys.path: + sys.path.insert(0, root_str) + + from agi_provider import create_agi_provider + + return create_agi_provider + + +def _build_messages(query: str | None, messages: list[dict[str, Any]] | None) -> list[dict[str, str]]: + if isinstance(messages, list) and messages: + return [ + {"role": str(item.get("role", "user")), "content": str(item.get("content", ""))} + for item in messages + if isinstance(item, dict) + ] + if isinstance(query, str) and query.strip(): + return [{"role": "user", "content": query.strip()}] + raise ValueError("Provide either a non-empty query or a messages array") + + +def _normalize_agi_stream_delta(chunk) -> dict[str, Any]: + if isinstance(chunk, dict): + return chunk + return {"type": "output", "data": str(chunk)} + + +def _provider_payload(provider, choice) -> dict[str, Any]: + base = getattr(provider, "_base_provider_choice", None) + if base is not None: + base_provider = getattr(base, "name", None) + base_model = getattr(base, "model", None) + else: + base_provider = getattr(choice, "name", None) + base_model = getattr(choice, "model", None) + return { + "name": "agi", + "base_provider": base_provider, + "base_model": base_model, + "wrapper_model": getattr(choice, "model", None), + } + + +def run_agi_analyze(query: str) -> dict[str, Any]: + """Analyze a query with AGI routing metadata.""" + if not isinstance(query, str) or not query.strip(): + raise ValueError("query is required") + + create_agi_provider = _load_agi_factory() + provider, choice = create_agi_provider() + + analysis = provider._analyze_query(query.strip()) + selected_agent, agent_score = provider._select_agent(analysis) + + return { + "success": True, + "query": query.strip(), + "analysis": analysis, + "routing": { + "selected_agent": selected_agent, + "agent_score": float(agent_score), + }, + "provider": _provider_payload(provider, choice), + } + + +def run_agi_reason( + query: str | None = None, + messages: list[dict[str, Any]] | None = None, + *, + include_reasoning_summary: bool = True, +) -> dict[str, Any]: + """Run full AGI completion and return response payload.""" + chat_messages = _build_messages(query, messages) + create_agi_provider = _load_agi_factory() + provider, choice = create_agi_provider() + result = provider.complete(chat_messages, stream=False) + if hasattr(result, "__iter__") and not isinstance(result, str): + result = "".join(str(chunk) for chunk in result) + + payload: dict[str, Any] = { + "success": True, + "response": str(result), + "provider": _provider_payload(provider, choice), + } + if include_reasoning_summary: + payload["reasoning"] = provider.get_reasoning_summary() + return payload + + +def run_agi_stream( + query: str | None = None, + messages: list[dict[str, Any]] | None = None, + *, + include_deltas: bool = True, +) -> dict[str, Any]: + """Run AGI streaming completion and return structured deltas plus full text.""" + chat_messages = _build_messages(query, messages) + create_agi_provider = _load_agi_factory() + provider, choice = create_agi_provider() + gen = provider.complete(chat_messages, stream=True) + + _non_output_types = frozenset({"analysis", "step", "payload", "error"}) + deltas: list[dict[str, Any]] = [] + text_parts: list[str] = [] + for chunk in gen: + delta = _normalize_agi_stream_delta(chunk) + if include_deltas: + deltas.append(delta) + if delta.get("type") not in _non_output_types: + text_parts.append(str(delta.get("data", ""))) + + payload: dict[str, Any] = { + "success": True, + "response": "".join(text_parts), + "provider": _provider_payload(provider, choice), + } + if include_deltas: + payload["deltas"] = deltas + return payload diff --git a/ai-projects/lmstudio-mcp/lmstudio_mcp_server.py b/ai-projects/lmstudio-mcp/lmstudio_mcp_server.py index a7a6c4e42..ffe604fd2 100644 --- a/ai-projects/lmstudio-mcp/lmstudio_mcp_server.py +++ b/ai-projects/lmstudio-mcp/lmstudio_mcp_server.py @@ -7,6 +7,7 @@ - Querying available models - Sending chat completions to local LM Studio instance - Managing model context and parameters +- AGI analyze/reason helpers for multi-agent routing """ import asyncio @@ -16,29 +17,29 @@ import sys from typing import Any, Dict, List, Optional +MCP_AVAILABLE = True try: from mcp.server import Server # type: ignore from mcp.server.stdio import stdio_server # type: ignore from mcp.types import TextContent, Tool # type: ignore except ImportError: - print("Error: MCP package not installed.") - print("Install with: pip install 'mcp>=0.9.0'") - sys.exit(1) + MCP_AVAILABLE = False + Server = None # type: ignore[assignment,misc] + stdio_server = None # type: ignore[assignment,misc] + TextContent = None # type: ignore[assignment,misc] + Tool = None # type: ignore[assignment,misc] try: import httpx except ImportError: - print("Error: httpx package not installed.") - print("Install with: pip install httpx") - sys.exit(1) + httpx = None # type: ignore[assignment] + +from agi_mcp_tools import run_agi_analyze, run_agi_reason, run_agi_stream # Setup logging logging.basicConfig(level=logging.INFO) logger = logging.getLogger(__name__) -# Initialize MCP server -app = Server("lmstudio-mcp") - # Default configuration DEFAULT_BASE_URL = "http://127.0.0.1:1234/v1" DEFAULT_MODEL = "local-model" @@ -57,6 +58,9 @@ def __init__( max_tokens: int = DEFAULT_MAX_TOKENS, timeout: float = 30.0, ): + if httpx is None: + raise RuntimeError("httpx package not installed. Install with: pip install httpx") + self.base_url = base_url.rstrip("/") self.model = model self.temperature = temperature @@ -128,28 +132,26 @@ async def chat_completion( data = response.json() if stream: - # For streaming, return the raw response data return { "success": True, "stream": True, "response": data, "model": model, } - else: - # Extract the message from non-streaming response - choice = data.get("choices", [{}])[0] - message = choice.get("message", {}).get("content", "") - stop_reason = choice.get("finish_reason", "unknown") - return { - "success": True, - "stream": False, - "message": message, - "stop_reason": stop_reason, - "model": model, - "usage": data.get("usage", {}), - "response": data, - } + choice = data.get("choices", [{}])[0] + message = choice.get("message", {}).get("content", "") + stop_reason = choice.get("finish_reason", "unknown") + + return { + "success": True, + "stream": False, + "message": message, + "stop_reason": stop_reason, + "model": model, + "usage": data.get("usage", {}), + "response": data, + } except httpx.HTTPError as e: return { "success": False, @@ -162,7 +164,6 @@ async def chat_completion( async def get_server_status(self) -> Dict[str, Any]: """Get server status information.""" try: - # Try to get models as a status check response = await self.client.get(f"{self.base_url}/models") response.raise_for_status() data = response.json() @@ -194,7 +195,6 @@ def get_client() -> LMStudioClient: if _client is None: base_url = os.getenv("LMSTUDIO_BASE_URL", DEFAULT_BASE_URL) model = os.getenv("LMSTUDIO_MODEL", DEFAULT_MODEL) - # Ensure environment defaults are strings before casting temperature = float(os.getenv("LMSTUDIO_TEMPERATURE", str(DEFAULT_TEMPERATURE))) max_tokens = int(os.getenv("LMSTUDIO_MAX_TOKENS", str(DEFAULT_MAX_TOKENS))) @@ -207,147 +207,207 @@ def get_client() -> LMStudioClient: return _client -@app.list_tools() -async def list_tools() -> List[Tool]: - """List available tools.""" - return [ - Tool( - name="list_models", - description="List all available models on the LM Studio server", - inputSchema={ - "type": "object", - "properties": {}, - "required": [], - }, - ), - Tool( - name="chat_completion", - description="Send a chat completion request to LM Studio", - inputSchema={ - "type": "object", - "properties": { - "messages": { - "type": "array", - "items": { - "type": "object", - "properties": { - "role": { - "type": "string", - "enum": ["system", "user", "assistant"], - "description": "Message role", - }, - "content": { - "type": "string", - "description": "Message content", +def _register_mcp_handlers(server: "Server") -> None: + @server.list_tools() + async def list_tools() -> List[Tool]: + """List available tools.""" + return [ + Tool( + name="list_models", + description="List all available models on the LM Studio server", + inputSchema={ + "type": "object", + "properties": {}, + "required": [], + }, + ), + Tool( + name="chat_completion", + description="Send a chat completion request to LM Studio", + inputSchema={ + "type": "object", + "properties": { + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["system", "user", "assistant"], + "description": "Message role", + }, + "content": { + "type": "string", + "description": "Message content", + }, }, + "required": ["role", "content"], }, - "required": ["role", "content"], + "description": "List of messages for the chat", + }, + "model": { + "type": "string", + "description": "Model ID (uses default if not specified)", + }, + "temperature": { + "type": "number", + "description": "Sampling temperature (0.0-2.0, default 0.7)", + }, + "max_tokens": { + "type": "integer", + "description": "Maximum tokens in response (default 2048)", }, - "description": "List of messages for the chat", }, - "model": { - "type": "string", - "description": "Model ID (uses default if not specified)", + "required": ["messages"], + }, + ), + Tool( + name="server_status", + description="Get LM Studio server status and configuration", + inputSchema={ + "type": "object", + "properties": {}, + "required": [], + }, + ), + Tool( + name="agi_analyze", + description="Analyze a query with the AGI provider (intent, domain, routing preview)", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "User query to analyze", + } }, - "temperature": { - "type": "number", - "description": "Sampling temperature (0.0-2.0, default 0.7)", + "required": ["query"], + }, + ), + Tool( + name="agi_reason", + description="Run full AGI reasoning completion for a query or message list", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Single-turn user query", + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["role", "content"], + }, + "description": "Optional chat-style message history", + }, + "include_reasoning_summary": { + "type": "boolean", + "description": "Include AGI reasoning summary metadata", + }, }, - "max_tokens": { - "type": "integer", - "description": "Maximum tokens in response (default 2048)", + }, + ), + Tool( + name="agi_stream", + description="Run AGI streaming completion and return structured deltas plus full text", + inputSchema={ + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Single-turn user query", + }, + "messages": { + "type": "array", + "items": { + "type": "object", + "properties": { + "role": {"type": "string"}, + "content": {"type": "string"}, + }, + "required": ["role", "content"], + }, + "description": "Optional chat-style message history", + }, + "include_deltas": { + "type": "boolean", + "description": "Include structured stream deltas in the response", + }, }, }, - "required": ["messages"], - }, - ), - Tool( - name="server_status", - description="Get LM Studio server status and configuration", - inputSchema={ - "type": "object", - "properties": {}, - "required": [], - }, - ), - ] - - -@app.call_tool() -async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: - """Call a tool.""" - client = get_client() + ), + ] - try: - if name == "list_models": - result = await client.list_models() - return [ - TextContent( - type="text", - text=json.dumps(result, indent=2), - ) - ] - - elif name == "chat_completion": - messages = arguments.get("messages", []) - if not messages: - return [ - TextContent( - type="text", - text=json.dumps({"error": "No messages provided"}, indent=2), + @server.call_tool() + async def call_tool(name: str, arguments: Dict[str, Any]) -> List[TextContent]: + """Call a tool.""" + client = get_client() + + try: + if name == "list_models": + result = await client.list_models() + elif name == "chat_completion": + messages = arguments.get("messages", []) + if not messages: + result = {"error": "No messages provided"} + else: + result = await client.chat_completion( + messages=messages, + model=arguments.get("model"), + temperature=arguments.get("temperature"), + max_tokens=arguments.get("max_tokens"), + stream=False, ) - ] - - model = arguments.get("model") - temperature = arguments.get("temperature") - max_tokens = arguments.get("max_tokens") - - result = await client.chat_completion( - messages=messages, - model=model, - temperature=temperature, - max_tokens=max_tokens, - stream=False, - ) - return [ - TextContent( - type="text", - text=json.dumps(result, indent=2), + elif name == "server_status": + result = await client.get_server_status() + elif name == "agi_analyze": + result = run_agi_analyze(str(arguments.get("query", ""))) + elif name == "agi_reason": + result = run_agi_reason( + query=arguments.get("query"), + messages=arguments.get("messages"), + include_reasoning_summary=bool( + arguments.get("include_reasoning_summary", True) + ), ) - ] - - elif name == "server_status": - result = await client.get_server_status() - return [ - TextContent( - type="text", - text=json.dumps(result, indent=2), + elif name == "agi_stream": + result = run_agi_stream( + query=arguments.get("query"), + messages=arguments.get("messages"), + include_deltas=bool(arguments.get("include_deltas", True)), ) - ] + else: + result = {"error": f"Unknown tool: {name}"} - else: - return [ - TextContent( - type="text", - text=json.dumps({"error": f"Unknown tool: {name}"}, indent=2), - ) - ] + return [TextContent(type="text", text=json.dumps(result, indent=2))] + except Exception as e: + logger.error(f"Tool execution error: {e}") + return [TextContent(type="text", text=json.dumps({"error": str(e)}, indent=2))] - except Exception as e: - logger.error(f"Tool execution error: {e}") - return [ - TextContent( - type="text", - text=json.dumps({"error": str(e)}, indent=2), - ) - ] + +if MCP_AVAILABLE: + app = Server("lmstudio-mcp") + _register_mcp_handlers(app) +else: + app = None async def main(): """Run the MCP server.""" + if not MCP_AVAILABLE: + print("Error: MCP package not installed.") + print("Install with: pip install 'mcp>=0.9.0'") + sys.exit(1) + client = get_client() - # Check connection on startup logger.info(f"Connecting to LM Studio at {client.base_url}") connected = await client.check_connection() @@ -359,7 +419,6 @@ async def main(): logger.warning(f"⚠ Could not connect to LM Studio at {client.base_url}") logger.info("Make sure LM Studio is running and the local server is enabled.") - # Start MCP server async with stdio_server() as (read_stream, write_stream): await app.run(read_stream, write_stream, app.create_initialization_options()) diff --git a/apps/aria/aria_controller.js b/apps/aria/aria_controller.js index f921a47ac..9949d9673 100644 --- a/apps/aria/aria_controller.js +++ b/apps/aria/aria_controller.js @@ -449,6 +449,7 @@ const expressions = { }; function log(message, isError = false) { + if (!logContainer) return; const entry = document.createElement('div'); entry.className = 'log-entry'; if (isError) entry.classList.add('log-error'); @@ -1474,11 +1475,13 @@ function stopIdleAnimation() { // Limb movement helpers function moveArm(arm, angle, duration = 500) { + if (!arm) return; arm.style.transition = `transform ${duration}ms ease-in-out`; arm.style.transform = `rotate(${angle}deg)`; } function moveLeg(leg, angle, duration = 500) { + if (!leg) return; leg.style.transition = `transform ${duration}ms ease-in-out`; leg.style.transform = `rotate(${angle}deg)`; } diff --git a/apps/aria/server.py b/apps/aria/server.py index 14b577f59..fcc60a373 100644 --- a/apps/aria/server.py +++ b/apps/aria/server.py @@ -334,6 +334,17 @@ def build_health_payload( "timestamp": datetime.datetime.now(timezone.utc).isoformat(), "llm_available": bool(llm), "model_loaded": bool(model_flag), + "agi_provider_supported": bool(llm), + "supported_providers": [ + "auto", + "local", + "ollama", + "lmstudio", + "azure", + "openai", + "quantum", + "agi", + ], "counts": { "objects": len(objects) if isinstance(objects, dict) else 0, "action_types": len(ARIA_ACTIONS), @@ -435,6 +446,8 @@ def _normalize_provider_alias(explicit: Optional[str]) -> Optional[str]: "quantum-llm": "quantum", "quantum_llm": "quantum", "azure_openai": "azure", + "agi-reasoning": "agi", + "agi_reasoning": "agi", } return alias_map.get(normalized, normalized) diff --git a/apps/chat/chat.js b/apps/chat/chat.js index 598bd8e43..7514f8efc 100644 --- a/apps/chat/chat.js +++ b/apps/chat/chat.js @@ -5,6 +5,9 @@ console.log('chat.js loaded - v2025-11-21-qai - Provider: QAI auto-detect with q const AI_BASE = ''; const API_BASE = `/api/chat`; const STREAM_API = `/api/chat/stream`; +const AGI_STREAM_API = `/api/agi/stream`; +const AGI_REASON_API = `/api/agi/reason`; +const AGI_ANALYZE_API = `/api/agi/analyze`; const STATUS_API = `/api/ai/status`; const QUANTUM_CLASSIFY_API = '/api/quantum/classify'; const QUANTUM_CIRCUIT_API = '/api/quantum/circuit'; @@ -17,6 +20,7 @@ let isProcessing = false; let messageCounter = 0; let currentProvider = 'auto'; // Always use auto-detect for best available let quantumMode = false; // Quantum enhancement toggle +let agiMode = false; // AGI reasoning pipeline toggle let systemStatus = null; let retryCount = 0; const MAX_RETRIES = 3; @@ -59,6 +63,7 @@ const exportButton = document.getElementById('exportButton'); const importButton = document.getElementById('importButton'); const toggleThemeButton = document.getElementById('toggleThemeButton'); const quantumModeButton = document.getElementById('quantumModeButton'); +const agiModeButton = document.getElementById('agiModeButton'); const quantumPanel = document.getElementById('quantumPanel'); const quantumPanelClose = document.getElementById('quantumPanelClose'); const quantumIndicator = document.getElementById('quantumIndicator'); @@ -85,13 +90,68 @@ let uploadedImageBase64 = null; let ariaAvatarGenerated = false; let ariaAvatarUrl = null; +function emitEmbeddedChatEvent(name, detail) { + if (!window.ARIA_EMBEDDED) return; + try { + document.dispatchEvent(new CustomEvent(name, { detail: detail || {} })); + } catch (e) { + /* ignore */ + } +} + +function notifyEmbeddedAssistant(text) { + if (!text) return; + emitEmbeddedChatEvent('aria-chat-assistant', { text: String(text) }); +} + +function initEmbeddedTransport() { + console.log('chat.js: ARIA_EMBEDDED — wiring chat transport (send + AGI)'); + + if (messageInput) { + messageInput.addEventListener('input', () => { + messageInput.style.height = 'auto'; + messageInput.style.height = Math.min(messageInput.scrollHeight, 150) + 'px'; + }); + messageInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + sendMessage(); + } + }); + } + + if (sendButton) sendButton.addEventListener('click', sendMessage); + if (agiModeButton) agiModeButton.addEventListener('click', toggleAgiMode); + if (cancelStreamBtn) { + cancelStreamBtn.addEventListener('click', function () { + if (activeAbortController) { + activeAbortController.abort(); + updateStatus('Streaming cancelled'); + } + cancelStreamBtn.style.display = 'none'; + }); + } + + loadFromStorage(); + // Embedded page has no stream toggle UI — prefer AGI SSE streaming. + streamEnabled = true; + fetchSystemStatus(); + + window.__ariaChatTransport = { + sendMessage: sendMessage, + getAgiMode: function () { + return agiMode; + }, + toggleAgiMode: toggleAgiMode, + }; +} + // Initialize document.addEventListener('DOMContentLoaded', () => { - // When the Aria interactive page embeds chat, it provides its own - // send/receive wiring and should avoid double-wiring the controls - // in this script. A page can opt-in by setting window.ARIA_EMBEDDED=true + // Embedded Aria page keeps character/lip-sync in index.html but uses + // chat.js for send routing (including AGI stream) to avoid drift. if (window && window.ARIA_EMBEDDED) { - console.log('chat.js: ARIA_EMBEDDED detected — skipping default UI wiring'); + initEmbeddedTransport(); return; } if (cancelStreamBtn) { @@ -137,7 +197,7 @@ document.addEventListener('DOMContentLoaded', () => { } }); - sendButton.addEventListener('click', sendMessage); + if (sendButton) sendButton.addEventListener('click', sendMessage); // Vision upload handlers if (visionUploadButton) { @@ -174,54 +234,69 @@ document.addEventListener('DOMContentLoaded', () => { } }, 2000); - newChatButton.addEventListener('click', () => { - console.log('New Chat button clicked'); - newChat(); - }); - clearButton.addEventListener('click', () => { - console.log('Clear button clicked'); - clearChat(false); - }); - exportButton.addEventListener('click', exportChat); - importButton.addEventListener('click', importChat); - toggleThemeButton.addEventListener('click', toggleTheme); - quantumModeButton.addEventListener('click', toggleQuantumMode); - if (quantumPanelClose) { + if (newChatButton) { + newChatButton.addEventListener('click', () => { + console.log('New Chat button clicked'); + newChat(); + }); + } + if (clearButton) { + clearButton.addEventListener('click', () => { + console.log('Clear button clicked'); + clearChat(false); + }); + } + if (exportButton) exportButton.addEventListener('click', exportChat); + if (importButton) importButton.addEventListener('click', importChat); + if (toggleThemeButton) toggleThemeButton.addEventListener('click', toggleTheme); + if (quantumModeButton) quantumModeButton.addEventListener('click', toggleQuantumMode); + if (agiModeButton) agiModeButton.addEventListener('click', toggleAgiMode); + if (quantumPanelClose && quantumPanel) { quantumPanelClose.addEventListener('click', () => { quantumPanel.style.display = 'none'; }); } - streamToggle.addEventListener('change', (e) => { - streamEnabled = !!e.target.checked; - saveToStorage(); - }); - tempSlider.addEventListener('input', (e) => { - temperature = parseFloat(e.target.value); - tempValue.textContent = temperature.toFixed(2); - }); - tempSlider.addEventListener('change', () => saveToStorage()); - maxTokensInput.addEventListener('change', (e) => { - const val = parseInt(e.target.value, 10); - if (!isNaN(val)) { - maxOutputTokens = Math.max(64, Math.min(40960, val)); - e.target.value = String(maxOutputTokens); + if (streamToggle) { + streamToggle.addEventListener('change', (e) => { + streamEnabled = !!e.target.checked; saveToStorage(); - } - }); - toggleSystemButton.addEventListener('click', () => { - const show = systemPromptBox.style.display !== 'block'; - systemPromptBox.style.display = show ? 'block' : 'none'; - }); - systemPromptInput.addEventListener('input', (e) => { - systemPrompt = e.target.value; - saveToStorage(); - }); + }); + } + if (tempSlider) { + tempSlider.addEventListener('input', (e) => { + temperature = parseFloat(e.target.value); + if (tempValue) tempValue.textContent = temperature.toFixed(2); + }); + tempSlider.addEventListener('change', () => saveToStorage()); + } + if (maxTokensInput) { + maxTokensInput.addEventListener('change', (e) => { + const val = parseInt(e.target.value, 10); + if (!isNaN(val)) { + maxOutputTokens = Math.max(64, Math.min(40960, val)); + e.target.value = String(maxOutputTokens); + saveToStorage(); + } + }); + } + if (toggleSystemButton && systemPromptBox) { + toggleSystemButton.addEventListener('click', () => { + const show = systemPromptBox.style.display !== 'block'; + systemPromptBox.style.display = show ? 'block' : 'none'; + }); + } + if (systemPromptInput) { + systemPromptInput.addEventListener('input', (e) => { + systemPrompt = e.target.value; + saveToStorage(); + }); + } // Load saved conversations and settings loadFromStorage(); // Focus input - messageInput.focus(); + if (messageInput) messageInput.focus(); // Fetch system status on load fetchSystemStatus(); @@ -246,7 +321,7 @@ function updateStatusFromSystem() { if (!systemStatus) return; // Show success message if everything looks good - if (systemStatus.status === 'ok') { + if (systemStatus.status === 'ok' && providerInfo) { const provider = systemStatus.data?.active_provider || 'local'; const model = systemStatus.data?.model || 'unknown'; updateStatus(`Ready - ${provider.toUpperCase()}`); @@ -264,14 +339,17 @@ async function sendMessage() { const text = messageInput.value.trim(); if (!text || isProcessing) return; - // Perform quantum analysis if enabled - if (quantumMode) { + emitEmbeddedChatEvent('aria-chat-send', { text }); + + // Perform quantum analysis if enabled (disabled while AGI mode is active) + if (quantumMode && !agiMode) { updateStatus('Performing quantum analysis...'); await performQuantumAnalysis(text); } // Add user message to UI addMessage('user', text); + emitEmbeddedChatEvent('aria-chat-message', { role: 'user', content: text }); messageInput.value = ''; messageInput.style.height = 'auto'; messageInput.disabled = true; @@ -287,8 +365,15 @@ async function sendMessage() { const typingIndicator = showTypingIndicator(); try { - // Choose streaming or one-shot - if (streamEnabled) { + // Choose AGI, streaming, or one-shot chat backends + if (agiMode) { + if (streamEnabled) { + if (cancelStreamBtn) cancelStreamBtn.style.display = 'inline-block'; + await streamAgiResponse(typingIndicator); + } else { + await agiOneShotResponse(typingIndicator); + } + } else if (streamEnabled) { if (cancelStreamBtn) cancelStreamBtn.style.display = 'inline-block'; await streamResponse(typingIndicator); } else { @@ -373,11 +458,12 @@ async function oneShotResponse(typingIndicator) { messages.push({ role: 'assistant', content: assistantMessage }); } updateMessageCount(); - if (data.provider && data.model) { + if (data.provider && data.model && providerInfo) { providerInfo.textContent = `${data.provider.toUpperCase()} - ${data.model}`; } updateStatus('Ready'); saveToStorage(); + notifyEmbeddedAssistant(assistantMessage); messageInput.disabled = false; sendButton.disabled = false; isProcessing = false; @@ -471,6 +557,7 @@ async function streamResponse(typingIndicator) { retryCount = 0; updateStatus('Ready'); saveToStorage(); + notifyEmbeddedAssistant(fullText); } catch (error) { typingIndicator.remove(); assistantDiv.remove(); @@ -492,6 +579,247 @@ async function streamResponse(typingIndicator) { } } +function extractAgiOutputText(delta) { + if (typeof delta === 'string') return delta; + if (delta?.type === 'output') return String(delta.data || ''); + return ''; +} + +function formatAgiProviderLabel(providerMeta) { + if (!providerMeta) return 'AGI'; + const base = providerMeta.base_provider; + const model = providerMeta.base_model; + if (base && base !== 'agi') { + return 'AGI (' + String(base).toUpperCase() + (model ? ' · ' + model : '') + ')'; + } + return 'AGI'; +} + +async function previewAgiRouting() { + const lastUser = [...messages].reverse().find(function (m) { return m.role === 'user'; }); + const query = lastUser && lastUser.content ? String(lastUser.content).trim() : ''; + if (!query) { + updateStatus('AGI reasoning enabled'); + return; + } + + try { + const response = await fetch(AGI_ANALYZE_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ query: query }), + }); + if (!response.ok) { + updateStatus('AGI reasoning enabled'); + return; + } + const data = await response.json(); + const agent = (data.routing && data.routing.selected_agent) || 'general'; + const domain = (data.analysis && data.analysis.domain) || 'general'; + updateStatus('AGI enabled → ' + agent + ' (' + domain + ')'); + if (providerInfo) { + providerInfo.textContent = formatAgiProviderLabel(data.provider); + } + } catch (e) { + updateStatus('AGI reasoning enabled'); + } +} + +function renderAgiDeltaHtml(delta) { + if (typeof globalThis.AGIStreamUtils !== 'undefined' && globalThis.AGIStreamUtils.prettyPrintDelta) { + return globalThis.AGIStreamUtils.prettyPrintDelta(delta); + } + if (!delta || typeof delta !== 'object') return ''; + return '
' + String(JSON.stringify(delta)) + ''; +} + +async function agiOneShotResponse(typingIndicator) { + const apiMessages = sanitizeConversationMessages(systemPrompt ? + [{ role: 'system', content: systemPrompt }, ...messages] : + messages); + + const response = await fetch(AGI_REASON_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: apiMessages, + include_reasoning_summary: true, + temperature: temperature, + max_output_tokens: maxOutputTokens, + }) + }); + + if (!response.ok) { + const errorText = await response.text(); + throw new Error(`HTTP ${response.status}: ${errorText || response.statusText}`); + } + + const data = await response.json(); + retryCount = 0; + typingIndicator.remove(); + + const assistantMessage = data.response || 'No AGI response received.'; + let displayMessage = assistantMessage; + if (data.reasoning && data.reasoning.last_agent_used) { + const agent = data.reasoning.last_agent_used; + const chains = data.reasoning.total_reasoning_chains; + const chainSuffix = chains ? ' (' + chains + ' chain' + (chains === 1 ? '' : 's') + ')' : ''; + displayMessage += '\n\n---\n*AGI routing: ' + agent + chainSuffix + '*'; + } + addMessage('assistant', displayMessage, true); + if (!isSyntheticCompactionPlaceholder(assistantMessage)) { + messages.push({ role: 'assistant', content: assistantMessage }); + } + updateMessageCount(); + if (providerInfo) { + providerInfo.textContent = formatAgiProviderLabel(data.provider); + } + updateStatus('Ready (AGI)'); + saveToStorage(); + notifyEmbeddedAssistant(assistantMessage); + messageInput.disabled = false; + sendButton.disabled = false; + isProcessing = false; + messageInput.focus(); +} + +async function streamAgiResponse(typingIndicator) { + const assistantDiv = document.createElement('div'); + assistantDiv.className = 'message assistant'; + const contentDiv = document.createElement('div'); + contentDiv.className = 'message-content'; + contentDiv.innerHTML = ''; + assistantDiv.appendChild(contentDiv); + chatMessages.appendChild(assistantDiv); + chatMessages.scrollTop = chatMessages.scrollHeight; + + activeAbortController = new AbortController(); + + const apiMessages = sanitizeConversationMessages(systemPrompt ? + [{ role: 'system', content: systemPrompt }, ...messages] : + messages); + + try { + const response = await fetch(AGI_STREAM_API, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + messages: apiMessages, + temperature: temperature, + max_output_tokens: maxOutputTokens, + }), + signal: activeAbortController.signal + }); + + if (!response.ok || !response.body) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + typingIndicator.remove(); + + const reader = response.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + let reasoningHtml = ''; + let outputText = ''; + + while (true) { + const { value, done } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const blocks = buffer.split('\n\n'); + buffer = blocks.pop() || ''; + + blocks.forEach(function (block) { + if (!block.trim()) return; + const lines = block.split('\n'); + let eventName = 'message'; + const dataLines = []; + lines.forEach(function (line) { + if (line.indexOf('event: ') === 0) { + eventName = line.slice(6).trim(); + } else if (line.indexOf('data: ') === 0) { + dataLines.push(line.slice(6)); + } + }); + + const dataStr = dataLines.join('\n').trim(); + if (!dataStr || dataStr === '[DONE]') return; + if (eventName === 'error') { + throw new Error(dataStr); + } + if (eventName === 'meta') { + try { + const meta = JSON.parse(dataStr); + if (providerInfo) { + providerInfo.textContent = formatAgiProviderLabel(meta); + } + } catch (e) { /* ignore malformed meta */ } + return; + } + + let payload; + try { + payload = JSON.parse(dataStr); + } catch (e) { + return; + } + + const delta = payload && payload.delta; + if (!delta) return; + + const chunkText = extractAgiOutputText(delta); + if (chunkText) { + outputText += chunkText; + } else if (delta.type && delta.type !== 'output') { + reasoningHtml += renderAgiDeltaHtml(delta); + } + + contentDiv.innerHTML = reasoningHtml + '