From ff6fa46d9e9d7267d95b98e2187270354d4217d8 Mon Sep 17 00:00:00 2001 From: Corey Leath Date: Tue, 18 Aug 2026 20:07:07 -0400 Subject: [PATCH 1/5] Prepare v1.1.0 release --- CHANGELOG.md | 71 ++-- RELEASE_NOTES_v1.1.0.md | 42 +++ agent/autonomy/store.py | 103 +++--- agent/autonomy/tools.py | 137 +++---- agent/reporter.py | 136 +++---- agent/tools/sagemaker_job.py | 145 ++++---- agent/tools/snowflake_query.py | 135 +++---- agent/tools/web_search.py | 105 +++--- agent/version.py | 21 ++ api/main.py | 163 +++------ infra/helm/agentic-ai-assistant/Chart.yaml | 12 +- pyproject.toml | 52 ++- requirements-dev.txt | 11 +- src/main.py | 114 +++--- src/utils/logger.py | 107 +++--- src/utils/tracker.py | 121 +++---- streamlit_app.py | 394 ++++++++++----------- tests/test_runtime_invariants.py | 257 +++++++------- 18 files changed, 1080 insertions(+), 1046 deletions(-) create mode 100644 RELEASE_NOTES_v1.1.0.md create mode 100644 agent/version.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 530f0bb..586fbfc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +1,48 @@ -# Changelog - -All notable changes to HelixAgent are documented here. - -The project follows Semantic Versioning and the Keep a Changelog format. - +# Changelog + +All notable changes to HelixAgent are documented here. + +The project follows Semantic Versioning and the Keep a Changelog format. + ## [Unreleased] ### Added -- Python 3.10 and 3.11 CI matrix. -- API contract tests and expanded data-ingestion edge-case coverage. -- Coverage XML and JUnit test artifacts. -- Ruff correctness gates and Python syntax validation. -- Container build and live health smoke tests. -- CodeQL, Gitleaks, Trivy, pip-audit, Dependabot, and CycloneDX SBOM automation. -- GitHub Release artifacts and GHCR image publishing. -- Security, contribution, release-readiness, and nine-tier deployment-hygiene documentation. -- Evidence-driven semantic-tag release validation with source checksums, CycloneDX SBOM attachment, and reproducibility instructions. -- Three-way vector-backend benchmark infrastructure that writes measurements only when executed. - ### Changed -- Hardened `DataIngestor` with file validation, split-parameter validation, deterministic partitioning, duplicate-column detection, and explicit types. -- Reworked the production image into isolated Java, C++, Python build stages and a non-root runtime stage. -- Made NumPy/BLAS the default cosine-similarity backend; the C++ ctypes backend is explicit opt-in interoperability and pure Python remains the degradation path. -- Hardened the ctypes boundary by coercing vectors to contiguous float64 buffers before pointer passing. -- Corrected vector-backend documentation to remove unsupported C++ performance claims. - -## [1.0.0] - 2025-06-20 +## [1.1.0] - 2026-08-18 ### Added - -- Java task planner. -- C++ cosine-similarity library. -- Python agent orchestrator. -- FastAPI service. -- Initial Docker and test infrastructure. - -[Unreleased]: https://github.com/CoreyLeath-code/HelixAgent/compare/v1.0.0...HEAD + +- Python 3.10 and 3.11 CI matrix. +- API contract tests and expanded data-ingestion edge-case coverage. +- Coverage XML and JUnit test artifacts. +- Ruff correctness gates and Python syntax validation. +- Container build and live health smoke tests. +- CodeQL, Gitleaks, Trivy, pip-audit, Dependabot, and CycloneDX SBOM automation. +- GitHub Release artifacts and GHCR image publishing. +- Security, contribution, release-readiness, and nine-tier deployment-hygiene documentation. +- Evidence-driven semantic-tag release validation with source checksums, CycloneDX SBOM attachment, and reproducibility instructions. +- Three-way vector-backend benchmark infrastructure that writes measurements only when executed. + +### Changed + +- Hardened `DataIngestor` with file validation, split-parameter validation, deterministic partitioning, duplicate-column detection, and explicit types. +- Reworked the production image into isolated Java, C++, Python build stages and a non-root runtime stage. +- Made NumPy/BLAS the default cosine-similarity backend; the C++ ctypes backend is explicit opt-in interoperability and pure Python remains the degradation path. +- Hardened the ctypes boundary by coercing vectors to contiguous float64 buffers before pointer passing. +- Corrected vector-backend documentation to remove unsupported C++ performance claims. + +## [1.0.0] - 2025-06-20 + +### Added + +- Java task planner. +- C++ cosine-similarity library. +- Python agent orchestrator. +- FastAPI service. +- Initial Docker and test infrastructure. + +[Unreleased]: https://github.com/CoreyLeath-code/HelixAgent/compare/v1.1.0...HEAD +[1.1.0]: https://github.com/CoreyLeath-code/HelixAgent/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/CoreyLeath-code/HelixAgent/releases/tag/v1.0.0 diff --git a/RELEASE_NOTES_v1.1.0.md b/RELEASE_NOTES_v1.1.0.md new file mode 100644 index 0000000..162bc6b --- /dev/null +++ b/RELEASE_NOTES_v1.1.0.md @@ -0,0 +1,42 @@ +# HelixAgent v1.1.0 + +This release packages the existing runtime and aligns its release evidence with the implementation already on `main`. It covers validation, delivery, documentation correction, and the existing vector-backend design without adding a performance claim or a new product capability. + +## Tag-triggered release contract + +Pushing a newly created annotated `v1.1.0` tag triggers **HelixAgent Release**. The workflow checks out the exact tag commit; verifies the tag, `pyproject.toml` version, and changelog section; runs Ruff, pytest, the release-image C++-interop probe, CodeQL, secret scanning, and CycloneDX SBOM generation; then attaches a source archive, SHA-256 checksum, `helixagent-release-sbom`, and reproduction instructions to the GitHub Release before publishing the validated GHCR image. + +## Added + +- Python 3.10 and 3.11 CI matrix. +- API contract tests and expanded data-ingestion edge-case coverage. +- Coverage XML and JUnit test artifacts. +- Ruff correctness gates and Python syntax validation. +- Container build and live health smoke tests. +- CodeQL, Gitleaks, Trivy, pip-audit, Dependabot, and CycloneDX SBOM automation. +- GitHub Release artifacts and GHCR image publishing. +- Security, contribution, release-readiness, and nine-tier deployment-hygiene documentation. +- Evidence-driven semantic-tag release validation with source checksums, CycloneDX SBOM attachment, and reproducibility instructions. +- Three-way vector-backend benchmark infrastructure that writes measurements only when executed. + +## Changed + +- Hardened `DataIngestor` with file validation, split-parameter validation, deterministic partitioning, duplicate-column detection, and explicit types. +- Reworked the production image into isolated Java, C++, Python build stages and a non-root runtime stage. +- Made NumPy/BLAS the default cosine-similarity backend; the C++ ctypes backend is explicit opt-in interoperability and pure Python remains the degradation path. +- Hardened the ctypes boundary by coercing vectors to contiguous float64 buffers before pointer passing. +- Corrected vector-backend documentation to remove unsupported C++ performance claims. + +## Verification + +- SBOM artifact: `helixagent-release-sbom` (CycloneDX JSON). +- Source checksum: `sha256sum` of the deterministic `git archive` source tarball (`gzip -n`). +- Reproduce the release gates: + + ~~~bash + pip install -r requirements-dev.txt + ruff check api agent src tests streamlit_app.py --select E9,F63,F7,F82 + pytest + docker build -t helixagent-release . + python -m benchmarks.vector_ops --output vector-ops-results.json + ~~~ diff --git a/agent/autonomy/store.py b/agent/autonomy/store.py index 3479956..a1bdb79 100644 --- a/agent/autonomy/store.py +++ b/agent/autonomy/store.py @@ -1,54 +1,55 @@ -"""Durable SQLite run store used for checkpointing and process recovery.""" - -from __future__ import annotations - +"""Durable SQLite run store used for checkpointing and process recovery.""" + +from __future__ import annotations + import os import sqlite3 from pathlib import Path - -from agent.autonomy.models import AgentRun - - -class RunNotFoundError(KeyError): - pass - - -class SQLiteRunStore: - def __init__(self, path: str | Path | None = None) -> None: - self.path = str(path or os.getenv("HELIXAGENT_RUN_DB", "data/helixagent_runs.db")) - if self.path != ":memory:": - Path(self.path).parent.mkdir(parents=True, exist_ok=True) - self._connection = sqlite3.connect(self.path, check_same_thread=False) - self._connection.execute( - "CREATE TABLE IF NOT EXISTS agent_runs " - "(id TEXT PRIMARY KEY, state_json TEXT NOT NULL, updated_at TEXT NOT NULL)" - ) - self._connection.commit() - - def close(self) -> None: - """Release the database handle deterministically.""" - self._connection.close() - - def __enter__(self) -> SQLiteRunStore: - return self - - def __exit__(self, *_exc: object) -> None: - self.close() - - def save(self, run: AgentRun) -> None: - run.touch() - self._connection.execute( - "INSERT INTO agent_runs(id, state_json, updated_at) VALUES (?, ?, ?) " - "ON CONFLICT(id) DO UPDATE SET state_json=excluded.state_json, " - "updated_at=excluded.updated_at", - (run.id, run.model_dump_json(), run.updated_at.isoformat()), - ) - self._connection.commit() - - def get(self, run_id: str) -> AgentRun: - row = self._connection.execute( - "SELECT state_json FROM agent_runs WHERE id = ?", (run_id,) - ).fetchone() - if row is None: - raise RunNotFoundError(run_id) - return AgentRun.model_validate_json(row[0]) +from typing import Self + +from agent.autonomy.models import AgentRun + + +class RunNotFoundError(KeyError): + pass + + +class SQLiteRunStore: + def __init__(self, path: str | Path | None = None) -> None: + self.path = str(path or os.getenv("HELIXAGENT_RUN_DB", "data/helixagent_runs.db")) + if self.path != ":memory:": + Path(self.path).parent.mkdir(parents=True, exist_ok=True) + self._connection = sqlite3.connect(self.path, check_same_thread=False) + self._connection.execute( + "CREATE TABLE IF NOT EXISTS agent_runs " + "(id TEXT PRIMARY KEY, state_json TEXT NOT NULL, updated_at TEXT NOT NULL)" + ) + self._connection.commit() + + def close(self) -> None: + """Release the database handle deterministically.""" + self._connection.close() + + def __enter__(self) -> Self: + return self + + def __exit__(self, *_exc: object) -> None: + self.close() + + def save(self, run: AgentRun) -> None: + run.touch() + self._connection.execute( + "INSERT INTO agent_runs(id, state_json, updated_at) VALUES (?, ?, ?) " + "ON CONFLICT(id) DO UPDATE SET state_json=excluded.state_json, " + "updated_at=excluded.updated_at", + (run.id, run.model_dump_json(), run.updated_at.isoformat()), + ) + self._connection.commit() + + def get(self, run_id: str) -> AgentRun: + row = self._connection.execute( + "SELECT state_json FROM agent_runs WHERE id = ?", (run_id,) + ).fetchone() + if row is None: + raise RunNotFoundError(run_id) + return AgentRun.model_validate_json(row[0]) diff --git a/agent/autonomy/tools.py b/agent/autonomy/tools.py index beb0132..d6ba48b 100644 --- a/agent/autonomy/tools.py +++ b/agent/autonomy/tools.py @@ -1,68 +1,69 @@ -"""Governed tool registry with typed metadata, budgets, retries, and timeouts.""" - -from __future__ import annotations - -import time -from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout -from dataclasses import dataclass -from typing import Any, Callable - -from agent.autonomy.models import Observation, RiskLevel, Task - - -ToolHandler = Callable[[dict[str, Any]], Any] - - -@dataclass(frozen=True) -class ToolSpec: - name: str - handler: ToolHandler - description: str - risk: RiskLevel = RiskLevel.READ_ONLY - timeout_seconds: float = 15.0 - - -class ToolRegistry: - def __init__(self) -> None: - self._tools: dict[str, ToolSpec] = {} - - def register(self, spec: ToolSpec) -> None: - if spec.name in self._tools: - raise ValueError(f"Tool already registered: {spec.name}") - self._tools[spec.name] = spec - - def get(self, name: str) -> ToolSpec: - try: - return self._tools[name] - except KeyError as exc: - raise KeyError(f"Unknown tool: {name}") from exc - - def execute(self, task: Task) -> Observation: - spec = self.get(task.tool) - started = time.perf_counter() - pool = ThreadPoolExecutor(max_workers=1) - future = pool.submit(spec.handler, task.arguments) - try: - output = future.result(spec.timeout_seconds) - pool.shutdown(wait=True) - return Observation( - task_id=task.id, - tool=task.tool, - success=True, - output=output, - duration_ms=(time.perf_counter() - started) * 1_000, - ) - except FutureTimeout: - future.cancel() - pool.shutdown(wait=False, cancel_futures=True) - error = f"Tool timed out after {spec.timeout_seconds:.1f}s" - except Exception as exc: # noqa: BLE001 - tool boundary normalizes failures - pool.shutdown(wait=True) - error = f"{type(exc).__name__}: {exc}" - return Observation( - task_id=task.id, - tool=task.tool, - success=False, - error=error, - duration_ms=(time.perf_counter() - started) * 1_000, - ) +"""Governed tool registry with typed metadata, budgets, retries, and timeouts.""" + +from __future__ import annotations + +import time +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import TimeoutError as FutureTimeout +from dataclasses import dataclass +from typing import Any + +from agent.autonomy.models import Observation, RiskLevel, Task + +ToolHandler = Callable[[dict[str, Any]], Any] + + +@dataclass(frozen=True) +class ToolSpec: + name: str + handler: ToolHandler + description: str + risk: RiskLevel = RiskLevel.READ_ONLY + timeout_seconds: float = 15.0 + + +class ToolRegistry: + def __init__(self) -> None: + self._tools: dict[str, ToolSpec] = {} + + def register(self, spec: ToolSpec) -> None: + if spec.name in self._tools: + raise ValueError(f"Tool already registered: {spec.name}") + self._tools[spec.name] = spec + + def get(self, name: str) -> ToolSpec: + try: + return self._tools[name] + except KeyError as exc: + raise KeyError(f"Unknown tool: {name}") from exc + + def execute(self, task: Task) -> Observation: + spec = self.get(task.tool) + started = time.perf_counter() + pool = ThreadPoolExecutor(max_workers=1) + future = pool.submit(spec.handler, task.arguments) + try: + output = future.result(spec.timeout_seconds) + pool.shutdown(wait=True) + return Observation( + task_id=task.id, + tool=task.tool, + success=True, + output=output, + duration_ms=(time.perf_counter() - started) * 1_000, + ) + except FutureTimeout: + future.cancel() + pool.shutdown(wait=False, cancel_futures=True) + error = f"Tool timed out after {spec.timeout_seconds:.1f}s" + except Exception as exc: # noqa: BLE001 - tool boundary normalizes failures + pool.shutdown(wait=True) + error = f"{type(exc).__name__}: {exc}" + return Observation( + task_id=task.id, + tool=task.tool, + success=False, + error=error, + duration_ms=(time.perf_counter() - started) * 1_000, + ) diff --git a/agent/reporter.py b/agent/reporter.py index 608a321..85cc3da 100644 --- a/agent/reporter.py +++ b/agent/reporter.py @@ -1,67 +1,69 @@ -# agent/reporter.py -import os -import datetime -import subprocess -from openai import OpenAI # Or your framework's custom wrapper - -def get_git_metadata(): - """Extracts recent activity directly from the repository environment.""" - try: - commits = subprocess.check_output( - ["git", "log", "--since=24 hours ago", "--oneline"] - ).decode("utf-8") - diff = subprocess.check_output( - ["git", "diff", "HEAD~1", "HEAD"] - ).decode("utf-8")[:2000] # Cap to prevent context blowing up - return commits, diff - except Exception: - return "No recent commits found or shallow clone.", "" - -def generate_daily_log(): - client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) - commits, diff = get_git_metadata() - date_str = datetime.date.today().strftime("%Y-%m-%d") - - prompt = f""" - You are an autonomous MLOps & System Hygiene Agent responsible for maintaining HelixAgent. - Analyze the following repository changes from the last 24 hours and write a professional system summary for today's log entry. - - Date: {date_str} - Recent Commits: - {commits} - - Recent Code Changes snippet: - {diff} - - Generate a markdown section containing: - 1. **System Health & Metrics**: Summary of code state, stability adjustments, or optimization changes. - 2. **Agent Execution Highlights**: Deduce which components (API, Agent core, Infrastructure) were affected and summarize progress. - 3. **Automated TODOs**: List technical debt or testing gaps discovered from the diff/commits. - - Format output cleanly as markdown. Do not include markdown block wrapping (```markdown). - """ - - response = client.chat.completions.create( - model="gpt-4o-mini", - messages=[{"role": "user", "content": prompt}], - temperature=0.2 - ) - - log_content = response.choices[0].message.content - - # Prepend or Append to DAILYLOG.md - log_file = "DAILYLOG.md" - existing_content = "" - if os.path.exists(log_file): - with open(log_file, "r") as f: - existing_content = f.read() - - header = f"# HelixAgent Autonomous Logs\n\n" if not existing_content else "" - - new_entry = f"## Log Entry: {date_str}\n\n{log_content}\n\n---\n\n" - - with open(log_file, "w") as f: - f.write(header + new_entry + existing_content.replace("# HelixAgent Autonomous Logs\n\n", "")) - -if __name__ == "__main__": - generate_daily_log() +# agent/reporter.py +import datetime +import os +import subprocess + +from openai import OpenAI # Or your framework's custom wrapper + + +def get_git_metadata(): + """Extracts recent activity directly from the repository environment.""" + try: + commits = subprocess.check_output( + ["git", "log", "--since=24 hours ago", "--oneline"] + ).decode("utf-8") + diff = subprocess.check_output( + ["git", "diff", "HEAD~1", "HEAD"] + ).decode("utf-8")[:2000] # Cap to prevent context blowing up + return commits, diff + except (OSError, subprocess.CalledProcessError): + return "No recent commits found or shallow clone.", "" + +def generate_daily_log(): + client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + commits, diff = get_git_metadata() + date_str = datetime.datetime.now(datetime.timezone.utc).date().strftime("%Y-%m-%d") + + prompt = f""" + You are an autonomous MLOps & System Hygiene Agent responsible for maintaining HelixAgent. + Analyze the following repository changes from the last 24 hours and write a professional system summary for today's log entry. + + Date: {date_str} + Recent Commits: + {commits} + + Recent Code Changes snippet: + {diff} + + Generate a markdown section containing: + 1. **System Health & Metrics**: Summary of code state, stability adjustments, or optimization changes. + 2. **Agent Execution Highlights**: Deduce which components (API, Agent core, Infrastructure) were affected and summarize progress. + 3. **Automated TODOs**: List technical debt or testing gaps discovered from the diff/commits. + + Format output cleanly as markdown. Do not include markdown block wrapping (```markdown). + """ + + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + temperature=0.2 + ) + + log_content = response.choices[0].message.content + + # Prepend or Append to DAILYLOG.md + log_file = "DAILYLOG.md" + existing_content = "" + if os.path.exists(log_file): + with open(log_file, "r") as f: + existing_content = f.read() + + header = "# HelixAgent Autonomous Logs\n\n" if not existing_content else "" + + new_entry = f"## Log Entry: {date_str}\n\n{log_content}\n\n---\n\n" + + with open(log_file, "w") as f: + f.write(header + new_entry + existing_content.replace("# HelixAgent Autonomous Logs\n\n", "")) + +if __name__ == "__main__": + generate_daily_log() diff --git a/agent/tools/sagemaker_job.py b/agent/tools/sagemaker_job.py index 78cc9d2..4110c02 100644 --- a/agent/tools/sagemaker_job.py +++ b/agent/tools/sagemaker_job.py @@ -1,73 +1,72 @@ -""" -sagemaker_job.py -================ -Tool for the Agentic AI Assistant: trigger SageMaker jobs or invoke -endpoints and surface quick status/results. - -Prerequisites -------------- -pip install boto3==1.34.78 -AWS credentials configured via env vars or IAM role. - -Environment Variables (CI/local) --------------------------------- -AWS_ACCESS_KEY_ID -AWS_SECRET_ACCESS_KEY -AWS_DEFAULT_REGION -""" - -import boto3 -import json -from typing import Dict - -sm_client = boto3.client("sagemaker") -runtime = boto3.client("sagemaker-runtime") - -# ------------------------------------------------------------------ # -# Batch-Transform helper -# ------------------------------------------------------------------ # -def start_batch_transform(job_name: str, - model_name: str, - input_s3: str, - output_s3: str, - instance_type: str = "ml.m5.xlarge", - instance_count: int = 1) -> str: - """ - Kick off a batch-transform job and return the ARN. - """ - response = sm_client.create_transform_job( - TransformJobName=job_name, - ModelName=model_name, - TransformInput={ - "DataSource": {"S3DataSource": {"S3Uri": input_s3, "S3DataType": "S3Prefix"}}, - "ContentType": "text/csv" - }, - TransformOutput={"S3OutputPath": output_s3}, - TransformResources={ - "InstanceType": instance_type, - "InstanceCount": instance_count - } - ) - return response["TransformJobArn"] - -def get_batch_status(job_name: str) -> Dict: - """Return status dict for batch job.""" - return sm_client.describe_transform_job(TransformJobName=job_name) - -# ------------------------------------------------------------------ # -# Real-time endpoint helper -# ------------------------------------------------------------------ # -def invoke_endpoint(endpoint_name: str, payload: str) -> str: - """ - Invoke a JSON endpoint and return stringified result. - """ - response = runtime.invoke_endpoint( - EndpointName=endpoint_name, - ContentType="application/json", - Body=payload.encode("utf-8") - ) - return response["Body"].read().decode() - -# Quick CLI test (comment out unless creds + endpoint configured) -# if __name__ == "__main__": -# print(invoke_endpoint("my-demo-endpoint", json.dumps({"data": [1,2,3]}))) +""" +sagemaker_job.py +================ +Tool for the Agentic AI Assistant: trigger SageMaker jobs or invoke +endpoints and surface quick status/results. + +Prerequisites +------------- +pip install boto3==1.34.78 +AWS credentials configured via env vars or IAM role. + +Environment Variables (CI/local) +-------------------------------- +AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY +AWS_DEFAULT_REGION +""" + + +import boto3 + +sm_client = boto3.client("sagemaker") +runtime = boto3.client("sagemaker-runtime") + +# ------------------------------------------------------------------ # +# Batch-Transform helper +# ------------------------------------------------------------------ # +def start_batch_transform(job_name: str, + model_name: str, + input_s3: str, + output_s3: str, + instance_type: str = "ml.m5.xlarge", + instance_count: int = 1) -> str: + """ + Kick off a batch-transform job and return the ARN. + """ + response = sm_client.create_transform_job( + TransformJobName=job_name, + ModelName=model_name, + TransformInput={ + "DataSource": {"S3DataSource": {"S3Uri": input_s3, "S3DataType": "S3Prefix"}}, + "ContentType": "text/csv" + }, + TransformOutput={"S3OutputPath": output_s3}, + TransformResources={ + "InstanceType": instance_type, + "InstanceCount": instance_count + } + ) + return response["TransformJobArn"] + +def get_batch_status(job_name: str) -> dict: + """Return status dict for batch job.""" + return sm_client.describe_transform_job(TransformJobName=job_name) + +# ------------------------------------------------------------------ # +# Real-time endpoint helper +# ------------------------------------------------------------------ # +def invoke_endpoint(endpoint_name: str, payload: str) -> str: + """ + Invoke a JSON endpoint and return stringified result. + """ + response = runtime.invoke_endpoint( + EndpointName=endpoint_name, + ContentType="application/json", + Body=payload.encode("utf-8") + ) + return response["Body"].read().decode() + +# Quick CLI test (comment out unless creds + endpoint configured) +# if __name__ == "__main__": +# print(invoke_endpoint("my-demo-endpoint", json.dumps({"data": [1,2,3]}))) diff --git a/agent/tools/snowflake_query.py b/agent/tools/snowflake_query.py index d3ab6bf..69900be 100644 --- a/agent/tools/snowflake_query.py +++ b/agent/tools/snowflake_query.py @@ -1,67 +1,68 @@ -""" -snowflake_query.py -================== -Tool for the Agentic AI Assistant: execute parameterized SQL against -Snowflake and return results in a Pythonic format (list[dict]). - -Requirements ------------- -pip install snowflake-connector-python==3.6.0 - -Environment Variables ---------------------- -SNOWFLAKE_ACCOUNT e.g. abc-xy12345 -SNOWFLAKE_USER e.g. COREY_LEATH -SNOWFLAKE_PASSWORD ***** (or use key-pair auth) -SNOWFLAKE_DATABASE e.g. ANALYTICS_DB -SNOWFLAKE_SCHEMA e.g. PUBLIC -SNOWFLAKE_WAREHOUSE e.g. COMPUTE_WH -""" - -import os -import snowflake.connector -from contextlib import contextmanager -from typing import List, Dict - -@contextmanager -def snowflake_connection(): - conn = snowflake.connector.connect( - account=os.getenv("SNOWFLAKE_ACCOUNT"), - user=os.getenv("SNOWFLAKE_USER"), - password=os.getenv("SNOWFLAKE_PASSWORD"), - database=os.getenv("SNOWFLAKE_DATABASE"), - schema=os.getenv("SNOWFLAKE_SCHEMA"), - warehouse=os.getenv("SNOWFLAKE_WAREHOUSE"), - ) - try: - yield conn - finally: - conn.close() - -def run_query(sql: str, params: tuple | None = None) -> List[Dict]: - """ - Execute SQL and return results as list of dicts. - - Parameters - ---------- - sql : str - Parameterized SQL (use %s placeholders). - params : tuple | None - Values for placeholders. - - Returns - ------- - list[dict] - Query results with keys=column names, values=rows. - """ - with snowflake_connection() as conn: - cur = conn.cursor(snowflake.connector.DictCursor) - cur.execute(sql, params) if params else cur.execute(sql) - results = cur.fetchall() - cur.close() - return results - -# Quick CLI test (commented; ensure env vars first) -# if __name__ == "__main__": -# rows = run_query("SELECT CURRENT_TIMESTAMP() AS ts") -# print(rows) +""" +snowflake_query.py +================== +Tool for the Agentic AI Assistant: execute parameterized SQL against +Snowflake and return results in a Pythonic format (list[dict]). + +Requirements +------------ +pip install snowflake-connector-python==3.6.0 + +Environment Variables +--------------------- +SNOWFLAKE_ACCOUNT e.g. abc-xy12345 +SNOWFLAKE_USER e.g. COREY_LEATH +SNOWFLAKE_PASSWORD ***** (or use key-pair auth) +SNOWFLAKE_DATABASE e.g. ANALYTICS_DB +SNOWFLAKE_SCHEMA e.g. PUBLIC +SNOWFLAKE_WAREHOUSE e.g. COMPUTE_WH +""" + +import os +from contextlib import contextmanager + +import snowflake.connector + + +@contextmanager +def snowflake_connection(): + conn = snowflake.connector.connect( + account=os.getenv("SNOWFLAKE_ACCOUNT"), + user=os.getenv("SNOWFLAKE_USER"), + password=os.getenv("SNOWFLAKE_PASSWORD"), + database=os.getenv("SNOWFLAKE_DATABASE"), + schema=os.getenv("SNOWFLAKE_SCHEMA"), + warehouse=os.getenv("SNOWFLAKE_WAREHOUSE"), + ) + try: + yield conn + finally: + conn.close() + +def run_query(sql: str, params: tuple | None = None) -> list[dict]: + """ + Execute SQL and return results as list of dicts. + + Parameters + ---------- + sql : str + Parameterized SQL (use %s placeholders). + params : tuple | None + Values for placeholders. + + Returns + ------- + list[dict] + Query results with keys=column names, values=rows. + """ + with snowflake_connection() as conn: + cur = conn.cursor(snowflake.connector.DictCursor) + cur.execute(sql, params) if params else cur.execute(sql) + results = cur.fetchall() + cur.close() + return results + +# Quick CLI test (commented; ensure env vars first) +# if __name__ == "__main__": +# rows = run_query("SELECT CURRENT_TIMESTAMP() AS ts") +# print(rows) diff --git a/agent/tools/web_search.py b/agent/tools/web_search.py index 0389b6b..419801a 100644 --- a/agent/tools/web_search.py +++ b/agent/tools/web_search.py @@ -1,52 +1,53 @@ -""" -web_search.py -============= -Tool for the Agentic AI Assistant: performs a web search and returns a -short summary suitable for LLM consumption. - -Real implementations could use: - • SerpAPI - • Bing Web Search (Azure Cognitive Services) - • Google Programmable Search - -For demo purposes this module: - 1. Queries DuckDuckGo’s HTML results page - 2. Extracts the top N result titles/snippets - 3. Returns a concatenated summary string - -Dependencies: - pip install ddgs -""" - -from ddgs import DDGS - -def search_and_summarize(query: str, max_results: int = 5) -> str: - """ - Run a web search and summarize the top results. - - Parameters - ---------- - query : str - The user’s search question. - max_results : int - How many results to consider (default 5). - - Returns - ------- - str - A multi-line summary of result titles + snippets. - """ - summary_lines = [] - with DDGS() as ddgs: - results = ddgs.text(query, max_results=max_results) - for idx, res in enumerate(results, 1): - title = res.get("title", "") - snippet = res.get("body", "") - summary_lines.append(f"{idx}. {title} — {snippet}") - - return "\n".join(summary_lines) - - -# Quick CLI test -if __name__ == "__main__": - print(search_and_summarize("latest advances in agentic AI", 3)) +""" +web_search.py +============= +Tool for the Agentic AI Assistant: performs a web search and returns a +short summary suitable for LLM consumption. + +Real implementations could use: + • SerpAPI + • Bing Web Search (Azure Cognitive Services) + • Google Programmable Search + +For demo purposes this module: + 1. Queries DuckDuckGo’s HTML results page + 2. Extracts the top N result titles/snippets + 3. Returns a concatenated summary string + +Dependencies: + pip install ddgs +""" + +from ddgs import DDGS + + +def search_and_summarize(query: str, max_results: int = 5) -> str: + """ + Run a web search and summarize the top results. + + Parameters + ---------- + query : str + The user’s search question. + max_results : int + How many results to consider (default 5). + + Returns + ------- + str + A multi-line summary of result titles + snippets. + """ + summary_lines = [] + with DDGS() as ddgs: + results = ddgs.text(query, max_results=max_results) + for idx, res in enumerate(results, 1): + title = res.get("title", "") + snippet = res.get("body", "") + summary_lines.append(f"{idx}. {title} — {snippet}") + + return "\n".join(summary_lines) + + +# Quick CLI test +if __name__ == "__main__": + print(search_and_summarize("latest advances in agentic AI", 3)) diff --git a/agent/version.py b/agent/version.py new file mode 100644 index 0000000..1822352 --- /dev/null +++ b/agent/version.py @@ -0,0 +1,21 @@ +"""Resolve HelixAgent's version from installed package metadata or pyproject.toml.""" + +from __future__ import annotations + +import re +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path + +_PROJECT_VERSION = re.compile(r'^version\s*=\s*"(?P[^"]+)"\s*$', re.MULTILINE) + + +def get_version() -> str: + """Return the installed distribution version or the source-tree project version.""" + try: + return version("helixagent") + except PackageNotFoundError: + pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml" + match = _PROJECT_VERSION.search(pyproject.read_text(encoding="utf-8")) + if match is None: + raise RuntimeError("Could not determine the HelixAgent project version.") + return match.group("version") diff --git a/api/main.py b/api/main.py index d76ad28..e04d217 100644 --- a/api/main.py +++ b/api/main.py @@ -1,111 +1,52 @@ -# api/main.py - -""" -HelixAgent FastAPI Application -------------------------------- -Main API entrypoint. Mounts monitoring (Prometheus + OpenTelemetry) and -exposes core routes for agent inference. -""" - -import os - -from fastapi import BackgroundTasks, FastAPI, HTTPException, status -from fastapi.responses import JSONResponse -from pydantic import BaseModel, Field - -from agent.autonomy.models import AgentRun -from agent.autonomy.runtime import AutonomousRuntime -from agent.autonomy.store import RunNotFoundError -from api.monitoring import setup_monitoring - -app = FastAPI( - title="HelixAgent API", - description="Modular AI agent framework for automation, reasoning, and decision-making.", - version="1.0.0", -) - -# Attach Prometheus metrics + OpenTelemetry tracing -setup_monitoring(app) - - -class PredictRequest(BaseModel): - prompt: str = Field(min_length=1, max_length=4_000) - - -class RunRequest(BaseModel): - objective: str = Field(min_length=1, max_length=4_000) - max_iterations: int = Field(default=12, ge=1, le=100) - tool_budget: int = Field(default=10, ge=1, le=100) - - -class ApprovalDecision(BaseModel): - approved: bool - - -runtime = AutonomousRuntime() - - -@app.get("/", include_in_schema=False) -async def root(): - """Redirect root to health check.""" - return JSONResponse({"status": "ok", "service": "HelixAgent"}) - - -@app.get("/health", tags=["Operations"]) -async def health(): - """Liveness / readiness probe for container orchestration.""" - return {"status": "healthy", "version": "1.0.0"} - - -@app.post("/predict", tags=["Agent"]) -async def predict(payload: PredictRequest): - """Backward-compatible synchronous endpoint.""" - run = runtime.submit(payload.prompt) - completed = runtime.run(run.id) - if completed.error: - raise HTTPException(status_code=503, detail={"run_id": run.id, "error": completed.error}) - return {"run_id": run.id, "status": completed.status, "result": completed.final_output} - - -@app.post("/runs", response_model=AgentRun, status_code=status.HTTP_202_ACCEPTED, tags=["Agent"]) -async def create_run(payload: RunRequest, background_tasks: BackgroundTasks) -> AgentRun: - """Create a durable autonomous run and execute it outside the request lifecycle.""" - run = runtime.submit( - payload.objective, - max_iterations=payload.max_iterations, - tool_budget=payload.tool_budget, - ) - background_tasks.add_task(runtime.run, run.id) - return run - - -@app.get("/runs/{run_id}", response_model=AgentRun, tags=["Agent"]) -async def get_run(run_id: str) -> AgentRun: - try: - return runtime.store.get(run_id) - except RunNotFoundError as exc: - raise HTTPException(status_code=404, detail="Run not found") from exc - - -@app.post("/runs/{run_id}/approvals/{task_id}", response_model=AgentRun, tags=["Agent"]) -async def decide_approval( - run_id: str, - task_id: str, - payload: ApprovalDecision, - background_tasks: BackgroundTasks, -) -> AgentRun: - try: - run = runtime.approve(run_id, task_id, payload.approved) - except (RunNotFoundError, KeyError) as exc: - raise HTTPException(status_code=404, detail=str(exc)) from exc - if payload.approved: - background_tasks.add_task(runtime.run, run.id) - return run - - -if __name__ == "__main__": - import uvicorn - - host = os.getenv("API_HOST", "0.0.0.0") - port = int(os.getenv("API_PORT", "8000")) - uvicorn.run("api.main:app", host=host, port=port, reload=False) +# api/main.py + +""" +HelixAgent FastAPI Application +------------------------------- +Main API entrypoint. Mounts monitoring (Prometheus + OpenTelemetry) and +exposes core routes for agent inference. +""" + +import os + +from fastapi…2402 tokens truncated…abels: + app: helixagent +data: + config.yaml: | + project: + name: "HelixAgent" + version: "1.1.0" + author: "Corey Leath" + description: "Modular AI agent framework for automation, reasoning, and decision-making." + + logging: + level: "INFO" + log_to_file: true + log_file: "logs/helixagent.log" + + agent: + model: "gpt-neo" + max_tokens: 512 + temperature: 0.7 + top_p: 0.9 + + api: + host: "0.0.0.0" + port: 8000 + reload: true + + dashboard: + host: "0.0.0.0" + port: 8501 + + mlflow: + enabled: true + experiment_name: "HelixAgent-Experiments" + tracking_uri: "http://mlflow:5000" + + data: + input_path: "data/input/" + output_path: "data/output/" + batch_size: 32 + shuffle: true + validation_split: 0.1 diff --git a/infra/helm/agentic-ai-assistant/Chart.yaml b/infra/helm/agentic-ai-assistant/Chart.yaml index 68b82cc..a07acea 100644 --- a/infra/helm/agentic-ai-assistant/Chart.yaml +++ b/infra/helm/agentic-ai-assistant/Chart.yaml @@ -1,6 +1,6 @@ -apiVersion: v2 -name: agentic-ai-assistant -description: Helm chart for deploying the Agentic AI Assistant (Python + Java + C++) -type: application -version: 0.1.0 -appVersion: "1.0.0" +apiVersion: v2 +name: agentic-ai-assistant +description: Helm chart for deploying the Agentic AI Assistant (Python + Java + C++) +type: application +version: 0.1.0 +appVersion: "1.1.0" diff --git a/pyproject.toml b/pyproject.toml index 30495b9..fc3acba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,29 +1,43 @@ [project] name = "helixagent" -version = "1.0.0" +version = "1.1.0" description = "Deterministic budgeted agent runtime with governed tools and SQLite checkpoints." readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "Corey Leath" }] -[tool.pytest.ini_options] -testpaths = ["tests"] -pythonpath = ["."] -addopts = "-v" +[project.urls] +Homepage = "https://github.com/CoreyLeath-code/HelixAgent" +Repository = "https://github.com/CoreyLeath-code/HelixAgent" +Changelog = "https://github.com/CoreyLeath-code/HelixAgent/blob/main/CHANGELOG.md" -[tool.coverage.run] -source = ["agent", "src", "api"] -omit = ["tests/*"] +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" -[tool.black] -line-length = 99 -target-version = ["py310"] - -[tool.isort] -profile = "black" -line_length = 99 - -[tool.flake8] -max-line-length = 99 -extend-ignore = ["E203", "W503"] +[tool.setuptools.packages.find] +where = ["."] +include = ["agent*", "api*", "src*"] +namespaces = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +addopts = "-v" + +[tool.coverage.run] +source = ["agent", "src", "api"] +omit = ["tests/*"] + +[tool.black] +line-length = 99 +target-version = ["py310"] + +[tool.isort] +profile = "black" +line_length = 99 + +[tool.flake8] +max-line-length = 99 +extend-ignore = ["E203", "W503"] diff --git a/requirements-dev.txt b/requirements-dev.txt index ed2cbb9..7c55e4e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,7 +1,8 @@ -r requirements.txt +setuptools>=68,<81 pytest-asyncio>=0.23.7 -ruff>=0.9.0 -mypy>=1.14.0 -bandit>=1.8.0 -pip-audit>=2.7.3 -hypothesis>=6.120,<7 +ruff>=0.9.0 +mypy>=1.14.0 +bandit>=1.8.0 +pip-audit>=2.7.3 +hypothesis>=6.120,<7 diff --git a/src/main.py b/src/main.py index cd15260..38b9cd8 100644 --- a/src/main.py +++ b/src/main.py @@ -1,57 +1,57 @@ -# src/main.py - -""" -HelixAgent CLI Entry Point --------------------------- -Run the agent locally from the command line. - -Usage: - python src/main.py - python src/main.py --prompt "Compare vectors and draft a summary" -""" - -import argparse -import os -import sys - -# Ensure the project root is in the path when running as a script -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) - -from src.utils.logger import setup_logger # noqa: E402 - -log = setup_logger() - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="HelixAgent – AI-powered agent framework" - ) - parser.add_argument( - "--prompt", - type=str, - default="Hello, HelixAgent! Run a quick smoke test.", - help="Prompt to send to the agent", - ) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - log.info("HelixAgent starting...") - log.info(f"Prompt: {args.prompt}") - - try: - from agent.agent_core import AgenticAssistant # noqa: E402 - - log.info("Initializing agent core...") - assistant = AgenticAssistant() - output = assistant.run(args.prompt) - log.info("Agent run complete.") - print(f"Agent Output: {output}") - except Exception as exc: # noqa: BLE001 - log.warning(f"Agent core unavailable ({exc}); running echo mode.") - print(f"[HelixAgent] Echo: {args.prompt}") - - -if __name__ == "__main__": - main() +# src/main.py + +""" +HelixAgent CLI Entry Point +-------------------------- +Run the agent locally from the command line. + +Usage: + python src/main.py + python src/main.py --prompt "Compare vectors and draft a summary" +""" + +import argparse +import os +import sys + +# Ensure the project root is in the path when running as a script +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from src.utils.logger import setup_logger + +log = setup_logger() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="HelixAgent – AI-powered agent framework" + ) + parser.add_argument( + "--prompt", + type=str, + default="Hello, HelixAgent! Run a quick smoke test.", + help="Prompt to send to the agent", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + log.info("HelixAgent starting...") + log.info(f"Prompt: {args.prompt}") + + try: + from agent.agent_core import AgenticAssistant + + log.info("Initializing agent core...") + assistant = AgenticAssistant() + output = assistant.run(args.prompt) + log.info("Agent run complete.") + print(f"Agent Output: {output}") + except Exception as exc: # noqa: BLE001 + log.warning(f"Agent core unavailable ({exc}); running echo mode.") + print(f"[HelixAgent] Echo: {args.prompt}") + + +if __name__ == "__main__": + main() diff --git a/src/utils/logger.py b/src/utils/logger.py index 5f83056..1356fac 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -1,53 +1,54 @@ -# src/utils/logger.py - -""" -Logger Utility for HelixAgent ------------------------------ -Provides centralized structured logging with loguru. -Ensures consistent, professional logs across all modules. -""" - -from loguru import logger -import sys - - -def setup_logger(log_file: str = "logs/helixagent.log", level: str = "INFO"): - """ - Configure the logger. - - Args: - log_file (str): Path to log file. - level (str): Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). - """ - # Remove default logger to reconfigure - logger.remove() - - # Console output (colorized) - logger.add( - sys.stdout, - format="{time:YYYY-MM-DD HH:mm:ss} | " - "{level: <8} | " - "{name}:{function}:{line} - " - "{message}", - colorize=True, - level=level, - ) - - # File output - logger.add( - log_file, - rotation="5 MB", - retention="10 days", - level=level, - enqueue=True, - format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", - ) - - logger.info("Logger initialized.") - return logger - - -# Example usage: -# from utils.logger import setup_logger -# log = setup_logger() -# log.info("HelixAgent started successfully.") +# src/utils/logger.py + +""" +Logger Utility for HelixAgent +----------------------------- +Provides centralized structured logging with loguru. +Ensures consistent, professional logs across all modules. +""" + +import sys + +from loguru import logger + + +def setup_logger(log_file: str = "logs/helixagent.log", level: str = "INFO"): + """ + Configure the logger. + + Args: + log_file (str): Path to log file. + level (str): Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). + """ + # Remove default logger to reconfigure + logger.remove() + + # Console output (colorized) + logger.add( + sys.stdout, + format="{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{name}:{function}:{line} - " + "{message}", + colorize=True, + level=level, + ) + + # File output + logger.add( + log_file, + rotation="5 MB", + retention="10 days", + level=level, + enqueue=True, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + ) + + logger.info("Logger initialized.") + return logger + + +# Example usage: +# from utils.logger import setup_logger +# log = setup_logger() +# log.info("HelixAgent started successfully.") diff --git a/src/utils/tracker.py b/src/utils/tracker.py index 99e840b..ea0e6e3 100644 --- a/src/utils/tracker.py +++ b/src/utils/tracker.py @@ -1,60 +1,61 @@ -# src/utils/tracker.py - -""" -Experiment Tracker for HelixAgent ---------------------------------- -Provides MLflow integration for logging experiments, metrics, -parameters, and artifacts. -""" - -import mlflow -from src.utils.logger import setup_logger - -log = setup_logger() - - -class ExperimentTracker: - def __init__(self, experiment_name: str = "HelixAgent-Experiments", tracking_uri: str = "http://127.0.0.1:5000"): - """ - Initialize MLflow tracker. - - Args: - experiment_name (str): Name of the MLflow experiment. - tracking_uri (str): MLflow tracking server URI. - """ - mlflow.set_tracking_uri(tracking_uri) - mlflow.set_experiment(experiment_name) - log.info(f"Initialized MLflow tracker: {experiment_name}") - - def start_run(self, run_name: str = None): - """Start a new MLflow run""" - return mlflow.start_run(run_name=run_name) - - def log_params(self, params: dict): - """Log parameters to MLflow""" - mlflow.log_params(params) - log.debug(f"Logged parameters: {params}") - - def log_metrics(self, metrics: dict, step: int = None): - """Log metrics to MLflow""" - mlflow.log_metrics(metrics, step=step) - log.debug(f"Logged metrics: {metrics}") - - def log_artifact(self, file_path: str): - """Log a file (artifact) to MLflow""" - mlflow.log_artifact(file_path) - log.debug(f"Logged artifact: {file_path}") - - def end_run(self): - """End the current MLflow run""" - mlflow.end_run() - log.info("MLflow run ended.") - - -# Example usage: -# tracker = ExperimentTracker() -# with tracker.start_run("test-run"): -# tracker.log_params({"learning_rate": 0.001, "batch_size": 32}) -# tracker.log_metrics({"accuracy": 0.85}, step=1) -# tracker.log_artifact("models/agent_model.pt") -# tracker.end_run() +# src/utils/tracker.py + +""" +Experiment Tracker for HelixAgent +--------------------------------- +Provides MLflow integration for logging experiments, metrics, +parameters, and artifacts. +""" + +import mlflow + +from src.utils.logger import setup_logger + +log = setup_logger() + + +class ExperimentTracker: + def __init__(self, experiment_name: str = "HelixAgent-Experiments", tracking_uri: str = "http://127.0.0.1:5000"): + """ + Initialize MLflow tracker. + + Args: + experiment_name (str): Name of the MLflow experiment. + tracking_uri (str): MLflow tracking server URI. + """ + mlflow.set_tracking_uri(tracking_uri) + mlflow.set_experiment(experiment_name) + log.info(f"Initialized MLflow tracker: {experiment_name}") + + def start_run(self, run_name: str | None = None): + """Start a new MLflow run""" + return mlflow.start_run(run_name=run_name) + + def log_params(self, params: dict): + """Log parameters to MLflow""" + mlflow.log_params(params) + log.debug(f"Logged parameters: {params}") + + def log_metrics(self, metrics: dict, step: int | None = None): + """Log metrics to MLflow""" + mlflow.log_metrics(metrics, step=step) + log.debug(f"Logged metrics: {metrics}") + + def log_artifact(self, file_path: str): + """Log a file (artifact) to MLflow""" + mlflow.log_artifact(file_path) + log.debug(f"Logged artifact: {file_path}") + + def end_run(self): + """End the current MLflow run""" + mlflow.end_run() + log.info("MLflow run ended.") + + +# Example usage: +# tracker = ExperimentTracker() +# with tracker.start_run("test-run"): +# tracker.log_params({"learning_rate": 0.001, "batch_size": 32}) +# tracker.log_metrics({"accuracy": 0.85}, step=1) +# tracker.log_artifact("models/agent_model.pt") +# tracker.end_run() diff --git a/streamlit_app.py b/streamlit_app.py index c3f8b0c..b7deb2f 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -1,197 +1,197 @@ -"""Interactive Streamlit demo for HelixAgent. - -This entry point is intentionally self-contained so it can be deployed directly -from the repository root on Streamlit Community Cloud. -""" - -from __future__ import annotations - -import time -from dataclasses import dataclass - -import streamlit as st - -from agent.agent_core import AgenticAssistant, cosine_sim - - -@dataclass(frozen=True) -class DemoExample: - """Preset prompt shown in the demo sidebar.""" - - label: str - prompt: str - - -EXAMPLES = ( - DemoExample( - "System summary", - "Summarize the current HelixAgent architecture and explain its main engineering strengths.", - ), - DemoExample( - "Vector workflow", - "Compare vectors and explain why cosine similarity is useful in AI systems.", - ), - DemoExample( - "Web-assisted plan", - "Search the web for recent MLOps reliability practices and produce a concise implementation plan.", - ), -) - - -@st.cache_resource(show_spinner=False) -def get_assistant() -> AgenticAssistant: - """Create one reusable agent instance per Streamlit session process.""" - - return AgenticAssistant() - - -def render_sidebar() -> None: - """Render architecture, capability, and example-prompt controls.""" - - with st.sidebar: - st.title("🧬 HelixAgent") - st.caption("Autonomous MLOps & multi-agent infrastructure demo") - - st.subheader("Capabilities") - st.markdown( - """ -- LangGraph workflow orchestration -- Python fallback planner -- Optional Java planner integration -- NumPy-default vectors with optional C++ ctypes interop -- FastAPI service layer -- CI/CD and supply-chain security -""" - ) - - st.subheader("Try an example") - for example in EXAMPLES: - if st.button(example.label, use_container_width=True): - st.session_state["prompt"] = example.prompt - - st.divider() - st.markdown( - "[View the source on GitHub](https://github.com/CoreyLeath-code/HelixAgent)" - ) - st.caption( - "The public demo uses built-in fallback behavior when optional native or external services are unavailable." - ) - - -def render_metrics() -> None: - """Show concise implementation details at the top of the page.""" - - col1, col2, col3, col4 = st.columns(4) - col1.metric("API", "FastAPI") - col2.metric("Orchestration", "LangGraph") - col3.metric("Runtime", "Python 3.11") - col4.metric("Deployment", "Streamlit") - - -def render_architecture() -> None: - """Render a lightweight architecture diagram without external assets.""" - - with st.expander("Architecture overview"): - st.graphviz_chart( - """ -digraph HelixAgent { - rankdir=LR; - node [shape=box, style=rounded]; - User -> Streamlit; - Streamlit -> Orchestrator; - Orchestrator -> Planner; - Orchestrator -> VectorUtility; - Orchestrator -> WebSearch; - Planner -> Response; - VectorUtility -> Response; - WebSearch -> Response; -} -""", - use_container_width=True, - ) - - -def render_vector_lab() -> None: - """Expose the vector utility as a transparent, deterministic mini-demo.""" - - with st.expander("Vector similarity lab"): - st.write("Compare two three-dimensional vectors using HelixAgent's cosine utility.") - left = st.text_input("Vector A", "1, 0, 1") - right = st.text_input("Vector B", "0.5, 0, 0.5") - - if st.button("Calculate similarity"): - try: - vector_a = [float(value.strip()) for value in left.split(",")] - vector_b = [float(value.strip()) for value in right.split(",")] - if len(vector_a) != len(vector_b) or not vector_a: - raise ValueError("Vectors must be non-empty and have equal dimensions.") - score = cosine_sim(vector_a, vector_b) - except ValueError as exc: - st.error(str(exc)) - else: - st.success(f"Cosine similarity: {score:.4f}") - - -def run_agent(prompt: str) -> tuple[str, float]: - """Run the agent and return its response with elapsed time.""" - - started = time.perf_counter() - response = get_assistant().run(prompt) - elapsed = time.perf_counter() - started - return response, elapsed - - -def main() -> None: - """Render the HelixAgent Streamlit application.""" - - st.set_page_config( - page_title="HelixAgent Demo", - page_icon="🧬", - layout="wide", - initial_sidebar_state="expanded", - ) - - render_sidebar() - - st.title("🧬 HelixAgent") - st.subheader("Enterprise multi-agent AI orchestration demo") - st.write( - "Explore HelixAgent's planning, orchestration, fallback execution, and vector-processing capabilities through an interactive interface." - ) - - render_metrics() - render_architecture() - - prompt = st.text_area( - "Ask HelixAgent", - key="prompt", - height=140, - placeholder="Example: Compare vectors and then draft an implementation summary.", - ) - - run_clicked = st.button("Run HelixAgent", type="primary", use_container_width=True) - if run_clicked: - if not prompt.strip(): - st.warning("Enter a prompt before running the agent.") - else: - with st.spinner("Planning and executing the workflow..."): - try: - response, elapsed = run_agent(prompt.strip()) - except Exception as exc: # pragma: no cover - defensive UI boundary - st.error("HelixAgent could not complete this request.") - st.exception(exc) - else: - st.subheader("Agent response") - st.code(response, language="text") - st.caption(f"Completed in {elapsed:.3f} seconds") - - render_vector_lab() - - st.divider() - st.caption( - "Portfolio demonstration by Corey Leath · Built with Streamlit, FastAPI, LangGraph, Python, and optional native integrations." - ) - - -if __name__ == "__main__": - main() +"""Interactive Streamlit demo for HelixAgent. + +This entry point is intentionally self-contained so it can be deployed directly +from the repository root on Streamlit Community Cloud. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass + +import streamlit as st + +from agent.agent_core import AgenticAssistant, cosine_sim + + +@dataclass(frozen=True) +class DemoExample: + """Preset prompt shown in the demo sidebar.""" + + label: str + prompt: str + + +EXAMPLES = ( + DemoExample( + "System summary", + "Summarize the current HelixAgent architecture and explain its main engineering strengths.", + ), + DemoExample( + "Vector workflow", + "Compare vectors and explain why cosine similarity is useful in AI systems.", + ), + DemoExample( + "Web-assisted plan", + "Search the web for recent MLOps reliability practices and produce a concise implementation plan.", + ), +) + + +@st.cache_resource(show_spinner=False) +def get_assistant() -> AgenticAssistant: + """Create one reusable agent instance per Streamlit session process.""" + + return AgenticAssistant() + + +def render_sidebar() -> None: + """Render architecture, capability, and example-prompt controls.""" + + with st.sidebar: + st.title("🧬 HelixAgent") + st.caption("Autonomous MLOps & multi-agent infrastructure demo") + + st.subheader("Capabilities") + st.markdown( + """ +- LangGraph workflow orchestration +- Python fallback planner +- Optional Java planner integration +- NumPy-default vectors with optional C++ ctypes interop +- FastAPI service layer +- CI/CD and supply-chain security +""" + ) + + st.subheader("Try an example") + for example in EXAMPLES: + if st.button(example.label, use_container_width=True): + st.session_state["prompt"] = example.prompt + + st.divider() + st.markdown( + "[View the source on GitHub](https://github.com/CoreyLeath-code/HelixAgent)" + ) + st.caption( + "The public demo uses built-in fallback behavior when optional native or external services are unavailable." + ) + + +def render_metrics() -> None: + """Show concise implementation details at the top of the page.""" + + col1, col2, col3, col4 = st.columns(4) + col1.metric("API", "FastAPI") + col2.metric("Orchestration", "LangGraph") + col3.metric("Runtime", "Python 3.11") + col4.metric("Deployment", "Streamlit") + + +def render_architecture() -> None: + """Render a lightweight architecture diagram without external assets.""" + + with st.expander("Architecture overview"): + st.graphviz_chart( + """ +digraph HelixAgent { + rankdir=LR; + node [shape=box, style=rounded]; + User -> Streamlit; + Streamlit -> Orchestrator; + Orchestrator -> Planner; + Orchestrator -> VectorUtility; + Orchestrator -> WebSearch; + Planner -> Response; + VectorUtility -> Response; + WebSearch -> Response; +} +""", + use_container_width=True, + ) + + +def render_vector_lab() -> None: + """Expose the vector utility as a transparent, deterministic mini-demo.""" + + with st.expander("Vector similarity lab"): + st.write("Compare two three-dimensional vectors using HelixAgent's cosine utility.") + left = st.text_input("Vector A", "1, 0, 1") + right = st.text_input("Vector B", "0.5, 0, 0.5") + + if st.button("Calculate similarity"): + try: + vector_a = [float(value.strip()) for value in left.split(",")] + vector_b = [float(value.strip()) for value in right.split(",")] + if len(vector_a) != len(vector_b) or not vector_a: + raise ValueError("Vectors must be non-empty and have equal dimensions.") + score = cosine_sim(vector_a, vector_b) + except ValueError as exc: + st.error(str(exc)) + else: + st.success(f"Cosine similarity: {score:.4f}") + + +def run_agent(prompt: str) -> tuple[str, float]: + """Run the agent and return its response with elapsed time.""" + + started = time.perf_counter() + response = get_assistant().run(prompt) + elapsed = time.perf_counter() - started + return response, elapsed + + +def main() -> None: + """Render the HelixAgent Streamlit application.""" + + st.set_page_config( + page_title="HelixAgent Demo", + page_icon="🧬", + layout="wide", + initial_sidebar_state="expanded", + ) + + render_sidebar() + + st.title("🧬 HelixAgent") + st.subheader("Enterprise multi-agent AI orchestration demo") + st.write( + "Explore HelixAgent's planning, orchestration, fallback execution, and vector-processing capabilities through an interactive interface." + ) + + render_metrics() + render_architecture() + + prompt = st.text_area( + "Ask HelixAgent", + key="prompt", + height=140, + placeholder="Example: Compare vectors and then draft an implementation summary.", + ) + + run_clicked = st.button("Run HelixAgent", type="primary", use_container_width=True) + if run_clicked: + if not prompt.strip(): + st.warning("Enter a prompt before running the agent.") + else: + with st.spinner("Planning and executing the workflow..."): + try: + response, elapsed = run_agent(prompt.strip()) + except Exception as exc: # noqa: BLE001 - defensive UI boundary + st.error("HelixAgent could not complete this request.") + st.exception(exc) + else: + st.subheader("Agent response") + st.code(response, language="text") + st.caption(f"Completed in {elapsed:.3f} seconds") + + render_vector_lab() + + st.divider() + st.caption( + "Portfolio demonstration by Corey Leath · Built with Streamlit, FastAPI, LangGraph, Python, and optional native integrations." + ) + + +if __name__ == "__main__": + main() diff --git a/tests/test_runtime_invariants.py b/tests/test_runtime_invariants.py index 3bb72e6..8ee07b0 100644 --- a/tests/test_runtime_invariants.py +++ b/tests/test_runtime_invariants.py @@ -1,128 +1,129 @@ -"""Adversarial contracts for runtime terminality and vector semantics.""" - -from __future__ import annotations - -import math -from pathlib import Path - -import pytest -from hypothesis import given, strategies as st - -from agent import agent_core -from agent.autonomy.models import GoalSpec, RunStatus, Task -from agent.autonomy.runtime import AutonomousRuntime -from agent.autonomy.store import SQLiteRunStore -from agent.autonomy.tools import ToolRegistry, ToolSpec - - -class StaticPlanner: - def __init__(self, tasks: list[Task]) -> None: - self.tasks = tasks - - def create_plan(self, _goal: GoalSpec) -> list[Task]: - return [task.model_copy(deep=True) for task in self.tasks] - - def replan(self, run, _failed): - return run.plan - - -def make_runtime(tmp_path: Path, tasks: list[Task], registry: ToolRegistry) -> AutonomousRuntime: - return AutonomousRuntime( - planner=StaticPlanner(tasks), - registry=registry, - store=SQLiteRunStore(tmp_path / "invariants.db"), - ) - - -def test_completed_run_is_not_executed_twice(tmp_path: Path) -> None: - calls: list[str] = [] - registry = ToolRegistry() - registry.register( - ToolSpec("once", lambda _arguments: calls.append("executed") or "done", "One call") - ) - runtime = make_runtime(tmp_path, [Task(objective="Run once", tool="once")], registry) - - submitted = runtime.submit("Run one task") - first = runtime.run(submitted.id) - second = runtime.run(submitted.id) - - assert first.status is RunStatus.COMPLETED - assert second.status is RunStatus.COMPLETED - assert calls == ["executed"] - assert second.tool_calls == 1 - - -def test_unknown_planned_tool_fails_and_persists(tmp_path: Path) -> None: - runtime = make_runtime( - tmp_path, - [Task(objective="Unknown", tool="missing_tool")], - ToolRegistry(), - ) - - completed = runtime.run(runtime.submit("Exercise failure boundary").id) - restored = runtime.store.get(completed.id) - - assert completed.status is RunStatus.FAILED - assert "Unknown tool" in completed.error - assert restored.status is RunStatus.FAILED - assert restored.tool_calls == 0 - - -def test_python_vector_fallback_defines_zero_and_dimension_behavior(monkeypatch) -> None: - monkeypatch.setattr(agent_core, "_lib_vec", None) - - assert agent_core.cosine_similarity_python([0.0, 0.0], [1.0, -1.0]) == 0.0 - with pytest.raises(ValueError, match="equal dimensions"): - agent_core.cosine_similarity_python([1.0], [1.0, 2.0]) - - -@pytest.mark.parametrize( - ("left", "right", "expected"), - [ - ([3.565393874732073e-277], [-1.0], -1.0), - ([1e308, 1e308], [1e308, 1e308], 1.0), - ], -) -def test_python_vector_fallback_is_stable_at_extreme_scales( - monkeypatch, left, right, expected -) -> None: - monkeypatch.setattr(agent_core, "_lib_vec", None) - - assert math.isclose( - agent_core.cosine_similarity_python(left, right), expected, rel_tol=1e-12, abs_tol=1e-12 - ) - - -@st.composite -def nonzero_vector_pairs(draw): - dimension = draw(st.integers(min_value=1, max_value=12)) - values = st.floats( - min_value=-1_000_000, - max_value=1_000_000, - allow_nan=False, - allow_infinity=False, - ) - left = draw(st.lists(values, min_size=dimension, max_size=dimension)) - right = draw(st.lists(values, min_size=dimension, max_size=dimension)) - if not any(left): - left[0] = 1.0 - if not any(right): - right[0] = -1.0 - return left, right - - -@given(nonzero_vector_pairs()) -def test_python_cosine_fallback_satisfies_basic_properties(pair) -> None: - # Hypothesis executes many examples, so use a local patch context instead of - # pytest's function-scoped monkeypatch fixture. - with pytest.MonkeyPatch.context() as patch: - patch.setattr(agent_core, "_lib_vec", None) - left, right = pair - - score = agent_core.cosine_similarity_python(left, right) - reverse = agent_core.cosine_similarity_python(right, left) - self_score = agent_core.cosine_similarity_python(left, left) - - assert -1.0 - 1e-12 <= score <= 1.0 + 1e-12 - assert math.isclose(score, reverse, rel_tol=1e-12, abs_tol=1e-12) - assert math.isclose(self_score, 1.0, rel_tol=1e-12, abs_tol=1e-12) +"""Adversarial contracts for runtime terminality and vector semantics.""" + +from __future__ import annotations + +import math +from pathlib import Path + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from agent import agent_core +from agent.autonomy.models import GoalSpec, RunStatus, Task +from agent.autonomy.runtime import AutonomousRuntime +from agent.autonomy.store import SQLiteRunStore +from agent.autonomy.tools import ToolRegistry, ToolSpec + + +class StaticPlanner: + def __init__(self, tasks: list[Task]) -> None: + self.tasks = tasks + + def create_plan(self, _goal: GoalSpec) -> list[Task]: + return [task.model_copy(deep=True) for task in self.tasks] + + def replan(self, run, _failed): + return run.plan + + +def make_runtime(tmp_path: Path, tasks: list[Task], registry: ToolRegistry) -> AutonomousRuntime: + return AutonomousRuntime( + planner=StaticPlanner(tasks), + registry=registry, + store=SQLiteRunStore(tmp_path / "invariants.db"), + ) + + +def test_completed_run_is_not_executed_twice(tmp_path: Path) -> None: + calls: list[str] = [] + registry = ToolRegistry() + registry.register( + ToolSpec("once", lambda _arguments: calls.append("executed") or "done", "One call") + ) + runtime = make_runtime(tmp_path, [Task(objective="Run once", tool="once")], registry) + + submitted = runtime.submit("Run one task") + first = runtime.run(submitted.id) + second = runtime.run(submitted.id) + + assert first.status is RunStatus.COMPLETED + assert second.status is RunStatus.COMPLETED + assert calls == ["executed"] + assert second.tool_calls == 1 + + +def test_unknown_planned_tool_fails_and_persists(tmp_path: Path) -> None: + runtime = make_runtime( + tmp_path, + [Task(objective="Unknown", tool="missing_tool")], + ToolRegistry(), + ) + + completed = runtime.run(runtime.submit("Exercise failure boundary").id) + restored = runtime.store.get(completed.id) + + assert completed.status is RunStatus.FAILED + assert "Unknown tool" in completed.error + assert restored.status is RunStatus.FAILED + assert restored.tool_calls == 0 + + +def test_python_vector_fallback_defines_zero_and_dimension_behavior(monkeypatch) -> None: + monkeypatch.setattr(agent_core, "_lib_vec", None) + + assert agent_core.cosine_similarity_python([0.0, 0.0], [1.0, -1.0]) == 0.0 + with pytest.raises(ValueError, match="equal dimensions"): + agent_core.cosine_similarity_python([1.0], [1.0, 2.0]) + + +@pytest.mark.parametrize( + ("left", "right", "expected"), + [ + ([3.565393874732073e-277], [-1.0], -1.0), + ([1e308, 1e308], [1e308, 1e308], 1.0), + ], +) +def test_python_vector_fallback_is_stable_at_extreme_scales( + monkeypatch, left, right, expected +) -> None: + monkeypatch.setattr(agent_core, "_lib_vec", None) + + assert math.isclose( + agent_core.cosine_similarity_python(left, right), expected, rel_tol=1e-12, abs_tol=1e-12 + ) + + +@st.composite +def nonzero_vector_pairs(draw): + dimension = draw(st.integers(min_value=1, max_value=12)) + values = st.floats( + min_value=-1_000_000, + max_value=1_000_000, + allow_nan=False, + allow_infinity=False, + ) + left = draw(st.lists(values, min_size=dimension, max_size=dimension)) + right = draw(st.lists(values, min_size=dimension, max_size=dimension)) + if not any(left): + left[0] = 1.0 + if not any(right): + right[0] = -1.0 + return left, right + + +@given(nonzero_vector_pairs()) +def test_python_cosine_fallback_satisfies_basic_properties(pair) -> None: + # Hypothesis executes many examples, so use a local patch context instead of + # pytest's function-scoped monkeypatch fixture. + with pytest.MonkeyPatch.context() as patch: + patch.setattr(agent_core, "_lib_vec", None) + left, right = pair + + score = agent_core.cosine_similarity_python(left, right) + reverse = agent_core.cosine_similarity_python(right, left) + self_score = agent_core.cosine_similarity_python(left, left) + + assert -1.0 - 1e-12 <= score <= 1.0 + 1e-12 + assert math.isclose(score, reverse, rel_tol=1e-12, abs_tol=1e-12) + assert math.isclose(self_score, 1.0, rel_tol=1e-12, abs_tol=1e-12) From acb3305d64d9ff163996752a340b0843ec9901c1 Mon Sep 17 00:00:00 2001 From: Corey Leath Date: Tue, 18 Aug 2026 20:08:35 -0400 Subject: [PATCH 2/5] Normalize v1.1.0 release preparation --- CHANGELOG.md | 72 ++-- agent/autonomy/store.py | 100 +++--- agent/autonomy/tools.py | 138 ++++---- agent/reporter.py | 134 +++---- agent/tools/sagemaker_job.py | 144 ++++---- agent/tools/snowflake_query.py | 114 +++--- agent/tools/web_search.py | 105 +++--- agent/version.py | 21 -- api/main.py | 163 ++++++--- infra/helm/agentic-ai-assistant/Chart.yaml | 12 +- pyproject.toml | 52 +-- requirements-dev.txt | 11 +- src/main.py | 114 +++--- src/utils/logger.py | 108 +++--- src/utils/tracker.py | 118 +++---- streamlit_app.py | 392 ++++++++++----------- tests/test_runtime_invariants.py | 258 +++++++------- 17 files changed, 1028 insertions(+), 1028 deletions(-) delete mode 100644 agent/version.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 586fbfc..3380d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ -# Changelog - -All notable changes to HelixAgent are documented here. - -The project follows Semantic Versioning and the Keep a Changelog format. - +# Changelog + +All notable changes to HelixAgent are documented here. + +The project follows Semantic Versioning and the Keep a Changelog format. + ## [Unreleased] ### Added @@ -13,36 +13,36 @@ The project follows Semantic Versioning and the Keep a Changelog format. ## [1.1.0] - 2026-08-18 ### Added - -- Python 3.10 and 3.11 CI matrix. -- API contract tests and expanded data-ingestion edge-case coverage. -- Coverage XML and JUnit test artifacts. -- Ruff correctness gates and Python syntax validation. -- Container build and live health smoke tests. -- CodeQL, Gitleaks, Trivy, pip-audit, Dependabot, and CycloneDX SBOM automation. -- GitHub Release artifacts and GHCR image publishing. -- Security, contribution, release-readiness, and nine-tier deployment-hygiene documentation. -- Evidence-driven semantic-tag release validation with source checksums, CycloneDX SBOM attachment, and reproducibility instructions. -- Three-way vector-backend benchmark infrastructure that writes measurements only when executed. - -### Changed - -- Hardened `DataIngestor` with file validation, split-parameter validation, deterministic partitioning, duplicate-column detection, and explicit types. -- Reworked the production image into isolated Java, C++, Python build stages and a non-root runtime stage. -- Made NumPy/BLAS the default cosine-similarity backend; the C++ ctypes backend is explicit opt-in interoperability and pure Python remains the degradation path. -- Hardened the ctypes boundary by coercing vectors to contiguous float64 buffers before pointer passing. -- Corrected vector-backend documentation to remove unsupported C++ performance claims. - -## [1.0.0] - 2025-06-20 - -### Added - -- Java task planner. -- C++ cosine-similarity library. -- Python agent orchestrator. -- FastAPI service. -- Initial Docker and test infrastructure. - + +- Python 3.10 and 3.11 CI matrix. +- API contract tests and expanded data-ingestion edge-case coverage. +- Coverage XML and JUnit test artifacts. +- Ruff correctness gates and Python syntax validation. +- Container build and live health smoke tests. +- CodeQL, Gitleaks, Trivy, pip-audit, Dependabot, and CycloneDX SBOM automation. +- GitHub Release artifacts and GHCR image publishing. +- Security, contribution, release-readiness, and nine-tier deployment-hygiene documentation. +- Evidence-driven semantic-tag release validation with source checksums, CycloneDX SBOM attachment, and reproducibility instructions. +- Three-way vector-backend benchmark infrastructure that writes measurements only when executed. + +### Changed + +- Hardened `DataIngestor` with file validation, split-parameter validation, deterministic partitioning, duplicate-column detection, and explicit types. +- Reworked the production image into isolated Java, C++, Python build stages and a non-root runtime stage. +- Made NumPy/BLAS the default cosine-similarity backend; the C++ ctypes backend is explicit opt-in interoperability and pure Python remains the degradation path. +- Hardened the ctypes boundary by coercing vectors to contiguous float64 buffers before pointer passing. +- Corrected vector-backend documentation to remove unsupported C++ performance claims. + +## [1.0.0] - 2025-06-20 + +### Added + +- Java task planner. +- C++ cosine-similarity library. +- Python agent orchestrator. +- FastAPI service. +- Initial Docker and test infrastructure. + [Unreleased]: https://github.com/CoreyLeath-code/HelixAgent/compare/v1.1.0...HEAD [1.1.0]: https://github.com/CoreyLeath-code/HelixAgent/compare/v1.0.0...v1.1.0 [1.0.0]: https://github.com/CoreyLeath-code/HelixAgent/releases/tag/v1.0.0 diff --git a/agent/autonomy/store.py b/agent/autonomy/store.py index a1bdb79..d89fc92 100644 --- a/agent/autonomy/store.py +++ b/agent/autonomy/store.py @@ -1,55 +1,55 @@ -"""Durable SQLite run store used for checkpointing and process recovery.""" - -from __future__ import annotations - +"""Durable SQLite run store used for checkpointing and process recovery.""" + +from __future__ import annotations + import os import sqlite3 from pathlib import Path from typing import Self - -from agent.autonomy.models import AgentRun - - -class RunNotFoundError(KeyError): - pass - - -class SQLiteRunStore: - def __init__(self, path: str | Path | None = None) -> None: - self.path = str(path or os.getenv("HELIXAGENT_RUN_DB", "data/helixagent_runs.db")) - if self.path != ":memory:": - Path(self.path).parent.mkdir(parents=True, exist_ok=True) - self._connection = sqlite3.connect(self.path, check_same_thread=False) - self._connection.execute( - "CREATE TABLE IF NOT EXISTS agent_runs " - "(id TEXT PRIMARY KEY, state_json TEXT NOT NULL, updated_at TEXT NOT NULL)" - ) - self._connection.commit() - - def close(self) -> None: - """Release the database handle deterministically.""" - self._connection.close() - + +from agent.autonomy.models import AgentRun + + +class RunNotFoundError(KeyError): + pass + + +class SQLiteRunStore: + def __init__(self, path: str | Path | None = None) -> None: + self.path = str(path or os.getenv("HELIXAGENT_RUN_DB", "data/helixagent_runs.db")) + if self.path != ":memory:": + Path(self.path).parent.mkdir(parents=True, exist_ok=True) + self._connection = sqlite3.connect(self.path, check_same_thread=False) + self._connection.execute( + "CREATE TABLE IF NOT EXISTS agent_runs " + "(id TEXT PRIMARY KEY, state_json TEXT NOT NULL, updated_at TEXT NOT NULL)" + ) + self._connection.commit() + + def close(self) -> None: + """Release the database handle deterministically.""" + self._connection.close() + def __enter__(self) -> Self: - return self - - def __exit__(self, *_exc: object) -> None: - self.close() - - def save(self, run: AgentRun) -> None: - run.touch() - self._connection.execute( - "INSERT INTO agent_runs(id, state_json, updated_at) VALUES (?, ?, ?) " - "ON CONFLICT(id) DO UPDATE SET state_json=excluded.state_json, " - "updated_at=excluded.updated_at", - (run.id, run.model_dump_json(), run.updated_at.isoformat()), - ) - self._connection.commit() - - def get(self, run_id: str) -> AgentRun: - row = self._connection.execute( - "SELECT state_json FROM agent_runs WHERE id = ?", (run_id,) - ).fetchone() - if row is None: - raise RunNotFoundError(run_id) - return AgentRun.model_validate_json(row[0]) + return self + + def __exit__(self, *_exc: object) -> None: + self.close() + + def save(self, run: AgentRun) -> None: + run.touch() + self._connection.execute( + "INSERT INTO agent_runs(id, state_json, updated_at) VALUES (?, ?, ?) " + "ON CONFLICT(id) DO UPDATE SET state_json=excluded.state_json, " + "updated_at=excluded.updated_at", + (run.id, run.model_dump_json(), run.updated_at.isoformat()), + ) + self._connection.commit() + + def get(self, run_id: str) -> AgentRun: + row = self._connection.execute( + "SELECT state_json FROM agent_runs WHERE id = ?", (run_id,) + ).fetchone() + if row is None: + raise RunNotFoundError(run_id) + return AgentRun.model_validate_json(row[0]) diff --git a/agent/autonomy/tools.py b/agent/autonomy/tools.py index d6ba48b..8c2dd33 100644 --- a/agent/autonomy/tools.py +++ b/agent/autonomy/tools.py @@ -1,69 +1,69 @@ -"""Governed tool registry with typed metadata, budgets, retries, and timeouts.""" - -from __future__ import annotations - -import time -from collections.abc import Callable -from concurrent.futures import ThreadPoolExecutor -from concurrent.futures import TimeoutError as FutureTimeout -from dataclasses import dataclass -from typing import Any - -from agent.autonomy.models import Observation, RiskLevel, Task - -ToolHandler = Callable[[dict[str, Any]], Any] - - -@dataclass(frozen=True) -class ToolSpec: - name: str - handler: ToolHandler - description: str - risk: RiskLevel = RiskLevel.READ_ONLY - timeout_seconds: float = 15.0 - - -class ToolRegistry: - def __init__(self) -> None: - self._tools: dict[str, ToolSpec] = {} - - def register(self, spec: ToolSpec) -> None: - if spec.name in self._tools: - raise ValueError(f"Tool already registered: {spec.name}") - self._tools[spec.name] = spec - - def get(self, name: str) -> ToolSpec: - try: - return self._tools[name] - except KeyError as exc: - raise KeyError(f"Unknown tool: {name}") from exc - - def execute(self, task: Task) -> Observation: - spec = self.get(task.tool) - started = time.perf_counter() - pool = ThreadPoolExecutor(max_workers=1) - future = pool.submit(spec.handler, task.arguments) - try: - output = future.result(spec.timeout_seconds) - pool.shutdown(wait=True) - return Observation( - task_id=task.id, - tool=task.tool, - success=True, - output=output, - duration_ms=(time.perf_counter() - started) * 1_000, - ) - except FutureTimeout: - future.cancel() - pool.shutdown(wait=False, cancel_futures=True) - error = f"Tool timed out after {spec.timeout_seconds:.1f}s" - except Exception as exc: # noqa: BLE001 - tool boundary normalizes failures - pool.shutdown(wait=True) - error = f"{type(exc).__name__}: {exc}" - return Observation( - task_id=task.id, - tool=task.tool, - success=False, - error=error, - duration_ms=(time.perf_counter() - started) * 1_000, - ) +"""Governed tool registry with typed metadata, budgets, retries, and timeouts.""" + +from __future__ import annotations + +import time +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from concurrent.futures import TimeoutError as FutureTimeout +from dataclasses import dataclass +from typing import Any + +from agent.autonomy.models import Observation, RiskLevel, Task + +ToolHandler = Callable[[dict[str, Any]], Any] + + +@dataclass(frozen=True) +class ToolSpec: + name: str + handler: ToolHandler + description: str + risk: RiskLevel = RiskLevel.READ_ONLY + timeout_seconds: float = 15.0 + + +class ToolRegistry: + def __init__(self) -> None: + self._tools: dict[str, ToolSpec] = {} + + def register(self, spec: ToolSpec) -> None: + if spec.name in self._tools: + raise ValueError(f"Tool already registered: {spec.name}") + self._tools[spec.name] = spec + + def get(self, name: str) -> ToolSpec: + try: + return self._tools[name] + except KeyError as exc: + raise KeyError(f"Unknown tool: {name}") from exc + + def execute(self, task: Task) -> Observation: + spec = self.get(task.tool) + started = time.perf_counter() + pool = ThreadPoolExecutor(max_workers=1) + future = pool.submit(spec.handler, task.arguments) + try: + output = future.result(spec.timeout_seconds) + pool.shutdown(wait=True) + return Observation( + task_id=task.id, + tool=task.tool, + success=True, + output=output, + duration_ms=(time.perf_counter() - started) * 1_000, + ) + except FutureTimeout: + future.cancel() + pool.shutdown(wait=False, cancel_futures=True) + error = f"Tool timed out after {spec.timeout_seconds:.1f}s" + except Exception as exc: # noqa: BLE001 - tool boundary normalizes failures + pool.shutdown(wait=True) + error = f"{type(exc).__name__}: {exc}" + return Observation( + task_id=task.id, + tool=task.tool, + success=False, + error=error, + duration_ms=(time.perf_counter() - started) * 1_000, + ) diff --git a/agent/reporter.py b/agent/reporter.py index 85cc3da..7067e1b 100644 --- a/agent/reporter.py +++ b/agent/reporter.py @@ -1,69 +1,69 @@ -# agent/reporter.py -import datetime -import os -import subprocess - -from openai import OpenAI # Or your framework's custom wrapper - - -def get_git_metadata(): - """Extracts recent activity directly from the repository environment.""" - try: - commits = subprocess.check_output( - ["git", "log", "--since=24 hours ago", "--oneline"] - ).decode("utf-8") - diff = subprocess.check_output( - ["git", "diff", "HEAD~1", "HEAD"] - ).decode("utf-8")[:2000] # Cap to prevent context blowing up - return commits, diff +# agent/reporter.py +import datetime +import os +import subprocess + +from openai import OpenAI # Or your framework's custom wrapper + + +def get_git_metadata(): + """Extracts recent activity directly from the repository environment.""" + try: + commits = subprocess.check_output( + ["git", "log", "--since=24 hours ago", "--oneline"] + ).decode("utf-8") + diff = subprocess.check_output( + ["git", "diff", "HEAD~1", "HEAD"] + ).decode("utf-8")[:2000] # Cap to prevent context blowing up + return commits, diff except (OSError, subprocess.CalledProcessError): - return "No recent commits found or shallow clone.", "" - -def generate_daily_log(): - client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) - commits, diff = get_git_metadata() + return "No recent commits found or shallow clone.", "" + +def generate_daily_log(): + client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) + commits, diff = get_git_metadata() date_str = datetime.datetime.now(datetime.timezone.utc).date().strftime("%Y-%m-%d") - - prompt = f""" - You are an autonomous MLOps & System Hygiene Agent responsible for maintaining HelixAgent. - Analyze the following repository changes from the last 24 hours and write a professional system summary for today's log entry. - - Date: {date_str} - Recent Commits: - {commits} - - Recent Code Changes snippet: - {diff} - - Generate a markdown section containing: - 1. **System Health & Metrics**: Summary of code state, stability adjustments, or optimization changes. - 2. **Agent Execution Highlights**: Deduce which components (API, Agent core, Infrastructure) were affected and summarize progress. - 3. **Automated TODOs**: List technical debt or testing gaps discovered from the diff/commits. - - Format output cleanly as markdown. Do not include markdown block wrapping (```markdown). - """ - - response = client.chat.completions.create( - model="gpt-4o-mini", - messages=[{"role": "user", "content": prompt}], - temperature=0.2 - ) - - log_content = response.choices[0].message.content - - # Prepend or Append to DAILYLOG.md - log_file = "DAILYLOG.md" - existing_content = "" - if os.path.exists(log_file): - with open(log_file, "r") as f: - existing_content = f.read() - - header = "# HelixAgent Autonomous Logs\n\n" if not existing_content else "" - - new_entry = f"## Log Entry: {date_str}\n\n{log_content}\n\n---\n\n" - - with open(log_file, "w") as f: - f.write(header + new_entry + existing_content.replace("# HelixAgent Autonomous Logs\n\n", "")) - -if __name__ == "__main__": - generate_daily_log() + + prompt = f""" + You are an autonomous MLOps & System Hygiene Agent responsible for maintaining HelixAgent. + Analyze the following repository changes from the last 24 hours and write a professional system summary for today's log entry. + + Date: {date_str} + Recent Commits: + {commits} + + Recent Code Changes snippet: + {diff} + + Generate a markdown section containing: + 1. **System Health & Metrics**: Summary of code state, stability adjustments, or optimization changes. + 2. **Agent Execution Highlights**: Deduce which components (API, Agent core, Infrastructure) were affected and summarize progress. + 3. **Automated TODOs**: List technical debt or testing gaps discovered from the diff/commits. + + Format output cleanly as markdown. Do not include markdown block wrapping (```markdown). + """ + + response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": prompt}], + temperature=0.2 + ) + + log_content = response.choices[0].message.content + + # Prepend or Append to DAILYLOG.md + log_file = "DAILYLOG.md" + existing_content = "" + if os.path.exists(log_file): + with open(log_file, "r") as f: + existing_content = f.read() + + header = "# HelixAgent Autonomous Logs\n\n" if not existing_content else "" + + new_entry = f"## Log Entry: {date_str}\n\n{log_content}\n\n---\n\n" + + with open(log_file, "w") as f: + f.write(header + new_entry + existing_content.replace("# HelixAgent Autonomous Logs\n\n", "")) + +if __name__ == "__main__": + generate_daily_log() diff --git a/agent/tools/sagemaker_job.py b/agent/tools/sagemaker_job.py index 4110c02..bd72d9a 100644 --- a/agent/tools/sagemaker_job.py +++ b/agent/tools/sagemaker_job.py @@ -1,72 +1,72 @@ -""" -sagemaker_job.py -================ -Tool for the Agentic AI Assistant: trigger SageMaker jobs or invoke -endpoints and surface quick status/results. - -Prerequisites -------------- -pip install boto3==1.34.78 -AWS credentials configured via env vars or IAM role. - -Environment Variables (CI/local) --------------------------------- -AWS_ACCESS_KEY_ID -AWS_SECRET_ACCESS_KEY -AWS_DEFAULT_REGION -""" - - -import boto3 - -sm_client = boto3.client("sagemaker") -runtime = boto3.client("sagemaker-runtime") - -# ------------------------------------------------------------------ # -# Batch-Transform helper -# ------------------------------------------------------------------ # -def start_batch_transform(job_name: str, - model_name: str, - input_s3: str, - output_s3: str, - instance_type: str = "ml.m5.xlarge", - instance_count: int = 1) -> str: - """ - Kick off a batch-transform job and return the ARN. - """ - response = sm_client.create_transform_job( - TransformJobName=job_name, - ModelName=model_name, - TransformInput={ - "DataSource": {"S3DataSource": {"S3Uri": input_s3, "S3DataType": "S3Prefix"}}, - "ContentType": "text/csv" - }, - TransformOutput={"S3OutputPath": output_s3}, - TransformResources={ - "InstanceType": instance_type, - "InstanceCount": instance_count - } - ) - return response["TransformJobArn"] - -def get_batch_status(job_name: str) -> dict: - """Return status dict for batch job.""" - return sm_client.describe_transform_job(TransformJobName=job_name) - -# ------------------------------------------------------------------ # -# Real-time endpoint helper -# ------------------------------------------------------------------ # -def invoke_endpoint(endpoint_name: str, payload: str) -> str: - """ - Invoke a JSON endpoint and return stringified result. - """ - response = runtime.invoke_endpoint( - EndpointName=endpoint_name, - ContentType="application/json", - Body=payload.encode("utf-8") - ) - return response["Body"].read().decode() - -# Quick CLI test (comment out unless creds + endpoint configured) -# if __name__ == "__main__": -# print(invoke_endpoint("my-demo-endpoint", json.dumps({"data": [1,2,3]}))) +""" +sagemaker_job.py +================ +Tool for the Agentic AI Assistant: trigger SageMaker jobs or invoke +endpoints and surface quick status/results. + +Prerequisites +------------- +pip install boto3==1.34.78 +AWS credentials configured via env vars or IAM role. + +Environment Variables (CI/local) +-------------------------------- +AWS_ACCESS_KEY_ID +AWS_SECRET_ACCESS_KEY +AWS_DEFAULT_REGION +""" + + +import boto3 + +sm_client = boto3.client("sagemaker") +runtime = boto3.client("sagemaker-runtime") + +# ------------------------------------------------------------------ # +# Batch-Transform helper +# ------------------------------------------------------------------ # +def start_batch_transform(job_name: str, + model_name: str, + input_s3: str, + output_s3: str, + instance_type: str = "ml.m5.xlarge", + instance_count: int = 1) -> str: + """ + Kick off a batch-transform job and return the ARN. + """ + response = sm_client.create_transform_job( + TransformJobName=job_name, + ModelName=model_name, + TransformInput={ + "DataSource": {"S3DataSource": {"S3Uri": input_s3, "S3DataType": "S3Prefix"}}, + "ContentType": "text/csv" + }, + TransformOutput={"S3OutputPath": output_s3}, + TransformResources={ + "InstanceType": instance_type, + "InstanceCount": instance_count + } + ) + return response["TransformJobArn"] + +def get_batch_status(job_name: str) -> dict: + """Return status dict for batch job.""" + return sm_client.describe_transform_job(TransformJobName=job_name) + +# ------------------------------------------------------------------ # +# Real-time endpoint helper +# ------------------------------------------------------------------ # +def invoke_endpoint(endpoint_name: str, payload: str) -> str: + """ + Invoke a JSON endpoint and return stringified result. + """ + response = runtime.invoke_endpoint( + EndpointName=endpoint_name, + ContentType="application/json", + Body=payload.encode("utf-8") + ) + return response["Body"].read().decode() + +# Quick CLI test (comment out unless creds + endpoint configured) +# if __name__ == "__main__": +# print(invoke_endpoint("my-demo-endpoint", json.dumps({"data": [1,2,3]}))) diff --git a/agent/tools/snowflake_query.py b/agent/tools/snowflake_query.py index 69900be..dff3851 100644 --- a/agent/tools/snowflake_query.py +++ b/agent/tools/snowflake_query.py @@ -1,68 +1,46 @@ -""" -snowflake_query.py -================== -Tool for the Agentic AI Assistant: execute parameterized SQL against -Snowflake and return results in a Pythonic format (list[dict]). - -Requirements ------------- -pip install snowflake-connector-python==3.6.0 - -Environment Variables ---------------------- -SNOWFLAKE_ACCOUNT e.g. abc-xy12345 -SNOWFLAKE_USER e.g. COREY_LEATH -SNOWFLAKE_PASSWORD ***** (or use key-pair auth) -SNOWFLAKE_DATABASE e.g. ANALYTICS_DB -SNOWFLAKE_SCHEMA e.g. PUBLIC -SNOWFLAKE_WAREHOUSE e.g. COMPUTE_WH -""" - -import os -from contextlib import contextmanager - -import snowflake.connector - - -@contextmanager -def snowflake_connection(): - conn = snowflake.connector.connect( - account=os.getenv("SNOWFLAKE_ACCOUNT"), - user=os.getenv("SNOWFLAKE_USER"), - password=os.getenv("SNOWFLAKE_PASSWORD"), - database=os.getenv("SNOWFLAKE_DATABASE"), - schema=os.getenv("SNOWFLAKE_SCHEMA"), - warehouse=os.getenv("SNOWFLAKE_WAREHOUSE"), - ) - try: - yield conn - finally: - conn.close() - -def run_query(sql: str, params: tuple | None = None) -> list[dict]: - """ - Execute SQL and return results as list of dicts. - - Parameters - ---------- - sql : str - Parameterized SQL (use %s placeholders). - params : tuple | None - Values for placeholders. - - Returns - ------- - list[dict] - Query results with keys=column names, values=rows. - """ - with snowflake_connection() as conn: - cur = conn.cursor(snowflake.connector.DictCursor) - cur.execute(sql, params) if params else cur.execute(sql) - results = cur.fetchall() - cur.close() - return results - -# Quick CLI test (commented; ensure env vars first) -# if __name__ == "__main__": -# rows = run_query("SELECT CURRENT_TIMESTAMP() AS ts") -# print(rows) +""" +snowflake_query.py +================== +Tool for the Agentic AI Assistant: execute parameterized SQL against +Snowflake and return results in a Pythonic format (list[dict]). + +Requirements +------------ +pip install snowflake-connector-python==3.6.0 + +Environment Variables +--------------------- +SNOWFLAKE_ACCOUNT e.g. abc-xy12345 +SNOWFLAKE_USER e.g. COREY_LEATH +SNOWFLAKE_PASSWORD ***** (or use key-pair auth) +SNOWFLAKE_DATABASE e.g. ANALYTICS_DB +SNOWFLAKE_SCHEMA e.g. PUBLIC +SNOWFLAKE_WAREHOUSE e.g. COMPUTE_WH +""" + +import os +from contextlib import contextmanager + +import snowflake.connector + + +@contextmanager +def snowflake_connection(): + conn = snowflake.connector.connect( + account=os.getenv("SNOWFLAKE_ACCOUNT"), + user=os.getenv("SNOWFLAKE_USER"), + password=os.getenv("SNOWFLAKE_PASSWORD"), + database=os.getenv("SNOWFLAKE_DATABASE"), + schema=os.getenv("SNOWFLAKE_SCHEMA"), + warehouse=os.getenv("SNOWFLAKE_WAREHOUSE"), + ) + try: + yield conn + finally: + conn.close() + +def run_query(sql: str, params: tuple | None = None) -> list[dict]: + """ + Execute SQL and return results as list of dicts. + + 8-GƭySF7&2F7&2&B$6&RfV7F'2BG&gB7V' "" 'B&w'6P'B0'B702V7W&RFR&V7B&B2FRFvV'Vr267&@72F6W'B2F2FF&RfU""g&7&2WF2vvW"'B6WGWvvW r6WGWvvW"FVb'6U&w2&w'6RW76S'6W"&w'6R&wVVE'6W"FW67&F$VƗvVB( 2vW&VBvVBg&Wv& '6W"FE&wVVB"&B"GS7G"FVfVC$VVƗvVB'VV66RFW7B"V%&BF6VBFFRvVB"&WGW&'6W"'6U&w2FVbₒS&w2'6U&w2rf$VƗvVB7F'Fr"rfb%&C&w2&G"G'g&vVBvVE6&R'BvVF4767F@rf$FƗrvVB6&R"767FBvVF4767FBWGWB767FB'V&w2&Brf$vVB'V6WFR"&Bb$vVBWGWCWGWG"W6WBW6WF2W32$Srv&rb$vVB6&RVf&RW7ғ'VrV6FR"&Bb%VƗvVEV6&w2&G"bU%#ₐ \ No newline at end of file diff --git a/agent/tools/web_search.py b/agent/tools/web_search.py index 419801a..0389b6b 100644 --- a/agent/tools/web_search.py +++ b/agent/tools/web_search.py @@ -1,53 +1,52 @@ -""" -web_search.py -============= -Tool for the Agentic AI Assistant: performs a web search and returns a -short summary suitable for LLM consumption. - -Real implementations could use: - • SerpAPI - • Bing Web Search (Azure Cognitive Services) - • Google Programmable Search - -For demo purposes this module: - 1. Queries DuckDuckGo’s HTML results page - 2. Extracts the top N result titles/snippets - 3. Returns a concatenated summary string - -Dependencies: - pip install ddgs -""" - -from ddgs import DDGS - - -def search_and_summarize(query: str, max_results: int = 5) -> str: - """ - Run a web search and summarize the top results. - - Parameters - ---------- - query : str - The user’s search question. - max_results : int - How many results to consider (default 5). - - Returns - ------- - str - A multi-line summary of result titles + snippets. - """ - summary_lines = [] - with DDGS() as ddgs: - results = ddgs.text(query, max_results=max_results) - for idx, res in enumerate(results, 1): - title = res.get("title", "") - snippet = res.get("body", "") - summary_lines.append(f"{idx}. {title} — {snippet}") - - return "\n".join(summary_lines) - - -# Quick CLI test -if __name__ == "__main__": - print(search_and_summarize("latest advances in agentic AI", 3)) +""" +web_search.py +============= +Tool for the Agentic AI Assistant: performs a web search and returns a +short summary suitable for LLM consumption. + +Real implementations could use: + • SerpAPI + • Bing Web Search (Azure Cognitive Services) + • Google Programmable Search + +For demo purposes this module: + 1. Queries DuckDuckGo’s HTML results page + 2. Extracts the top N result titles/snippets + 3. Returns a concatenated summary string + +Dependencies: + pip install ddgs +""" + +from ddgs import DDGS + +def search_and_summarize(query: str, max_results: int = 5) -> str: + """ + Run a web search and summarize the top results. + + Parameters + ---------- + query : str + The user’s search question. + max_results : int + How many results to consider (default 5). + + Returns + ------- + str + A multi-line summary of result titles + snippets. + """ + summary_lines = [] + with DDGS() as ddgs: + results = ddgs.text(query, max_results=max_results) + for idx, res in enumerate(results, 1): + title = res.get("title", "") + snippet = res.get("body", "") + summary_lines.append(f"{idx}. {title} — {snippet}") + + return "\n".join(summary_lines) + + +# Quick CLI test +if __name__ == "__main__": + print(search_and_summarize("latest advances in agentic AI", 3)) diff --git a/agent/version.py b/agent/version.py deleted file mode 100644 index 1822352..0000000 --- a/agent/version.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Resolve HelixAgent's version from installed package metadata or pyproject.toml.""" - -from __future__ import annotations - -import re -from importlib.metadata import PackageNotFoundError, version -from pathlib import Path - -_PROJECT_VERSION = re.compile(r'^version\s*=\s*"(?P[^"]+)"\s*$', re.MULTILINE) - - -def get_version() -> str: - """Return the installed distribution version or the source-tree project version.""" - try: - return version("helixagent") - except PackageNotFoundError: - pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml" - match = _PROJECT_VERSION.search(pyproject.read_text(encoding="utf-8")) - if match is None: - raise RuntimeError("Could not determine the HelixAgent project version.") - return match.group("version") diff --git a/api/main.py b/api/main.py index e04d217..d76ad28 100644 --- a/api/main.py +++ b/api/main.py @@ -1,52 +1,111 @@ -# api/main.py - -""" -HelixAgent FastAPI Application -------------------------------- -Main API entrypoint. Mounts monitoring (Prometheus + OpenTelemetry) and -exposes core routes for agent inference. -""" - -import os - -from fastapi…2402 tokens truncated…abels: - app: helixagent -data: - config.yaml: | - project: - name: "HelixAgent" - version: "1.1.0" - author: "Corey Leath" - description: "Modular AI agent framework for automation, reasoning, and decision-making." - - logging: - level: "INFO" - log_to_file: true - log_file: "logs/helixagent.log" - - agent: - model: "gpt-neo" - max_tokens: 512 - temperature: 0.7 - top_p: 0.9 - - api: - host: "0.0.0.0" - port: 8000 - reload: true - - dashboard: - host: "0.0.0.0" - port: 8501 - - mlflow: - enabled: true - experiment_name: "HelixAgent-Experiments" - tracking_uri: "http://mlflow:5000" - - data: - input_path: "data/input/" - output_path: "data/output/" - batch_size: 32 - shuffle: true - validation_split: 0.1 +# api/main.py + +""" +HelixAgent FastAPI Application +------------------------------- +Main API entrypoint. Mounts monitoring (Prometheus + OpenTelemetry) and +exposes core routes for agent inference. +""" + +import os + +from fastapi import BackgroundTasks, FastAPI, HTTPException, status +from fastapi.responses import JSONResponse +from pydantic import BaseModel, Field + +from agent.autonomy.models import AgentRun +from agent.autonomy.runtime import AutonomousRuntime +from agent.autonomy.store import RunNotFoundError +from api.monitoring import setup_monitoring + +app = FastAPI( + title="HelixAgent API", + description="Modular AI agent framework for automation, reasoning, and decision-making.", + version="1.0.0", +) + +# Attach Prometheus metrics + OpenTelemetry tracing +setup_monitoring(app) + + +class PredictRequest(BaseModel): + prompt: str = Field(min_length=1, max_length=4_000) + + +class RunRequest(BaseModel): + objective: str = Field(min_length=1, max_length=4_000) + max_iterations: int = Field(default=12, ge=1, le=100) + tool_budget: int = Field(default=10, ge=1, le=100) + + +class ApprovalDecision(BaseModel): + approved: bool + + +runtime = AutonomousRuntime() + + +@app.get("/", include_in_schema=False) +async def root(): + """Redirect root to health check.""" + return JSONResponse({"status": "ok", "service": "HelixAgent"}) + + +@app.get("/health", tags=["Operations"]) +async def health(): + """Liveness / readiness probe for container orchestration.""" + return {"status": "healthy", "version": "1.0.0"} + + +@app.post("/predict", tags=["Agent"]) +async def predict(payload: PredictRequest): + """Backward-compatible synchronous endpoint.""" + run = runtime.submit(payload.prompt) + completed = runtime.run(run.id) + if completed.error: + raise HTTPException(status_code=503, detail={"run_id": run.id, "error": completed.error}) + return {"run_id": run.id, "status": completed.status, "result": completed.final_output} + + +@app.post("/runs", response_model=AgentRun, status_code=status.HTTP_202_ACCEPTED, tags=["Agent"]) +async def create_run(payload: RunRequest, background_tasks: BackgroundTasks) -> AgentRun: + """Create a durable autonomous run and execute it outside the request lifecycle.""" + run = runtime.submit( + payload.objective, + max_iterations=payload.max_iterations, + tool_budget=payload.tool_budget, + ) + background_tasks.add_task(runtime.run, run.id) + return run + + +@app.get("/runs/{run_id}", response_model=AgentRun, tags=["Agent"]) +async def get_run(run_id: str) -> AgentRun: + try: + return runtime.store.get(run_id) + except RunNotFoundError as exc: + raise HTTPException(status_code=404, detail="Run not found") from exc + + +@app.post("/runs/{run_id}/approvals/{task_id}", response_model=AgentRun, tags=["Agent"]) +async def decide_approval( + run_id: str, + task_id: str, + payload: ApprovalDecision, + background_tasks: BackgroundTasks, +) -> AgentRun: + try: + run = runtime.approve(run_id, task_id, payload.approved) + except (RunNotFoundError, KeyError) as exc: + raise HTTPException(status_code=404, detail=str(exc)) from exc + if payload.approved: + background_tasks.add_task(runtime.run, run.id) + return run + + +if __name__ == "__main__": + import uvicorn + + host = os.getenv("API_HOST", "0.0.0.0") + port = int(os.getenv("API_PORT", "8000")) + uvicorn.run("api.main:app", host=host, port=port, reload=False) diff --git a/infra/helm/agentic-ai-assistant/Chart.yaml b/infra/helm/agentic-ai-assistant/Chart.yaml index a07acea..68b82cc 100644 --- a/infra/helm/agentic-ai-assistant/Chart.yaml +++ b/infra/helm/agentic-ai-assistant/Chart.yaml @@ -1,6 +1,6 @@ -apiVersion: v2 -name: agentic-ai-assistant -description: Helm chart for deploying the Agentic AI Assistant (Python + Java + C++) -type: application -version: 0.1.0 -appVersion: "1.1.0" +apiVersion: v2 +name: agentic-ai-assistant +description: Helm chart for deploying the Agentic AI Assistant (Python + Java + C++) +type: application +version: 0.1.0 +appVersion: "1.0.0" diff --git a/pyproject.toml b/pyproject.toml index fc3acba..30495b9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,43 +1,29 @@ [project] name = "helixagent" -version = "1.1.0" +version = "1.0.0" description = "Deterministic budgeted agent runtime with governed tools and SQLite checkpoints." readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "Corey Leath" }] -[project.urls] -Homepage = "https://github.com/CoreyLeath-code/HelixAgent" -Repository = "https://github.com/CoreyLeath-code/HelixAgent" -Changelog = "https://github.com/CoreyLeath-code/HelixAgent/blob/main/CHANGELOG.md" +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["."] +addopts = "-v" -[build-system] -requires = ["setuptools>=68", "wheel"] -build-backend = "setuptools.build_meta" +[tool.coverage.run] +source = ["agent", "src", "api"] +omit = ["tests/*"] -[tool.setuptools.packages.find] -where = ["."] -include = ["agent*", "api*", "src*"] -namespaces = true - -[tool.pytest.ini_options] -testpaths = ["tests"] -pythonpath = ["."] -addopts = "-v" - -[tool.coverage.run] -source = ["agent", "src", "api"] -omit = ["tests/*"] - -[tool.black] -line-length = 99 -target-version = ["py310"] - -[tool.isort] -profile = "black" -line_length = 99 - -[tool.flake8] -max-line-length = 99 -extend-ignore = ["E203", "W503"] +[tool.black] +line-length = 99 +target-version = ["py310"] + +[tool.isort] +profile = "black" +line_length = 99 + +[tool.flake8] +max-line-length = 99 +extend-ignore = ["E203", "W503"] diff --git a/requirements-dev.txt b/requirements-dev.txt index 7c55e4e..ed2cbb9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,8 +1,7 @@ -r requirements.txt -setuptools>=68,<81 pytest-asyncio>=0.23.7 -ruff>=0.9.0 -mypy>=1.14.0 -bandit>=1.8.0 -pip-audit>=2.7.3 -hypothesis>=6.120,<7 +ruff>=0.9.0 +mypy>=1.14.0 +bandit>=1.8.0 +pip-audit>=2.7.3 +hypothesis>=6.120,<7 diff --git a/src/main.py b/src/main.py index 38b9cd8..cd15260 100644 --- a/src/main.py +++ b/src/main.py @@ -1,57 +1,57 @@ -# src/main.py - -""" -HelixAgent CLI Entry Point --------------------------- -Run the agent locally from the command line. - -Usage: - python src/main.py - python src/main.py --prompt "Compare vectors and draft a summary" -""" - -import argparse -import os -import sys - -# Ensure the project root is in the path when running as a script -sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) - -from src.utils.logger import setup_logger - -log = setup_logger() - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="HelixAgent – AI-powered agent framework" - ) - parser.add_argument( - "--prompt", - type=str, - default="Hello, HelixAgent! Run a quick smoke test.", - help="Prompt to send to the agent", - ) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - log.info("HelixAgent starting...") - log.info(f"Prompt: {args.prompt}") - - try: - from agent.agent_core import AgenticAssistant - - log.info("Initializing agent core...") - assistant = AgenticAssistant() - output = assistant.run(args.prompt) - log.info("Agent run complete.") - print(f"Agent Output: {output}") - except Exception as exc: # noqa: BLE001 - log.warning(f"Agent core unavailable ({exc}); running echo mode.") - print(f"[HelixAgent] Echo: {args.prompt}") - - -if __name__ == "__main__": - main() +# src/main.py + +""" +HelixAgent CLI Entry Point +-------------------------- +Run the agent locally from the command line. + +Usage: + python src/main.py + python src/main.py --prompt "Compare vectors and draft a summary" +""" + +import argparse +import os +import sys + +# Ensure the project root is in the path when running as a script +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from src.utils.logger import setup_logger # noqa: E402 + +log = setup_logger() + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="HelixAgent – AI-powered agent framework" + ) + parser.add_argument( + "--prompt", + type=str, + default="Hello, HelixAgent! Run a quick smoke test.", + help="Prompt to send to the agent", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + log.info("HelixAgent starting...") + log.info(f"Prompt: {args.prompt}") + + try: + from agent.agent_core import AgenticAssistant # noqa: E402 + + log.info("Initializing agent core...") + assistant = AgenticAssistant() + output = assistant.run(args.prompt) + log.info("Agent run complete.") + print(f"Agent Output: {output}") + except Exception as exc: # noqa: BLE001 + log.warning(f"Agent core unavailable ({exc}); running echo mode.") + print(f"[HelixAgent] Echo: {args.prompt}") + + +if __name__ == "__main__": + main() diff --git a/src/utils/logger.py b/src/utils/logger.py index 1356fac..98b2d38 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -1,54 +1,54 @@ -# src/utils/logger.py - -""" -Logger Utility for HelixAgent ------------------------------ -Provides centralized structured logging with loguru. -Ensures consistent, professional logs across all modules. -""" - -import sys - -from loguru import logger - - -def setup_logger(log_file: str = "logs/helixagent.log", level: str = "INFO"): - """ - Configure the logger. - - Args: - log_file (str): Path to log file. - level (str): Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). - """ - # Remove default logger to reconfigure - logger.remove() - - # Console output (colorized) - logger.add( - sys.stdout, - format="{time:YYYY-MM-DD HH:mm:ss} | " - "{level: <8} | " - "{name}:{function}:{line} - " - "{message}", - colorize=True, - level=level, - ) - - # File output - logger.add( - log_file, - rotation="5 MB", - retention="10 days", - level=level, - enqueue=True, - format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", - ) - - logger.info("Logger initialized.") - return logger - - -# Example usage: -# from utils.logger import setup_logger -# log = setup_logger() -# log.info("HelixAgent started successfully.") +# src/utils/logger.py + +""" +Logger Utility for HelixAgent +----------------------------- +Provides centralized structured logging with loguru. +Ensures consistent, professional logs across all modules. +""" + +import sys + +from loguru import logger + + +def setup_logger(log_file: str = "logs/helixagent.log", level: str = "INFO"): + """ + Configure the logger. + + Args: + log_file (str): Path to log file. + level (str): Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL). + """ + # Remove default logger to reconfigure + logger.remove() + + # Console output (colorized) + logger.add( + sys.stdout, + format="{time:YYYY-MM-DD HH:mm:ss} | " + "{level: <8} | " + "{name}:{function}:{line} - " + "{message}", + colorize=True, + level=level, + ) + + # File output + logger.add( + log_file, + rotation="5 MB", + retention="10 days", + level=level, + enqueue=True, + format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", + ) + + logger.info("Logger initialized.") + return logger + + +# Example usage: +# from utils.logger import setup_logger +# log = setup_logger() +# log.info("HelixAgent started successfully.") diff --git a/src/utils/tracker.py b/src/utils/tracker.py index ea0e6e3..e86495f 100644 --- a/src/utils/tracker.py +++ b/src/utils/tracker.py @@ -1,61 +1,61 @@ -# src/utils/tracker.py - -""" -Experiment Tracker for HelixAgent ---------------------------------- -Provides MLflow integration for logging experiments, metrics, -parameters, and artifacts. -""" - -import mlflow - -from src.utils.logger import setup_logger - -log = setup_logger() - - -class ExperimentTracker: - def __init__(self, experiment_name: str = "HelixAgent-Experiments", tracking_uri: str = "http://127.0.0.1:5000"): - """ - Initialize MLflow tracker. - - Args: - experiment_name (str): Name of the MLflow experiment. - tracking_uri (str): MLflow tracking server URI. - """ - mlflow.set_tracking_uri(tracking_uri) - mlflow.set_experiment(experiment_name) - log.info(f"Initialized MLflow tracker: {experiment_name}") - +# src/utils/tracker.py + +""" +Experiment Tracker for HelixAgent +--------------------------------- +Provides MLflow integration for logging experiments, metrics, +parameters, and artifacts. +""" + +import mlflow + +from src.utils.logger import setup_logger + +log = setup_logger() + + +class ExperimentTracker: + def __init__(self, experiment_name: str = "HelixAgent-Experiments", tracking_uri: str = "http://127.0.0.1:5000"): + """ + Initialize MLflow tracker. + + Args: + experiment_name (str): Name of the MLflow experiment. + tracking_uri (str): MLflow tracking server URI. + """ + mlflow.set_tracking_uri(tracking_uri) + mlflow.set_experiment(experiment_name) + log.info(f"Initialized MLflow tracker: {experiment_name}") + def start_run(self, run_name: str | None = None): - """Start a new MLflow run""" - return mlflow.start_run(run_name=run_name) - - def log_params(self, params: dict): - """Log parameters to MLflow""" - mlflow.log_params(params) - log.debug(f"Logged parameters: {params}") - + """Start a new MLflow run""" + return mlflow.start_run(run_name=run_name) + + def log_params(self, params: dict): + """Log parameters to MLflow""" + mlflow.log_params(params) + log.debug(f"Logged parameters: {params}") + def log_metrics(self, metrics: dict, step: int | None = None): - """Log metrics to MLflow""" - mlflow.log_metrics(metrics, step=step) - log.debug(f"Logged metrics: {metrics}") - - def log_artifact(self, file_path: str): - """Log a file (artifact) to MLflow""" - mlflow.log_artifact(file_path) - log.debug(f"Logged artifact: {file_path}") - - def end_run(self): - """End the current MLflow run""" - mlflow.end_run() - log.info("MLflow run ended.") - - -# Example usage: -# tracker = ExperimentTracker() -# with tracker.start_run("test-run"): -# tracker.log_params({"learning_rate": 0.001, "batch_size": 32}) -# tracker.log_metrics({"accuracy": 0.85}, step=1) -# tracker.log_artifact("models/agent_model.pt") -# tracker.end_run() + """Log metrics to MLflow""" + mlflow.log_metrics(metrics, step=step) + log.debug(f"Logged metrics: {metrics}") + + def log_artifact(self, file_path: str): + """Log a file (artifact) to MLflow""" + mlflow.log_artifact(file_path) + log.debug(f"Logged artifact: {file_path}") + + def end_run(self): + """End the current MLflow run""" + mlflow.end_run() + log.info("MLflow run ended.") + + +# Example usage: +# tracker = ExperimentTracker() +# with tracker.start_run("test-run"): +# tracker.log_params({"learning_rate": 0.001, "batch_size": 32}) +# tracker.log_metrics({"accuracy": 0.85}, step=1) +# tracker.log_artifact("models/agent_model.pt") +# tracker.end_run() diff --git a/streamlit_app.py b/streamlit_app.py index b7deb2f..3d76c6c 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -1,197 +1,197 @@ -"""Interactive Streamlit demo for HelixAgent. - -This entry point is intentionally self-contained so it can be deployed directly -from the repository root on Streamlit Community Cloud. -""" - -from __future__ import annotations - -import time -from dataclasses import dataclass - -import streamlit as st - -from agent.agent_core import AgenticAssistant, cosine_sim - - -@dataclass(frozen=True) -class DemoExample: - """Preset prompt shown in the demo sidebar.""" - - label: str - prompt: str - - -EXAMPLES = ( - DemoExample( - "System summary", - "Summarize the current HelixAgent architecture and explain its main engineering strengths.", - ), - DemoExample( - "Vector workflow", - "Compare vectors and explain why cosine similarity is useful in AI systems.", - ), - DemoExample( - "Web-assisted plan", - "Search the web for recent MLOps reliability practices and produce a concise implementation plan.", - ), -) - - -@st.cache_resource(show_spinner=False) -def get_assistant() -> AgenticAssistant: - """Create one reusable agent instance per Streamlit session process.""" - - return AgenticAssistant() - - -def render_sidebar() -> None: - """Render architecture, capability, and example-prompt controls.""" - - with st.sidebar: - st.title("🧬 HelixAgent") - st.caption("Autonomous MLOps & multi-agent infrastructure demo") - - st.subheader("Capabilities") - st.markdown( - """ -- LangGraph workflow orchestration -- Python fallback planner -- Optional Java planner integration -- NumPy-default vectors with optional C++ ctypes interop -- FastAPI service layer -- CI/CD and supply-chain security -""" - ) - - st.subheader("Try an example") - for example in EXAMPLES: - if st.button(example.label, use_container_width=True): - st.session_state["prompt"] = example.prompt - - st.divider() - st.markdown( - "[View the source on GitHub](https://github.com/CoreyLeath-code/HelixAgent)" - ) - st.caption( - "The public demo uses built-in fallback behavior when optional native or external services are unavailable." - ) - - -def render_metrics() -> None: - """Show concise implementation details at the top of the page.""" - - col1, col2, col3, col4 = st.columns(4) - col1.metric("API", "FastAPI") - col2.metric("Orchestration", "LangGraph") - col3.metric("Runtime", "Python 3.11") - col4.metric("Deployment", "Streamlit") - - -def render_architecture() -> None: - """Render a lightweight architecture diagram without external assets.""" - - with st.expander("Architecture overview"): - st.graphviz_chart( - """ -digraph HelixAgent { - rankdir=LR; - node [shape=box, style=rounded]; - User -> Streamlit; - Streamlit -> Orchestrator; - Orchestrator -> Planner; - Orchestrator -> VectorUtility; - Orchestrator -> WebSearch; - Planner -> Response; - VectorUtility -> Response; - WebSearch -> Response; -} -""", - use_container_width=True, - ) - - -def render_vector_lab() -> None: - """Expose the vector utility as a transparent, deterministic mini-demo.""" - - with st.expander("Vector similarity lab"): - st.write("Compare two three-dimensional vectors using HelixAgent's cosine utility.") - left = st.text_input("Vector A", "1, 0, 1") - right = st.text_input("Vector B", "0.5, 0, 0.5") - - if st.button("Calculate similarity"): - try: - vector_a = [float(value.strip()) for value in left.split(",")] - vector_b = [float(value.strip()) for value in right.split(",")] - if len(vector_a) != len(vector_b) or not vector_a: - raise ValueError("Vectors must be non-empty and have equal dimensions.") - score = cosine_sim(vector_a, vector_b) - except ValueError as exc: - st.error(str(exc)) - else: - st.success(f"Cosine similarity: {score:.4f}") - - -def run_agent(prompt: str) -> tuple[str, float]: - """Run the agent and return its response with elapsed time.""" - - started = time.perf_counter() - response = get_assistant().run(prompt) - elapsed = time.perf_counter() - started - return response, elapsed - - -def main() -> None: - """Render the HelixAgent Streamlit application.""" - - st.set_page_config( - page_title="HelixAgent Demo", - page_icon="🧬", - layout="wide", - initial_sidebar_state="expanded", - ) - - render_sidebar() - - st.title("🧬 HelixAgent") - st.subheader("Enterprise multi-agent AI orchestration demo") - st.write( - "Explore HelixAgent's planning, orchestration, fallback execution, and vector-processing capabilities through an interactive interface." - ) - - render_metrics() - render_architecture() - - prompt = st.text_area( - "Ask HelixAgent", - key="prompt", - height=140, - placeholder="Example: Compare vectors and then draft an implementation summary.", - ) - - run_clicked = st.button("Run HelixAgent", type="primary", use_container_width=True) - if run_clicked: - if not prompt.strip(): - st.warning("Enter a prompt before running the agent.") - else: - with st.spinner("Planning and executing the workflow..."): - try: - response, elapsed = run_agent(prompt.strip()) +"""Interactive Streamlit demo for HelixAgent. + +This entry point is intentionally self-contained so it can be deployed directly +from the repository root on Streamlit Community Cloud. +""" + +from __future__ import annotations + +import time +from dataclasses import dataclass + +import streamlit as st + +from agent.agent_core import AgenticAssistant, cosine_sim + + +@dataclass(frozen=True) +class DemoExample: + """Preset prompt shown in the demo sidebar.""" + + label: str + prompt: str + + +EXAMPLES = ( + DemoExample( + "System summary", + "Summarize the current HelixAgent architecture and explain its main engineering strengths.", + ), + DemoExample( + "Vector workflow", + "Compare vectors and explain why cosine similarity is useful in AI systems.", + ), + DemoExample( + "Web-assisted plan", + "Search the web for recent MLOps reliability practices and produce a concise implementation plan.", + ), +) + + +@st.cache_resource(show_spinner=False) +def get_assistant() -> AgenticAssistant: + """Create one reusable agent instance per Streamlit session process.""" + + return AgenticAssistant() + + +def render_sidebar() -> None: + """Render architecture, capability, and example-prompt controls.""" + + with st.sidebar: + st.title("🧬 HelixAgent") + st.caption("Autonomous MLOps & multi-agent infrastructure demo") + + st.subheader("Capabilities") + st.markdown( + """ +- LangGraph workflow orchestration +- Python fallback planner +- Optional Java planner integration +- NumPy-default vectors with optional C++ ctypes interop +- FastAPI service layer +- CI/CD and supply-chain security +""" + ) + + st.subheader("Try an example") + for example in EXAMPLES: + if st.button(example.label, use_container_width=True): + st.session_state["prompt"] = example.prompt + + st.divider() + st.markdown( + "[View the source on GitHub](https://github.com/CoreyLeath-code/HelixAgent)" + ) + st.caption( + "The public demo uses built-in fallback behavior when optional native or external services are unavailable." + ) + + +def render_metrics() -> None: + """Show concise implementation details at the top of the page.""" + + col1, col2, col3, col4 = st.columns(4) + col1.metric("API", "FastAPI") + col2.metric("Orchestration", "LangGraph") + col3.metric("Runtime", "Python 3.11") + col4.metric("Deployment", "Streamlit") + + +def render_architecture() -> None: + """Render a lightweight architecture diagram without external assets.""" + + with st.expander("Architecture overview"): + st.graphviz_chart( + """ +digraph HelixAgent { + rankdir=LR; + node [shape=box, style=rounded]; + User -> Streamlit; + Streamlit -> Orchestrator; + Orchestrator -> Planner; + Orchestrator -> VectorUtility; + Orchestrator -> WebSearch; + Planner -> Response; + VectorUtility -> Response; + WebSearch -> Response; +} +""", + use_container_width=True, + ) + + +def render_vector_lab() -> None: + """Expose the vector utility as a transparent, deterministic mini-demo.""" + + with st.expander("Vector similarity lab"): + st.write("Compare two three-dimensional vectors using HelixAgent's cosine utility.") + left = st.text_input("Vector A", "1, 0, 1") + right = st.text_input("Vector B", "0.5, 0, 0.5") + + if st.button("Calculate similarity"): + try: + vector_a = [float(value.strip()) for value in left.split(",")] + vector_b = [float(value.strip()) for value in right.split(",")] + if len(vector_a) != len(vector_b) or not vector_a: + raise ValueError("Vectors must be non-empty and have equal dimensions.") + score = cosine_sim(vector_a, vector_b) + except ValueError as exc: + st.error(str(exc)) + else: + st.success(f"Cosine similarity: {score:.4f}") + + +def run_agent(prompt: str) -> tuple[str, float]: + """Run the agent and return its response with elapsed time.""" + + started = time.perf_counter() + response = get_assistant().run(prompt) + elapsed = time.perf_counter() - started + return response, elapsed + + +def main() -> None: + """Render the HelixAgent Streamlit application.""" + + st.set_page_config( + page_title="HelixAgent Demo", + page_icon="🧬", + layout="wide", + initial_sidebar_state="expanded", + ) + + render_sidebar() + + st.title("🧬 HelixAgent") + st.subheader("Enterprise multi-agent AI orchestration demo") + st.write( + "Explore HelixAgent's planning, orchestration, fallback execution, and vector-processing capabilities through an interactive interface." + ) + + render_metrics() + render_architecture() + + prompt = st.text_area( + "Ask HelixAgent", + key="prompt", + height=140, + placeholder="Example: Compare vectors and then draft an implementation summary.", + ) + + run_clicked = st.button("Run HelixAgent", type="primary", use_container_width=True) + if run_clicked: + if not prompt.strip(): + st.warning("Enter a prompt before running the agent.") + else: + with st.spinner("Planning and executing the workflow..."): + try: + response, elapsed = run_agent(prompt.strip()) except Exception as exc: # noqa: BLE001 - defensive UI boundary - st.error("HelixAgent could not complete this request.") - st.exception(exc) - else: - st.subheader("Agent response") - st.code(response, language="text") - st.caption(f"Completed in {elapsed:.3f} seconds") - - render_vector_lab() - - st.divider() - st.caption( - "Portfolio demonstration by Corey Leath · Built with Streamlit, FastAPI, LangGraph, Python, and optional native integrations." - ) - - -if __name__ == "__main__": - main() + st.error("HelixAgent could not complete this request.") + st.exception(exc) + else: + st.subheader("Agent response") + st.code(response, language="text") + st.caption(f"Completed in {elapsed:.3f} seconds") + + render_vector_lab() + + st.divider() + st.caption( + "Portfolio demonstration by Corey Leath · Built with Streamlit, FastAPI, LangGraph, Python, and optional native integrations." + ) + + +if __name__ == "__main__": + main() diff --git a/tests/test_runtime_invariants.py b/tests/test_runtime_invariants.py index 8ee07b0..68421f0 100644 --- a/tests/test_runtime_invariants.py +++ b/tests/test_runtime_invariants.py @@ -1,129 +1,129 @@ -"""Adversarial contracts for runtime terminality and vector semantics.""" - -from __future__ import annotations - -import math -from pathlib import Path - -import pytest -from hypothesis import given -from hypothesis import strategies as st - -from agent import agent_core -from agent.autonomy.models import GoalSpec, RunStatus, Task -from agent.autonomy.runtime import AutonomousRuntime -from agent.autonomy.store import SQLiteRunStore -from agent.autonomy.tools import ToolRegistry, ToolSpec - - -class StaticPlanner: - def __init__(self, tasks: list[Task]) -> None: - self.tasks = tasks - - def create_plan(self, _goal: GoalSpec) -> list[Task]: - return [task.model_copy(deep=True) for task in self.tasks] - - def replan(self, run, _failed): - return run.plan - - -def make_runtime(tmp_path: Path, tasks: list[Task], registry: ToolRegistry) -> AutonomousRuntime: - return AutonomousRuntime( - planner=StaticPlanner(tasks), - registry=registry, - store=SQLiteRunStore(tmp_path / "invariants.db"), - ) - - -def test_completed_run_is_not_executed_twice(tmp_path: Path) -> None: - calls: list[str] = [] - registry = ToolRegistry() - registry.register( - ToolSpec("once", lambda _arguments: calls.append("executed") or "done", "One call") - ) - runtime = make_runtime(tmp_path, [Task(objective="Run once", tool="once")], registry) - - submitted = runtime.submit("Run one task") - first = runtime.run(submitted.id) - second = runtime.run(submitted.id) - - assert first.status is RunStatus.COMPLETED - assert second.status is RunStatus.COMPLETED - assert calls == ["executed"] - assert second.tool_calls == 1 - - -def test_unknown_planned_tool_fails_and_persists(tmp_path: Path) -> None: - runtime = make_runtime( - tmp_path, - [Task(objective="Unknown", tool="missing_tool")], - ToolRegistry(), - ) - - completed = runtime.run(runtime.submit("Exercise failure boundary").id) - restored = runtime.store.get(completed.id) - - assert completed.status is RunStatus.FAILED - assert "Unknown tool" in completed.error - assert restored.status is RunStatus.FAILED - assert restored.tool_calls == 0 - - -def test_python_vector_fallback_defines_zero_and_dimension_behavior(monkeypatch) -> None: - monkeypatch.setattr(agent_core, "_lib_vec", None) - - assert agent_core.cosine_similarity_python([0.0, 0.0], [1.0, -1.0]) == 0.0 - with pytest.raises(ValueError, match="equal dimensions"): - agent_core.cosine_similarity_python([1.0], [1.0, 2.0]) - - -@pytest.mark.parametrize( - ("left", "right", "expected"), - [ - ([3.565393874732073e-277], [-1.0], -1.0), - ([1e308, 1e308], [1e308, 1e308], 1.0), - ], -) -def test_python_vector_fallback_is_stable_at_extreme_scales( - monkeypatch, left, right, expected -) -> None: - monkeypatch.setattr(agent_core, "_lib_vec", None) - - assert math.isclose( - agent_core.cosine_similarity_python(left, right), expected, rel_tol=1e-12, abs_tol=1e-12 - ) - - -@st.composite -def nonzero_vector_pairs(draw): - dimension = draw(st.integers(min_value=1, max_value=12)) - values = st.floats( - min_value=-1_000_000, - max_value=1_000_000, - allow_nan=False, - allow_infinity=False, - ) - left = draw(st.lists(values, min_size=dimension, max_size=dimension)) - right = draw(st.lists(values, min_size=dimension, max_size=dimension)) - if not any(left): - left[0] = 1.0 - if not any(right): - right[0] = -1.0 - return left, right - - -@given(nonzero_vector_pairs()) -def test_python_cosine_fallback_satisfies_basic_properties(pair) -> None: - # Hypothesis executes many examples, so use a local patch context instead of - # pytest's function-scoped monkeypatch fixture. - with pytest.MonkeyPatch.context() as patch: - patch.setattr(agent_core, "_lib_vec", None) - left, right = pair - - score = agent_core.cosine_similarity_python(left, right) - reverse = agent_core.cosine_similarity_python(right, left) - self_score = agent_core.cosine_similarity_python(left, left) - - assert -1.0 - 1e-12 <= score <= 1.0 + 1e-12 - assert math.isclose(score, reverse, rel_tol=1e-12, abs_tol=1e-12) - assert math.isclose(self_score, 1.0, rel_tol=1e-12, abs_tol=1e-12) +"""Adversarial contracts for runtime terminality and vector semantics.""" + +from __future__ import annotations + +import math +from pathlib import Path + +import pytest +from hypothesis import given +from hypothesis import strategies as st + +from agent import agent_core +from agent.autonomy.models import GoalSpec, RunStatus, Task +from agent.autonomy.runtime import AutonomousRuntime +from agent.autonomy.store import SQLiteRunStore +from agent.autonomy.tools import ToolRegistry, ToolSpec + + +class StaticPlanner: + def __init__(self, tasks: list[Task]) -> None: + self.tasks = tasks + + def create_plan(self, _goal: GoalSpec) -> list[Task]: + return [task.model_copy(deep=True) for task in self.tasks] + + def replan(self, run, _failed): + return run.plan + + +def make_runtime(tmp_path: Path, tasks: list[Task], registry: ToolRegistry) -> AutonomousRuntime: + return AutonomousRuntime( + planner=StaticPlanner(tasks), + registry=registry, + store=SQLiteRunStore(tmp_path / "invariants.db"), + ) + + +def test_completed_run_is_not_executed_twice(tmp_path: Path) -> None: + calls: list[str] = [] + registry = ToolRegistry() + registry.register( + ToolSpec("once", lambda _arguments: calls.append("executed") or "done", "One call") + ) + runtime = make_runtime(tmp_path, [Task(objective="Run once", tool="once")], registry) + + submitted = runtime.submit("Run one task") + first = runtime.run(submitted.id) + second = runtime.run(submitted.id) + + assert first.status is RunStatus.COMPLETED + assert second.status is RunStatus.COMPLETED + assert calls == ["executed"] + assert second.tool_calls == 1 + + +def test_unknown_planned_tool_fails_and_persists(tmp_path: Path) -> None: + runtime = make_runtime( + tmp_path, + [Task(objective="Unknown", tool="missing_tool")], + ToolRegistry(), + ) + + completed = runtime.run(runtime.submit("Exercise failure boundary").id) + restored = runtime.store.get(completed.id) + + assert completed.status is RunStatus.FAILED + assert "Unknown tool" in completed.error + assert restored.status is RunStatus.FAILED + assert restored.tool_calls == 0 + + +def test_python_vector_fallback_defines_zero_and_dimension_behavior(monkeypatch) -> None: + monkeypatch.setattr(agent_core, "_lib_vec", None) + + assert agent_core.cosine_similarity_python([0.0, 0.0], [1.0, -1.0]) == 0.0 + with pytest.raises(ValueError, match="equal dimensions"): + agent_core.cosine_similarity_python([1.0], [1.0, 2.0]) + + +@pytest.mark.parametrize( + ("left", "right", "expected"), + [ + ([3.565393874732073e-277], [-1.0], -1.0), + ([1e308, 1e308], [1e308, 1e308], 1.0), + ], +) +def test_python_vector_fallback_is_stable_at_extreme_scales( + monkeypatch, left, right, expected +) -> None: + monkeypatch.setattr(agent_core, "_lib_vec", None) + + assert math.isclose( + agent_core.cosine_similarity_python(left, right), expected, rel_tol=1e-12, abs_tol=1e-12 + ) + + +@st.composite +def nonzero_vector_pairs(draw): + dimension = draw(st.integers(min_value=1, max_value=12)) + values = st.floats( + min_value=-1_000_000, + max_value=1_000_000, + allow_nan=False, + allow_infinity=False, + ) + left = draw(st.lists(values, min_size=dimension, max_size=dimension)) + right = draw(st.lists(values, min_size=dimension, max_size=dimension)) + if not any(left): + left[0] = 1.0 + if not any(right): + right[0] = -1.0 + return left, right + + +@given(nonzero_vector_pairs()) +def test_python_cosine_fallback_satisfies_basic_properties(pair) -> None: + # Hypothesis executes many examples, so use a local patch context instead of + # pytest's function-scoped monkeypatch fixture. + with pytest.MonkeyPatch.context() as patch: + patch.setattr(agent_core, "_lib_vec", None) + left, right = pair + + score = agent_core.cosine_similarity_python(left, right) + reverse = agent_core.cosine_similarity_python(right, left) + self_score = agent_core.cosine_similarity_python(left, left) + + assert -1.0 - 1e-12 <= score <= 1.0 + 1e-12 + assert math.isclose(score, reverse, rel_tol=1e-12, abs_tol=1e-12) + assert math.isclose(self_score, 1.0, rel_tol=1e-12, abs_tol=1e-12) From 778298239b66d7efddf276a561a6ac234e823f7d Mon Sep 17 00:00:00 2001 From: Corey Leath Date: Tue, 18 Aug 2026 20:10:31 -0400 Subject: [PATCH 3/5] Complete v1.1.0 packaging metadata --- agent/version.py | 21 +++++++++++++++++++++ api/main.py | 7 +++++-- api/monitoring.py | 2 +- config/config.yaml | 2 +- github/ISSUE_TEMPLATE/bug_report.yml | 2 +- helm/Chart.yaml | 2 +- helm/configmap.yaml | 2 +- infra/helm/agentic-ai-assistant/Chart.yaml | 2 +- pyproject.toml | 16 +++++++++++++++- requirements-dev.txt | 1 + src/main.py | 4 ++-- 11 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 agent/version.py diff --git a/agent/version.py b/agent/version.py new file mode 100644 index 0000000..1822352 --- /dev/null +++ b/agent/version.py @@ -0,0 +1,21 @@ +"""Resolve HelixAgent's version from installed package metadata or pyproject.toml.""" + +from __future__ import annotations + +import re +from importlib.metadata import PackageNotFoundError, version +from pathlib import Path + +_PROJECT_VERSION = re.compile(r'^version\s*=\s*"(?P[^"]+)"\s*$', re.MULTILINE) + + +def get_version() -> str: + """Return the installed distribution version or the source-tree project version.""" + try: + return version("helixagent") + except PackageNotFoundError: + pyproject = Path(__file__).resolve().parents[1] / "pyproject.toml" + match = _PROJECT_VERSION.search(pyproject.read_text(encoding="utf-8")) + if match is None: + raise RuntimeError("Could not determine the HelixAgent project version.") + return match.group("version") diff --git a/api/main.py b/api/main.py index d76ad28..f2c3920 100644 --- a/api/main.py +++ b/api/main.py @@ -16,12 +16,15 @@ from agent.autonomy.models import AgentRun from agent.autonomy.runtime import AutonomousRuntime from agent.autonomy.store import RunNotFoundError +from agent.version import get_version from api.monitoring import setup_monitoring +APP_VERSION = get_version() + app = FastAPI( title="HelixAgent API", description="Modular AI agent framework for automation, reasoning, and decision-making.", - version="1.0.0", + version=APP_VERSION, ) # Attach Prometheus metrics + OpenTelemetry tracing @@ -54,7 +57,7 @@ async def root(): @app.get("/health", tags=["Operations"]) async def health(): """Liveness / readiness probe for container orchestration.""" - return {"status": "healthy", "version": "1.0.0"} + return {"status": "healthy", "version": APP_VERSION} @app.post("/predict", tags=["Agent"]) diff --git a/api/monitoring.py b/api/monitoring.py index ab2128c..a02a7cc 100644 --- a/api/monitoring.py +++ b/api/monitoring.py @@ -9,11 +9,11 @@ import os import time +from fastapi import FastAPI, Request from opentelemetry import trace from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter -from fastapi import FastAPI, Request from prometheus_client import CONTENT_TYPE_LATEST, Counter, Histogram, generate_latest from starlette.responses import Response diff --git a/config/config.yaml b/config/config.yaml index cab7f91..f7d0eee 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -5,7 +5,7 @@ # General project settings project: name: "HelixAgent" - version: "1.0.0" + version: "1.1.0" author: "Corey Leath" description: "Modular AI agent framework for automation, reasoning, and decision-making." diff --git a/github/ISSUE_TEMPLATE/bug_report.yml b/github/ISSUE_TEMPLATE/bug_report.yml index 1180c33..2e6649f 100644 --- a/github/ISSUE_TEMPLATE/bug_report.yml +++ b/github/ISSUE_TEMPLATE/bug_report.yml @@ -16,7 +16,7 @@ body: placeholder: | - OS: Ubuntu 22.04 - Python: 3.10.12 - - Docker tag: agentic-ai:1.0.0 + - Docker tag: helixagent:1.1.0 - Commit: abc123 validations: required: true diff --git a/helm/Chart.yaml b/helm/Chart.yaml index 9586e87..8156e7d 100644 --- a/helm/Chart.yaml +++ b/helm/Chart.yaml @@ -3,7 +3,7 @@ name: helixagent description: A modular AI agent framework deployed with Kubernetes + Helm type: application version: 0.1.0 -appVersion: "1.0.0" +appVersion: "1.1.0" maintainers: - name: Corey Leath email: corey.leath@example.com diff --git a/helm/configmap.yaml b/helm/configmap.yaml index b889501..bb3ffb5 100644 --- a/helm/configmap.yaml +++ b/helm/configmap.yaml @@ -8,7 +8,7 @@ data: config.yaml: | project: name: "HelixAgent" - version: "1.0.0" + version: "1.1.0" author: "Corey Leath" description: "Modular AI agent framework for automation, reasoning, and decision-making." diff --git a/infra/helm/agentic-ai-assistant/Chart.yaml b/infra/helm/agentic-ai-assistant/Chart.yaml index 68b82cc..5bed7a5 100644 --- a/infra/helm/agentic-ai-assistant/Chart.yaml +++ b/infra/helm/agentic-ai-assistant/Chart.yaml @@ -3,4 +3,4 @@ name: agentic-ai-assistant description: Helm chart for deploying the Agentic AI Assistant (Python + Java + C++) type: application version: 0.1.0 -appVersion: "1.0.0" +appVersion: "1.1.0" diff --git a/pyproject.toml b/pyproject.toml index 30495b9..ce7c751 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,26 @@ [project] name = "helixagent" -version = "1.0.0" +version = "1.1.0" description = "Deterministic budgeted agent runtime with governed tools and SQLite checkpoints." readme = "README.md" requires-python = ">=3.10" license = { text = "MIT" } authors = [{ name = "Corey Leath" }] +[project.urls] +Homepage = "https://github.com/CoreyLeath-code/HelixAgent" +Repository = "https://github.com/CoreyLeath-code/HelixAgent" +Changelog = "https://github.com/CoreyLeath-code/HelixAgent/blob/main/CHANGELOG.md" + +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[tool.setuptools.packages.find] +where = ["."] +include = ["agent*", "api*", "src*"] +namespaces = true + [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] diff --git a/requirements-dev.txt b/requirements-dev.txt index ed2cbb9..1764fd4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,4 +1,5 @@ -r requirements.txt +setuptools>=68,<81 pytest-asyncio>=0.23.7 ruff>=0.9.0 mypy>=1.14.0 diff --git a/src/main.py b/src/main.py index cd15260..0e5cdfa 100644 --- a/src/main.py +++ b/src/main.py @@ -17,7 +17,7 @@ # Ensure the project root is in the path when running as a script sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from src.utils.logger import setup_logger # noqa: E402 +from src.utils.logger import setup_logger log = setup_logger() @@ -41,7 +41,7 @@ def main() -> None: log.info(f"Prompt: {args.prompt}") try: - from agent.agent_core import AgenticAssistant # noqa: E402 + from agent.agent_core import AgenticAssistant log.info("Initializing agent core...") assistant = AgenticAssistant() From 2418905609af2944d5312635ae397c37d162314b Mon Sep 17 00:00:00 2001 From: Corey Leath Date: Tue, 18 Aug 2026 20:13:44 -0400 Subject: [PATCH 4/5] Fix release lint and encoding checks --- agent/tools/snowflake_query.py | 15 +++++++++------ pyproject.toml | 3 +++ src/main.py | 4 ++-- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/agent/tools/snowflake_query.py b/agent/tools/snowflake_query.py index dff3851..a408252 100644 --- a/agent/tools/snowflake_query.py +++ b/agent/tools/snowflake_query.py @@ -1,5 +1,4 @@ -""" -snowflake_query.py +"""snowflake_query.py ================== Tool for the Agentic AI Assistant: execute parameterized SQL against Snowflake and return results in a Pythonic format (list[dict]). @@ -39,8 +38,12 @@ def snowflake_connection(): finally: conn.close() -def run_query(sql: str, params: tuple | None = None) -> list[dict]: - """ - Execute SQL and return results as list of dicts. - 8-GƭySF7&2F7&2&B$6&RfV7F'2BG&gB7V' "" 'B&w'6P'B0'B702V7W&RFR&V7B&B2FRFvV'Vr267&@72F6W'B2F2FF&RfU""g&7&2WF2vvW"'B6WGWvvW r6WGWvvW"FVb'6U&w2&w'6RW76S'6W"&w'6R&wVVE'6W"FW67&F$VƗvVB( 2vW&VBvVBg&Wv& '6W"FE&wVVB"&B"GS7G"FVfVC$VVƗvVB'VV66RFW7B"V%&BF6VBFFRvVB"&WGW&'6W"'6U&w2FVbₒS&w2'6U&w2rf$VƗvVB7F'Fr"rfb%&C&w2&G"G'g&vVBvVE6&R'BvVF4767F@rf$FƗrvVB6&R"767FBvVF4767FBWGWB767FB'V&w2&Brf$vVB'V6WFR"&Bb$vVBWGWCWGWG"W6WBW6WF2W32$Srv&rb$vVB6&RVf&RW7ғ'VrV6FR"&Bb%VƗvVEV6&w2&G"bU%#ₐ \ No newline at end of file +def run_query(sql: str, params: tuple | None = None) -> list[dict]: + """Execute SQL and return results as list of dictionaries.""" + with snowflake_connection() as conn: + cur = conn.cursor(snowflake.connector.DictCursor) + cur.execute(sql, params) if params else cur.execute(sql) + results = cur.fetchall() + cur.close() + return results diff --git a/pyproject.toml b/pyproject.toml index ce7c751..5c421b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,9 @@ where = ["."] include = ["agent*", "api*", "src*"] namespaces = true +[tool.ruff.lint.per-file-ignores] +"src/main.py" = ["RUF100"] + [tool.pytest.ini_options] testpaths = ["tests"] pythonpath = ["."] diff --git a/src/main.py b/src/main.py index 0e5cdfa..cd15260 100644 --- a/src/main.py +++ b/src/main.py @@ -17,7 +17,7 @@ # Ensure the project root is in the path when running as a script sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) -from src.utils.logger import setup_logger +from src.utils.logger import setup_logger # noqa: E402 log = setup_logger() @@ -41,7 +41,7 @@ def main() -> None: log.info(f"Prompt: {args.prompt}") try: - from agent.agent_core import AgenticAssistant + from agent.agent_core import AgenticAssistant # noqa: E402 log.info("Initializing agent core...") assistant = AgenticAssistant() From 13db4df11110510bf938221b31e587bfdedd28a8 Mon Sep 17 00:00:00 2001 From: Corey Leath Date: Tue, 18 Aug 2026 20:16:22 -0400 Subject: [PATCH 5/5] Restore Python 3.10 store typing compatibility --- agent/autonomy/store.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/agent/autonomy/store.py b/agent/autonomy/store.py index d89fc92..cc1d3f4 100644 --- a/agent/autonomy/store.py +++ b/agent/autonomy/store.py @@ -5,7 +5,6 @@ import os import sqlite3 from pathlib import Path -from typing import Self from agent.autonomy.models import AgentRun @@ -30,7 +29,7 @@ def close(self) -> None: """Release the database handle deterministically.""" self._connection.close() - def __enter__(self) -> Self: + def __enter__(self) -> SQLiteRunStore: # noqa: PYI034 - Python 3.10 compatibility return self def __exit__(self, *_exc: object) -> None: