Skip to content

Feature/3.0#23

Merged
Cro22 merged 18 commits into
mainfrom
feature/3.0
May 31, 2026
Merged

Feature/3.0#23
Cro22 merged 18 commits into
mainfrom
feature/3.0

Conversation

@Cro22

@Cro22 Cro22 commented May 31, 2026

Copy link
Copy Markdown
Owner

No description provided.

Cro22 and others added 18 commits May 15, 2026 13:56
Part A of milestone 8.1: expose CloudOracle's cost data over an HTTP API
the upcoming Python agent (insights-agent/) can call as a LangGraph tool.

Why two endpoints instead of just one: the agent's reasoning improves
when it can both compare providers (cost-summary) and drill into a
single provider's service mix (cost-by-service). The /api/v1/* prefix
keeps the dashboard's existing /api/* routes untouched so the embedded
React UI is not coupled to the agent's auth model — only v1 requires
X-API-Key, v0 dashboard endpoints stay open as before.

Data semantics: there is no Cost Explorer / Billing integration yet, so
the v1 endpoints approximate period spend from the cost_snapshots table
(average projected monthly rate per (account, service), scaled by
days/30). Every response carries data_source="snapshots_approximation"
and a human-readable note so downstream clients can surface the
disclaimer to the user. Real CUR ingestion is a follow-up.

Internals:
- Added APIConfig (CLOUDORACLE_API_KEY / _API_PORT / _API_SHUTDOWN_TIMEOUT)
- New db.ListSnapshotsInRange (inclusive [start, end])
- authMiddleware (constant-time compare) + requestIDMiddleware
- apiData interface — single data dep for both v0 dashboard and v1
  handlers, replacing scattered db.* calls so tests can run without
  Postgres. Coverage on internal/api/ is now 86%.
- Graceful shutdown via Server.Run honouring SIGINT/SIGTERM with a
  configurable timeout; runServe refuses to start without an API key.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…raction

First commit of the LangGraph-based FinOps agent that consumes the Go
/api/v1 cost endpoints. Lays down the project skeleton, config / logging
mirroring the Go side (stderr + text|json switch matching slog), and an
LLMProvider ABC so future providers (Claude, OpenAI) are additive without
touching the graph code.

- pyproject.toml with uv, Python 3.12, pytest+coverage (>=80% gate),
  ruff (strict select), mypy (strict mode)
- config.Settings (pydantic-settings) fails fast on missing required keys
- logging.setup mirrors Go slog semantics (LOG_LEVEL/LOG_FORMAT, stderr)
- llm.LLMProvider ABC + llm.GeminiProvider (default gemini-2.5-flash to
  match the Go side and avoid drift)
- tests cover config validation, logging wiring, and provider construction
  (real Google SDK patched out so no network in CI); 100% coverage

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t endpoints

CloudOracleClient (httpx.AsyncClient) wraps the v1 cost endpoints with
X-API-Key auth, configurable timeout, and per-request X-Request-ID
generated with the same 24-hex shape as the Go server's newRequestID —
so a Python-side log line can be correlated with the Go-side log by
request_id without manual stitching.

build_tools() exposes the client methods as LangChain StructuredTools
with rich descriptions that tell the LLM how to surface the
`data_source: snapshots_approximation` caveat to the end user.

Errors are propagated, not swallowed:
- 4xx/5xx → CloudOracleAPIError with status / Go `code` / request_id
- timeouts and network failures → CloudOracleTransportError
- malformed JSON or non-object body → CloudOracleAPIError

Inputs validated locally before issuing the HTTP call:
- ISO YYYY-MM-DD format and start <= end
- provider in {aws, gcp, azure} (also normalizes casing)
- top in [1, 1000]

Tests use pytest-httpx to mock the Go API; cover happy paths, all error
classes (401, 4xx, 5xx, non-JSON, non-object), timeout, network error,
and every local-validation branch. Coverage on tools/cloudoracle.py: 96%.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
graph/basic.py exposes build_graph(llm, tools) and ask(graph, question)
returning an AgentResult (answer + ordered tool_calls + raw messages).
The system prompt is short on purpose — long static instructions tend to
drift from the model's actual behavior; the tool docstrings carry the
domain-specific guidance about the snapshot caveat.

Cross-cutting fix in tools/cloudoracle.py: the LangChain tool wrappers
now translate CloudOracleAPIError / CloudOracleTransportError / ValueError
into ToolException, which langgraph's ToolNode catches and surfaces back
to the LLM as a tool observation. Letting the original RuntimeError
propagate aborts the whole graph run, which is the wrong UX for a
transient 5xx or a malformed date — the model should see the error and
either retry or explain to the user.

Tests use a hand-rolled ScriptedChatModel that implements bind_tools and
returns pre-scripted AIMessages. Covers: tool selection happy path,
two-tool sequential invocation, no-tool-call (off-scope) path, tool-error
recovery, and the multimodal AIMessage.content variant Gemini returns.
51 tests, ~97% coverage.

Note: create_react_agent is deprecated in langgraph 1.0 (moved to
langchain.agents); sub-hito 8.4 will refactor to a hand-rolled supervisor
pattern. Until then we filter the warning in pyproject.toml to keep test
output clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local-only state from the Claude Code harness (session transcripts,
agent scratch space, etc.) should not end up in the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`uv run python -m insights_agent.main "<question>"` (or the
`insights-agent` console script) wires Settings → logging → GeminiProvider
→ CloudOracleClient → ReAct graph and prints either the natural-language
answer or a JSON envelope (`--json`). `--verbose` streams the tool calls
the model made to stderr so the operator can see which /api/v1 endpoint
was actually consulted.

Top-level error handling maps the realistic failure modes to distinct
exit codes (130 on Ctrl-C, 2 on missing/invalid config, 1 on runtime
errors) so callers in shell pipelines can branch deterministically.

Includes the tiny `build_graph` signature widening `list[BaseTool] →
Sequence[BaseTool]` so the CLI can pass the result of `build_tools`
without an extra conversion at the call site.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… diagram

`insights-agent/README.md` was a stub. Replace it with a self-contained
setup-in-under-10-minutes guide covering: prerequisites, `uv sync`, the
seven env vars (required vs optional, defaults), CLI flags and exit
codes, an end-to-end smoke test the operator can run by hand against a
local Go server, dev workflow (pytest / ruff / mypy), and a one-page
architecture pointer table mapping concerns to source files.

Root README gains an "AI Insights Agent" section with a Mermaid arch
diagram (User → CLI → LangGraph → Gemini → tools → Go API → Postgres)
and a roadmap update marking sub-hitos 8.0 / 8.1 done with the remaining
8.2–8.7 items listed so readers can place this work in the larger plan.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Standardize the whole repo on English for comments, docstrings, and code
example queries so a reader on the project doesn't have to bounce between
languages. The change is text-only — no identifiers, behavior, or tests
move.

Translations:
  - All `sub-hito 8.x` references in code → `milestone 8.x`
    (pyproject.toml, cost_handlers.go, llm/__init__.py, graph/basic.py,
    tools/cloudoracle.py).
  - `internal/cloud/*_test.go` test docstrings and inline comments.
  - `internal/report/pdf.go` section markers and field comments.
  - Sample queries in `insights-agent/README.md` and the matching scripted
    AIMessage / `ask(...)` query in `tests/test_graph.py` — the existing
    asserts (`"$150"`, `"snapshots"`) still match the new English answer
    so the test still passes.

Plus pre-existing working-tree formatting in `README.md` (single-line
badges, table padding in the v2 callout, an extra blockquote blank line,
and a Mermaid example query already updated to English) folded into the
same commit since it was already staged-adjacent and is the same kind
of language/cosmetic cleanup.

Verified after: `uv run pytest` (58/58, 91.83% coverage), `uv run ruff
check .`, `uv run mypy src/`, and `go test ./internal/cloud/... ./internal/api/...
./internal/report/...` all green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…endpoint

Milestone 8.2 (more tools): expose the rule-based analyzer findings as
agent-friendly savings recommendations.

Go: new authed GET /api/v1/recommendations handler that runs analyzer.Analyze
over the current inventory, with optional provider/severity filters and a top
cap. Totals (total_count, total_monthly_savings_usd, by_severity) describe the
full filtered set before the cap. Carries data_source: "heuristic_rules" to
distinguish heuristic estimates from the snapshot-derived cost endpoints.

Python: CloudOracleClient.recommendations() + cloudoracle_recommendations tool
with a rich docstring; system prompt updated to surface the heuristic_rules
caveat. Validation errors map to ToolException so the ReAct loop can recover.

Tests: 8 Go handler tests; extended Python tool tests. Both suites green
(internal/api; 65 Python tests, 92% coverage, ruff + mypy clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Milestone 8.2 (more tools): answer "is my spend growing?" with a per-day cost
time series.

Go: new authed GET /api/v1/cost-trends handler over ListTrends(days). Returns
the per-day series plus a precomputed first/latest/change summary
(absolute_usd, percent_from_first, direction up/down/flat) so the agent phrases
the trend without crunching the array. percent_from_first is null when the
first day is zero. Optional provider filter recomputes each day's total from
that day's per-service breakdown. days clamps to 1..365. Shares the
snapshots_approximation data_source with the cost endpoints.

Python: CloudOracleClient.cost_trends() + cloudoracle_cost_trends tool with a
rich docstring steering trend/over-time questions here (vs cost_summary for a
single period). Validation errors map to ToolException.

Tests: 9 Go handler tests (delta/direction, provider recompute, days clamp,
zero-first nil percent, flat, empty, auth, error); extended Python tool tests.
Both suites green (internal/api; 71 Python tests, 93% coverage, ruff + mypy
clean).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Milestone 8.2 complete (more tools): answer "what do I have?" with a resource
inventory summary.

Go: new authed GET /api/v1/inventory handler over ListResources, aggregating
counts and projected monthly cost by provider and by (provider, service).
Optional provider filter; top cap applies only to by_service so the totals
stay accurate when the list is truncated. Because resources carry AccountID,
the "functions" provider disambiguation (gcp vs azure) is exact here. Distinct
data_source: live_inventory — costs are summed per-resource projected monthly
rates from the latest scan, not billed spend.

Python: CloudOracleClient.inventory() + cloudoracle_inventory tool with a
docstring steering "what do I have?" / footprint questions here (vs cost_summary
for spend over a range). Validation errors map to ToolException.

Tests: 7 Go handler tests (aggregation, provider filter, top cap with accurate
totals, functions disambiguation, auth, empty, error); extended Python tool
tests. Both suites green (internal/api; 77 Python tests, 93% coverage, ruff +
mypy clean).

The agent now ships 5 tools across 5 authenticated v1 endpoints, closing
milestone 8.2.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ne 8.3)

Add a sixth agent tool, finops_knowledge_search, that retrieves from a curated
FinOps knowledge base for conceptual / policy / how-to questions the HTTP tools
can't answer (rightsizing, commitment discounts, data-source caveats, cost
allocation, glossary).

Architecture (RAG kept in Python, where LangChain lives; the Go server stays a
clean data API):
- knowledge/: 5 packaged markdown notes, shipped in the wheel.
- rag/corpus.py: load + chunk markdown to Documents (offline-testable).
- rag/embeddings.py: EmbeddingsProvider ABC + Gemini impl, mirroring the
  llm/ provider pattern.
- rag/store.py: langchain-postgres PGVector factory + store-agnostic retriever.
- rag/ingest.py: ingest_corpus() core + insights-agent-ingest console script.
- tools/knowledge.py: build_knowledge_tool(retriever) -> finops_knowledge_search,
  formatting results with [source: file — title] citations; errors map to
  ToolException so the ReAct loop can recover.

Wiring is optional and gated on DATABASE_URL: with it unset the agent runs with
just the five HTTP tools and no Postgres dependency. config.py gains
database_url / embeddings_model / knowledge_collection / rag_top_k; main.py adds
the knowledge tool only when a pgvector DB is configured; the system prompt
steers conceptual questions to it. docker-compose switches Postgres to
pgvector/pgvector:pg16 (drop-in).

Tested fully offline (no DB, no embeddings API): corpus chunking, and the real
retrieval + citation path via InMemoryVectorStore + DeterministicFakeEmbedding.
100 Python tests, 92% coverage, ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tone 8.4)

Replace create_react_agent on the production path with an explicit StateGraph:

    START → supervisor → {worker} → supervisor → … → synthesize → END

- supervisor routes by tool call: bound with one routing tool per specialist
  plus `finish`, the tool it calls names the next hop. Routing via tool calls
  (not with_structured_output) keeps the node driveable by the scripted fake
  model the suite already uses.
- three specialist workers, each a hand-rolled ReAct loop (_run_react, the
  actual create_react_agent replacement) over a tool subset:
  cost_analyst (cost-summary/by-service/trends/inventory),
  savings_advisor (recommendations + knowledge),
  concept_expert (knowledge). A worker contributes one summarizing message;
  its tool churn stays local so the supervisor/synthesizer see a clean
  transcript.
- synthesize composes the final answer from the findings, in the user's
  language, with data-source caveats and citations.
- a hop cap bounds the supervisor loop so a model that never emits `finish`
  still terminates.

main.py now builds the supervisor; graph/basic.py (create_react_agent) is
retained as the simple graph and still owns the shared AgentResult /
_stringify_content helpers the supervisor reuses.

Tests: test_supervisor.py drives it end-to-end with the scripted model —
single-worker route→tool→finish→synthesize, two-specialist routing, off-scope
finish-without-worker, hop cap, plus _run_react and _to_text units. 109 Python
tests, 93% coverage, ruff + mypy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…llback (8.5)

Three of milestone 8.5's production guardrails, wrapping every run through
guardrails/runner.py:run_guarded (the single entry point the CLI and the
upcoming HTTP surface share):

- Cost/usage caps: RunLimits (max_hops, max_tool_calls, max_worker_iters) from
  settings, threaded into the supervisor graph and the worker ReAct loop. When
  a cap is hit the supervisor stops dispatching and synthesizes from what it
  has, so a confused or injected loop can't run up unbounded LLM/tool cost.
  Workers now also surface their tool observations through the graph state.

- Layered answer validation (guardrails/validation.py): deterministic grounding
  first — every monetary figure in the answer must match a number in the tool
  observations, an unmatched figure is a hard fail; then an optional LLM judge
  for a second opinion when the answer makes numeric claims that pass the
  deterministic layer.

- Deterministic fallback (guardrails/fallback.py): on a run exception (quota,
  timeout) or a failed validation, return an honest no-LLM answer rendering the
  raw tool data (or stating nothing was retrieved) instead of a fabricated
  narrative or a raw traceback.

config gains MAX_HOPS / MAX_TOOL_CALLS / MAX_WORKER_ITERS /
ENABLE_ANSWER_VALIDATION / ENABLE_LLM_JUDGE; main wires run_guarded and the
--json output now includes fallback_used + the validation verdict.

Tests cover figure extraction, grounding (pass/fail/tolerance), the judge
layers, fallback rendering, and run_guarded (happy / exception / invalid /
disabled). 131 Python tests, 93% coverage, ruff + mypy clean. HTTP surface
follows next.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Expose the agent over HTTP, sharing one runtime with the CLI:

- runtime.py: GeminiAgentRunner assembles the model + client + tools + graph +
  run limits once and exposes ask() through the guardrails. The CLI (main.py)
  now uses it too, so the two entry points behave identically; the RAG
  knowledge-tool builder moved here from main.
- api/app.py: FastAPI create_app with GET /health and POST /ask
  ({query} -> {answer, tool_calls, fallback_used, validation}). The stack is
  built once in the lifespan; optional X-API-Key auth via AGENT_API_KEY (same
  convention as the Go server). create_app(runner=...) injects a fake runner so
  the surface is testable without Gemini / a live Go server / Postgres.
- api/serve.py: insights-agent-serve console script (uvicorn).
- config gains AGENT_HOST / AGENT_PORT / AGENT_API_KEY.

Tests drive the surface with FastAPI's TestClient and an injected fake runner:
health, ask happy path + metadata, empty-query 422, fallback passthrough,
auth enforced/open, and that an injected runner isn't closed by the app. 138
Python tests, 91% coverage, ruff + mypy clean.

Milestone 8.5 complete: cost caps + layered validation + deterministic fallback
+ HTTP surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…action (8.7)

Replace the hard-wired snapshot cost path in the v1 endpoints with a
billing.Source interface so a real billing integration can be swapped in by
config, starting with AWS Cost Explorer.

- internal/billing: CostRecord / Report / Source / SourceError, and
  CostExplorerSource — a GetCostAndUsage query grouped by SERVICE over the
  period (CE's exclusive end handled), summed across time buckets and pages,
  returning real unblended cost with data_source "billing_aws_cost_explorer".
  The CE client is narrowed to an injectable interface (mocked in tests), the
  same pattern internal/cloud uses for EC2/RDS.
- internal/api: snapshotSource implements billing.Source over the existing
  cost_snapshots aggregation (preserves data_source "snapshots_approximation"
  and the snapshot_query_failed code exactly). The cost-summary /
  cost-by-service handlers now group normalized records and echo the report's
  dynamic data_source; the snapshot-specific aggregateByProvider/ByService
  helpers are gone. Server gains a WithBillingSource option (default snapshots).
- config: CLOUDORACLE_BILLING_PROVIDER (snapshots | aws_cost_explorer). cmd
  builds the CE source from AWS_REGION/AWS_PROFILE when selected and falls back
  to snapshots (loudly) if init fails.

Tests: CE source (bucket/page summation, exclusive-end TimePeriod, error
wrapping, missing-metric skip) with a fake client; api handlers against an
injected non-snapshot source (dynamic data_source, provider filter, error
code). Existing snapshot cost tests pass unchanged. The agent's FinOps corpus
documents the new real-billing data source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The internal/diff golden tests (byte-exact Markdown/narrative fixtures) failed
on Windows checkouts: core.autocrlf=true with no .gitattributes rewrote the
fixtures to CRLF while the renderer emits "\n". The committed fixture content
was already correct, so this adds `* text=auto eol=lf` (plus binary markers)
to keep LF in the working tree on every platform. All Go tests now pass.

Also adds docs/v3-guide.md — the Insights Agent guide (supervisor + RAG +
guardrails architecture, the /api/v1 contract and data_source semantics, real
billing via AWS Cost Explorer, CLI/HTTP usage) alongside v1/v2-guide — and
links it from the README.
@Cro22
Cro22 merged commit 85dac82 into main May 31, 2026
2 of 3 checks passed
Cro22 added a commit that referenced this pull request Jun 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant