Files
zeekayandHanzo Dev 841ea1c0da no hogs: products and insights
716 files rewritten, 30 renamed, same family mapping as the frontend pass.

Held back, and each for a reason rather than by oversight:

  db_table values          insights_hogfunction, insights_hogflow,
                           insights_hogfunctiontemplate, hogflow_templates.
                           Renaming a table is ALTER TABLE, so it ships as a
                           migration with the raw SQL in nodejs that names it.
  hog_invocation_results   a Datastore table with Kafka engine tables on it
  products/review_hog      the module path is the Django app path; moving it
                           moves app_label and every migration is recorded under
                           (app_label, name). That is the same history break as
                           renaming a migration file, one level up.
  */migrations/*           five files, untouched
  whogho                   an integration vendor

Checked: 395 of 397 changed Python files compile. The two that do not —
insightsql/database/database.py and temporal/schedule.py — fail identically at
origin/main under the same interpreter, because they use `type X = ...` and
`except*`, which need the 3.13 this repo pins and not the 3.9 doing the check.
Running the check against the original is what told those apart from a real
break.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-09 09:16:43 -07:00

3.3 KiB

AI observability agent guide

Repo-wide conventions live in ../../AGENTS.md — kea over hooks, generated API over api.get, ph_scoped_capture in Celery, etc. Read that first.

This file is only for rules that are specific to products/ai_observability/ (and to the query runners in insights/insightsql_queries/ai/).

If you don't know the folder yet, read README.md first for the directory map. Skip it if you already know where things live — it's orientation, not a rulebook.

Update both query runners together

LLMTrace and LLMTraceEvent are built by two parallel query runners:

  • insights/insightsql_queries/ai/trace_query_runner.py (single trace read)
  • insights/insightsql_queries/ai/traces_query_runner.py (list of traces)

Both define their own _map_trace and _map_event. Neither imports from the other. Any time you add, remove, or rename a field on LLMTrace or LLMTraceEvent, or change how an event is shaped into either, change both runners in the same PR and cover the change in insights/insightsql_queries/ai/test/. The type checker will not catch drift here.

The same rule applies to routing via ai_events vs events. If one runner adopts a new code path, the other needs the matching change so single-trace and list reads don't diverge.

Product docs go under docs/

Product ADRs and engineering decision records belong in products/ai_observability/docs/, not at the product root.

Custom @action methods need explicit scope action lists

ScopeBasePermission (in insights/permissions.py) only resolves <scope_object>:read / <scope_object>:write from its built-in defaults — read_actions = ["list", "retrieve"] and write_actions = ["create", "update", "partial_update", "patch", "destroy"]. Any custom @action(detail=..., methods=[...]) falls through to None, which causes APIScopePermission.has_permission to return False with "This action does not support Personal API Key access". Every personal API key, OAuth token, and MCP call to that endpoint 403s — even though the ViewSet has scope_object set.

Two ways to fix it on a ViewSet with custom actions:

  • Per-class action lists. Add scope_object_read_actions and / or scope_object_write_actions on the ViewSet, listing the custom action method names. Best when the actions are stable and share the ViewSet's scope.

    class EvaluationConfigViewSet(TeamAndOrgViewSetMixin, viewsets.ViewSet):
        scope_object = "evaluation"
        scope_object_read_actions = ["list"]
        scope_object_write_actions = ["set_active_key"]
    
  • Per-action override. Pass required_scopes=[...] to @action(...). Best when the scope for one action diverges from the rest of the ViewSet (e.g. a write-shaped endpoint that's safe enough to expose with :read, like EvaluationViewSet.test_script).

    @action(detail=False, methods=["post"], required_scopes=["evaluation:read"])
    def test_script(self, request, **kwargs): ...
    

When you add or rename a custom action, update one of the two — if you forget, the new endpoint silently 403s every API-key caller. The default test client uses force_login and skips the scope check, so unit tests pass even when the production path is broken. Verify with a personal API key (or via the MCP layer) before merging.