diff --git a/CHANGELOG.md b/CHANGELOG.md index 530f0bb..3380d36 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,12 @@ The project follows Semantic Versioning and the Keep a Changelog format. ### Added +### Changed + +## [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. @@ -37,5 +43,6 @@ The project follows Semantic Versioning and the Keep a Changelog format. - FastAPI service. - Initial Docker and test infrastructure. -[Unreleased]: https://github.com/CoreyLeath-code/HelixAgent/compare/v1.0.0...HEAD +[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..cc1d3f4 100644 --- a/agent/autonomy/store.py +++ b/agent/autonomy/store.py @@ -29,7 +29,7 @@ def close(self) -> None: """Release the database handle deterministically.""" self._connection.close() - def __enter__(self) -> SQLiteRunStore: + def __enter__(self) -> SQLiteRunStore: # noqa: PYI034 - Python 3.10 compatibility return self def __exit__(self, *_exc: object) -> None: diff --git a/agent/autonomy/tools.py b/agent/autonomy/tools.py index beb0132..8c2dd33 100644 --- a/agent/autonomy/tools.py +++ b/agent/autonomy/tools.py @@ -3,13 +3,14 @@ from __future__ import annotations import time -from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout +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, Callable +from typing import Any from agent.autonomy.models import Observation, RiskLevel, Task - ToolHandler = Callable[[dict[str, Any]], Any] diff --git a/agent/reporter.py b/agent/reporter.py index 608a321..7067e1b 100644 --- a/agent/reporter.py +++ b/agent/reporter.py @@ -1,9 +1,11 @@ # agent/reporter.py -import os 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: @@ -14,13 +16,13 @@ def get_git_metadata(): ["git", "diff", "HEAD~1", "HEAD"] ).decode("utf-8")[:2000] # Cap to prevent context blowing up return commits, diff - except Exception: + 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.date.today().strftime("%Y-%m-%d") + 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. @@ -56,7 +58,7 @@ def generate_daily_log(): with open(log_file, "r") as f: existing_content = f.read() - header = f"# HelixAgent Autonomous Logs\n\n" if not existing_content else "" + 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" diff --git a/agent/tools/sagemaker_job.py b/agent/tools/sagemaker_job.py index 78cc9d2..bd72d9a 100644 --- a/agent/tools/sagemaker_job.py +++ b/agent/tools/sagemaker_job.py @@ -16,9 +16,8 @@ AWS_DEFAULT_REGION """ + import boto3 -import json -from typing import Dict sm_client = boto3.client("sagemaker") runtime = boto3.client("sagemaker-runtime") @@ -50,7 +49,7 @@ def start_batch_transform(job_name: str, ) return response["TransformJobArn"] -def get_batch_status(job_name: str) -> Dict: +def get_batch_status(job_name: str) -> dict: """Return status dict for batch job.""" return sm_client.describe_transform_job(TransformJobName=job_name) diff --git a/agent/tools/snowflake_query.py b/agent/tools/snowflake_query.py index d3ab6bf..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]). @@ -19,9 +18,10 @@ """ import os -import snowflake.connector from contextlib import contextmanager -from typing import List, Dict + +import snowflake.connector + @contextmanager def snowflake_connection(): @@ -38,30 +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. - - 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. - """ +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 - -# 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/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..5c421b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,12 +1,29 @@ [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.ruff.lint.per-file-ignores] +"src/main.py" = ["RUF100"] + [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/utils/logger.py b/src/utils/logger.py index 5f83056..98b2d38 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -7,9 +7,10 @@ Ensures consistent, professional logs across all modules. """ -from loguru import logger import sys +from loguru import logger + def setup_logger(log_file: str = "logs/helixagent.log", level: str = "INFO"): """ diff --git a/src/utils/tracker.py b/src/utils/tracker.py index 99e840b..e86495f 100644 --- a/src/utils/tracker.py +++ b/src/utils/tracker.py @@ -8,6 +8,7 @@ """ import mlflow + from src.utils.logger import setup_logger log = setup_logger() @@ -26,7 +27,7 @@ def __init__(self, experiment_name: str = "HelixAgent-Experiments", tracking_uri mlflow.set_experiment(experiment_name) log.info(f"Initialized MLflow tracker: {experiment_name}") - def start_run(self, run_name: str = None): + def start_run(self, run_name: str | None = None): """Start a new MLflow run""" return mlflow.start_run(run_name=run_name) @@ -35,7 +36,7 @@ def log_params(self, params: dict): mlflow.log_params(params) log.debug(f"Logged parameters: {params}") - def log_metrics(self, metrics: dict, step: int = None): + 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}") diff --git a/streamlit_app.py b/streamlit_app.py index c3f8b0c..3d76c6c 100644 --- a/streamlit_app.py +++ b/streamlit_app.py @@ -177,7 +177,7 @@ def main() -> None: 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 + except Exception as exc: # noqa: BLE001 - defensive UI boundary st.error("HelixAgent could not complete this request.") st.exception(exc) else: diff --git a/tests/test_runtime_invariants.py b/tests/test_runtime_invariants.py index 3bb72e6..68421f0 100644 --- a/tests/test_runtime_invariants.py +++ b/tests/test_runtime_invariants.py @@ -6,7 +6,8 @@ from pathlib import Path import pytest -from hypothesis import given, strategies as st +from hypothesis import given +from hypothesis import strategies as st from agent import agent_core from agent.autonomy.models import GoalSpec, RunStatus, Task