Skip to content

Feature/3.0 - #32

Merged
Cro22 merged 84 commits into
mainfrom
feature/3.0
Aug 25, 2026
Merged

Feature/3.0#32
Cro22 merged 84 commits into
mainfrom
feature/3.0

Conversation

@Cro22

@Cro22 Cro22 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

No description provided.

Cro22 and others added 30 commits April 11, 2026 21:21
feat: add architecture decisions and tests for LLM providers and PDF …
feat: optimize sorting of findings by savings and update README with …
feat: implement pagination and sorting for findings in the dashboard
docs: enhance README with CloudOracle's analysis-first approach and c…
- Added detailed integration tests for EC2, RDS, EBS, Lambda, NAT Gateway, and Aurora PostgreSQL cost estimation logic.
- Introduced `checkpoint135` for estimating Terraform resource change costs from a Terraform plan file.
- Added `probe-pricing` for diagnosing AWS Pricing API warnings and exploring resource attributes.
- Integrated Terraform files for configuring AWS provider dependencies and locking versions.
Cro22 and others added 28 commits May 18, 2026 19:56
`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.
Rename the "AI Insights Agent" heading to "v3 — Insights Agent (current
focus)" so it's unambiguously v3 and parallel to the v1/v2 sections, move the
"current focus" marker from v2 to v3, and drop "(in progress)" from the v3
roadmap entry now that the v3 milestones are complete.
An end-to-end run surfaced this: the cost_analyst worker produced the full
answer ("AWS spend was 8662.07 USD ...") but the final answer was just a
trailing caveat ("Your final bill may differ."). The synthesizer was fed the
workers' contributions as prior *AIMessages*, so the model treated the answer
as already given and only appended a short follow-up, dropping the numbers.

Fix: _synthesis_input collects the user question plus the specialists' findings
into a single human turn the model answers fresh, instead of replaying worker
AIMessages. Adds offline regression tests (unit on _synthesis_input, plus a
graph-level assertion that the synthesizer's input is a human turn carrying the
finding, not a replayed assistant turn).
…nknown behavior

The claudeRequest.MaxTokens field used the JSON tag "maxTokens", but the Anthropic Messages API expects snake_case "max_tokens". As written the field was silently ignored and Claude fell back to its own default instead of the configured 1024 cap.

Also align README.md and docs/architecture.md with the actual parser behavior: the iac decoders do not parse an after_unknown field. Unknown-until-apply attributes arrive as JSON null and are treated as missing; a missing required attribute routes the resource to Skipped.
Adds a billing.Source that reads real net cost (list cost plus credits)
from the standard GCP billing export in BigQuery, grouped by service, as
the GCP counterpart to the AWS Cost Explorer source. Selectable via
CLOUDORACLE_BILLING_PROVIDER=gcp_bigquery; reports data_source
billing_gcp_bigquery.

- internal/billing/bigquery.go: BigQuerySource with a narrow bigQueryAPI
  seam (returns parsed rows) so tests run offline; real client wraps
  *bigquery.Client via ADC.
- config: gcp_bigquery enum + CLOUDORACLE_GCP_BILLING_DATASET/_TABLE,
  with a cross-field check requiring project+dataset+table.
- main: billing wiring switch gains the GCP case with loud snapshot
  fallback on init failure.
- docs + agent knowledge + README roadmap updated.
Extends the pr-check cost engine to GCP, starting with
google_compute_instance. GCP has no attribute-queryable pricing API like
AWS's, so compute is priced from a curated static table embedded at build
time (us-central1 base rates + per-region multipliers + PD $/GB-month),
which is deterministic and testable offline. Estimates cap at Medium
confidence with a "static price table" caveat.

- internal/iac/gcp: extractor package mirroring internal/iac/aws.
  ExtractComputeInstance reads machine_type (short name or self-link URL),
  zone→region, scheduling (preemptible / provisioning_model=SPOT), and
  boot_disk.initialize_params size/type.
- internal/pricing/gcp_prices.{json,go}: embedded price table + lookups.
- internal/pricing/gcp_compute.go: EstimateGCPComputeInstance (compute +
  boot disk). Region multiplier and preemptible/Spot drop confidence to
  Low; Spot is priced at on-demand as a labeled upper bound.
- internal/pricing/change.go: estimateState routes google_* types through
  the GCP path (no AWS Pricing API src). Unpriced machine types become a
  Skipped change, not a hard error.

Verified end-to-end: `oracle pr-check` on a mixed GCP plan renders the
comment with correct per-resource breakdowns, region multiplier, Spot
caveat, and skips for unsupported types.
Adds standalone persistent-disk pricing to the GCP path, reusing the
embedded PD $/GB-month table. Priced at the base rate (storage region
variation is not modeled); Medium confidence with the static-table caveat.

- iac/gcp: ExtractComputeDisk (type + size) wired into Extract /
  SupportedTypes.
- pricing/gcp_disk.go: EstimateGCPComputeDisk; defaults type to
  pd-standard. Unknown type or a size the plan omits (image/snapshot-sized
  disks) becomes a Skipped change via errUnpricedGCPDisk, not a hard error.
- change.go: dispatch the ComputeDisk arm.
…(GCP v2)

Adds Cloud SQL pricing (compute + storage) from the embedded static rates.
Custom tiers (db-custom-V-M) are priced per vCPU-hour + per GB-RAM-hour;
shared-core (db-f1-micro, db-g1-small) are flat; legacy db-n1-* tiers parse
to their vCPU/RAM ratios. REGIONAL (HA) availability doubles compute and
storage. Priced at US rates (region variation not modeled), Medium
confidence with the static-table caveat.

SQL Server bundles licensing into its per-vCPU rate that we don't model, so
SQLSERVER_* versions are Skipped with a clear reason rather than mis-priced
at PostgreSQL rates. Unknown/absent tiers and unknown disk types also Skip.

- iac/gcp: ExtractSQLInstance (database_version + settings.tier/disk_size/
  disk_type/availability_type) wired into Extract / SupportedTypes.
- pricing/gcp_sql.go: EstimateGCPSQLInstance + parseSQLTier.
- gcp_prices.json: cloudsql rate block.
- change.go: dispatch the SQLInstance arm.

Verified end-to-end: db-custom-4-16384 REGIONAL + 100GB SSD renders as
$438.71 (compute $404.71, storage $34.00) with the HA caveat; a SQL Server
instance in the same plan is skipped.
- v2-guide: split Supported resources into AWS (live Pricing API) and GCP
  (embedded static table); new "How GCP is priced" subsection explaining
  the static-table decision, the drift caveat, and how --region works for
  GCP (zone-derived region overrides the flag). Also generalize the
  --region flag/input descriptions beyond AWS, and fix a stale reference
  (estimator.go → change.go).
- README: v2 blurb and roadmap note GCP pricing (compute instance, disk,
  Cloud SQL) via the static table.
feat(billing): GCP BigQuery billing-export cost source
feat(pricing): GCP v2 — price google_compute_instance in Terraform plans
The stacked PRs #29/#30/#31 merged into their intermediate bases rather
than cascading to feature/3.0, leaving M1 (google_compute_disk), M2
(google_sql_database_instance), and M3 (docs) stranded. This completes the
stack onto feature/3.0 alongside the already-landed billing (#27) and
compute (#28).
Switch the workflow examples from @v2.0.0 to @v2 so consumers track the
v2 major line and pick up minor releases (e.g. GCP pricing in v2.1.0)
without editing their workflow.
- Test badge 469→581 unit, 21→22 integration (func Test count after the
  GCP pricing + BigQuery billing tests).
- Tech stack: AWS SDK adds Pricing + Cost Explorer; GCP SDK adds BigQuery.
main and feature/3.0 carried the same v3 work under different SHAs (v3 was
merged to main via #23-#26 as fresh commits), so a plain merge conflicted on
13 files. feature/3.0 is a verified content superset of main (a -X theirs
merge of feature/3.0 into main produces a tree identical to feature/3.0), so
conflicts resolve in favor of feature/3.0. This merge records main as a parent
so PR #32 (feature/3.0 -> main) merges without conflicts; the tree is
unchanged from feature/3.0.
The e2e-test/ Terraform config was removed (72128fe) but cost-self-test.yml
was left running `terraform init` in the now-missing e2e-test/ directory, so
the cost-impact check failed before it ever built the Action.

Replace the live-Terraform + AWS-credentials flow with a committed GCP plan
fixture (e2e-test/plan.json). GCP resources price from the embedded static
table, so the self-test now needs no cloud credentials and no Terraform — it
just builds the Docker Action from the checkout (`uses: ./`) and runs
`oracle pr-check --no-llm` against the fixture, exercising the compute /
disk / Cloud SQL estimators and the skipped-unsupported-type path end-to-end
(and dogfooding the new GCP pricing on this very PR).
@github-actions

Copy link
Copy Markdown

💰 Cloud Cost Impact

Net monthly change: +$630.11 🔴

This plan adds 4 resources, with a net monthly cost increase of +$630.11.

Top movers by cost impact

Resource Action Δ Monthly Confidence
google_compute_instance.batch 🆕 create +$231.96 low
google_sql_database_instance.main 🆕 create +$219.36 medium
google_compute_instance.web 🆕 create +$158.79 medium
google_compute_disk.data 🆕 create +$20.00 medium
📋 Full breakdown (4 priced, 1 skipped)

Created (4)

  • google_compute_instance.batch — +$231.96
    • Compute: +$226.96
    • BootDisk: +$5.00
  • google_sql_database_instance.main — +$219.36
    • Compute: +$202.36
    • Storage: +$17.00
  • google_compute_instance.web — +$158.79
    • Compute: +$141.79
    • BootDisk: +$17.00
  • google_compute_disk.data — +$20.00
    • Disk: +$20.00

Skipped (1)

  • google_storage_bucket.assets (google_storage_bucket) — unsupported resource type: google_storage_bucket
⚠️ Assumptions and caveats
  • 1 resources skipped (1 unsupported types, 0 estimation failures)
  • Net cost increase this plan
  • Priced from a static GCP price table (may drift from current list price) (applies to: google_compute_instance.batch, google_compute_instance.web, google_compute_disk.data)
  • Spot/preemptible VM priced at on-demand rate (real cost is 60–91% lower) (applies to: google_compute_instance.batch)
  • Priced from a static GCP price table at US rates (may drift; other regions differ) (applies to: google_sql_database_instance.main)
  • REGIONAL (HA) availability doubles compute and storage (applies to: google_sql_database_instance.main)

Generated by CloudOracle · Confidence: low

@Cro22
Cro22 merged commit b56f220 into main Aug 25, 2026
3 checks passed
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