Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions modules/agent/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,8 +265,8 @@ Adding a new domain (`modules/payroll/`, `modules/inventory_v2/`): drop the dire
Every turn's system prompt is assembled as:

```
_system + _date_block() + _instruction_block() + _memory_block()
baked today the firm's list this caller's facts
_system + _date_block() + _instruction_block() + _queries_block() + _memory_block()
baked today the firm's list the reads kept handy this caller's facts
```

`_system` is composed once at container start from `prompts/` and is identical for every turn; the
Expand All @@ -281,6 +281,10 @@ into `_system`, or ordering one ahead of the baked block, forecloses that for no
- `_instruction_block()` — the firm's `INSTRUCTION#` rows, one prefix Query, firm-scoped so it loads
for every caller including pokers and scheduled invokes. Empty list ⇒ no section at all: an empty
heading is prompt weight and an invitation to invent policy.
- `_queries_block()` — the product reads this firm keeps handy (modules/metrics): the
`metric_queries` rows the owner pinned (`manage_metrics op=pin`, no cap), then the names the
firm ran last (the usage table's newest rows, `GERP#recent_queries` many, default 10), as name,
description and parameters, never the SQL. Two Queries and a GetItem; empty ⇒ no section.
- `_memory_block()` — the caller's `MEMORY#<account_id>#` rows, one prefix Query, gated on the
JWT-verified caller ContextVar (no caller ⇒ nothing).

Expand Down
4 changes: 3 additions & 1 deletion modules/metrics/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ and joined to the books. Why in `README.md`.
from the OS, SecureString) and returns it once with the url; the same caller again rotates.
`unpublish_source` deletes it. `list_sources` names callers (DescribeParameters, no values).
`record` writes one event from the conversation. `query {name, params, window|start+end}` is
the read: a `metric_queries` registry row run by name (below). There is no inline SQL
the read: a `metric_queries` registry row run by name (below). `pin {name, pinned}` sets or
clears the row's `pinned` flag, which the prompt's dynamic tail reads every turn (the agent
module), with no cap on how many. There is no inline SQL
- **`record_metric`** (`metric_rules.py`) — params `{event, subject: <ctx field>, properties:
{name: <ctx field>}}`. Reads the subject and the properties off the callsite's ctx, sends with
`via: rule` and the exec id, returns `[]` so a callsite that folds returns into postings is
Expand Down
2 changes: 1 addition & 1 deletion modules/metrics/infra/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ resource "aws_iam_role_policy" "manage" {
# a query is a metric_queries row in the registry table: read by name, and a canonical one
# written on first use
Effect = "Allow"
Action = ["dynamodb:Query", "dynamodb:PutItem"]
Action = ["dynamodb:Query", "dynamodb:PutItem", "dynamodb:UpdateItem"]
Resource = "arn:aws:dynamodb:${local.region}:${local.account_id}:table/${local.schema_table}"
},
{
Expand Down
5 changes: 3 additions & 2 deletions modules/metrics/kb.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,9 @@ save it, then call it:
manage_metrics op=query name=joined_by_plan params={"event": "member.joined"} window=last_month

`params` is the order of the `?` markers; `start`, `end`, `zone` and `grain` are filled from the
window when the row declares them. There is no way to run SQL that is not a row. Say "keep this one
handy" and the row is pinned into every turn's prompt.
window when the row declares them. There is no way to run SQL that is not a row. When the owner says
to keep one handy, `manage_metrics op=pin name=<name>` puts the row into every turn's prompt;
`pinned=false` takes it out. As many as the owner wants.

The joins are yours: revenue per active member is `get_statement` for the period over `active`
for the same period; cost per check-in is the cost structure over `count`.
11 changes: 10 additions & 1 deletion modules/metrics/lambdas/manage_metrics/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
op: list_sources who may POST
op: record one event now, from the conversation
op: query a read by name: a `metric_queries` registry row, its parameters bound
op: pin keep a query in front of the agent every turn, or stop

A query is a registry row (`rows.py`): the gerp's own, or a canonical one copied in on first use.
There is no inline SQL; a query the agent writes is saved as a row (`write_schema op=extend`) and
Expand All @@ -31,7 +32,7 @@
TOKEN_PREFIX = "METRICS_TOKEN_"
CALLER_RE = re.compile(r"^[a-z0-9][a-z0-9_-]*$")

OPS = ("publish_source", "unpublish_source", "list_sources", "record", "query")
OPS = ("publish_source", "unpublish_source", "list_sources", "record", "query", "pin")


def ok(body, code=200):
Expand Down Expand Up @@ -137,6 +138,14 @@ def _usage(name: str, engine: str, result: dict) -> None:
})


def _pin(body):
"""The owner said to keep one handy, or to stop: the row's `pinned` flag, which the prompt's
dynamic tail reads every turn (modules/agent). No cap: the prompt's size is the owner's."""
if "pinned" in body and not isinstance(body["pinned"], bool):
return err("pinned: true or false")
return ok(rows.pin(body.get("name"), body.get("pinned", True)))


def _query(body):
name = body.get("name")
row = rows.read(name)
Expand Down
9 changes: 9 additions & 0 deletions modules/metrics/lambdas/manage_metrics/rows.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,15 @@ def read(name: str) -> dict:
raise NoSuchQuery(name)


def pin(name: str, pinned: bool) -> dict:
"""Set or clear `pinned` on the query's row; the prompt's tail carries a pinned row every turn.
A canonical name not yet in the table is copied first, so a pin never needs a prior call."""
row = read(name)
_table().update_item(Key={"registry": REGISTRY, "bucket_name": f"{row['engine']}#{name}"},
UpdateExpression="SET pinned = :p", ExpressionAttributeValues={":p": bool(pinned)})
return {"name": name, "engine": row["engine"], "pinned": bool(pinned)}


def _copy(engine: str, name: str, entry: dict, pinned) -> None:
"""The canonical entry as a row of the gerp's table, the shape `seed_schema` writes."""
item = {"registry": REGISTRY, "bucket_name": f"{engine}#{name}", "bucket": engine, "name": name,
Expand Down
11 changes: 8 additions & 3 deletions modules/metrics/lambdas/manage_metrics/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@
"unpublish_source",
"list_sources",
"record",
"query"
"query",
"pin"
],
"description": "publish_source | unpublish_source | list_sources: the apps that may POST. record: one event now. query: a read by name."
"description": "publish_source | unpublish_source | list_sources: the apps that may POST. record: one event now. query: a read by name. pin: keep a query in front of you every turn (pinned true), or stop (false)."
},
"caller": {
"type": "string",
Expand All @@ -38,7 +39,7 @@
},
"name": {
"type": "string",
"description": "query: the query's name \u2014 a metric_queries row of this firm's, or a canonical one (active, count, count_by, funnel_3, retention), copied in on first use. No inline SQL: save one with write_schema op=extend, then call it."
"description": "query, pin: the query's name \u2014 a metric_queries row of this firm's, or a canonical one (active, count, count_by, funnel_3, retention), copied in on first use. No inline SQL: save one with write_schema op=extend, then call it."
},
"params": {
"type": "object",
Expand All @@ -55,6 +56,10 @@
"end": {
"type": "string",
"description": "query: window end, exclusive, ISO date or datetime in the firm's zone."
},
"pinned": {
"type": "boolean",
"description": "pin: true to keep the query in every turn's prompt, false to stop. Default true."
}
}
}
14 changes: 14 additions & 0 deletions tests/metrics/local/test_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,20 @@ def test_a_canonical_row_follows_the_file_and_a_saved_row_is_left_alone():
assert body["rows"] == [{"mine": 2}], "an extension row is never touched"


def test_a_pin_sets_the_flag_the_prompt_reads_and_a_canonical_name_pins_on_first_use():
with scratch_env():
tool = load_lambda("manage_metrics")
r = tool.handler({"body": json.dumps({"op": "pin", "name": "retention"})}, None)
assert r["statusCode"] == 200 and json.loads(r["body"]) == {"name": "retention", "engine": "athena", "pinned": True}
assert query_rows()["retention"]["pinned"]["BOOL"] is True, "copied in and pinned in one call"
r = tool.handler({"body": json.dumps({"op": "pin", "name": "retention", "pinned": False})}, None)
assert json.loads(r["body"])["pinned"] is False and query_rows()["retention"]["pinned"]["BOOL"] is False
r = tool.handler({"body": json.dumps({"op": "pin", "name": "nope"})}, None)
assert r["statusCode"] == 404
r = tool.handler({"body": json.dumps({"op": "pin", "name": "retention", "pinned": "yes"})}, None)
assert r["statusCode"] == 400 and "pinned: true or false" in json.loads(r["body"])["error"]


def test_a_missing_name_fails_naming_it_and_there_is_no_inline_sql():
with scratch_env():
tool = load_lambda("manage_metrics")
Expand Down
Loading