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
12 changes: 7 additions & 5 deletions .claude/skills/check/rules.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
Errors (exit 1):
- a top-level import that isn't the standard library, boto3 or botocore (optional packages load lazily)
- an import of another analyzer (shared code goes in _kit/), except a companion importing the analyzer it
builds on (COMPANIONS: s3_explorer.py on s3.py); an import of a module a few analyzers share by any other
analyzer (SHARED: _bedrock.py, for bedrock_kb.py and bedrock_chat.py); a shared module importing an analyzer;
builds on (COMPANIONS: s3_explorer.py on s3.py, agent.py on lambda_functions.py); an import of a module a few
analyzers share by any other analyzer (SHARED: _bedrock.py, for bedrock_kb.py, bedrock_chat.py and agent.py);
a shared module importing an analyzer;
and _kit/ importing any of them
- an analyzer defining a name that _kit/ has, or the shared module it imports (a copy of a shared helper: import
it from there instead); and a shared module defining a name that _kit/ has
Expand Down Expand Up @@ -41,12 +42,13 @@
KIT = ANALYZERS / "_kit" # the code the analyzers share: checked like them, but it has no sections or View
ALLOWED_TOP_LEVEL = {"boto3", "botocore"}
# A companion builds on one analyzer: it imports that analyzer (lazily, so it still imports alone), its UI class
# is a <Service>View or <Service>Explorer, and its AWS calls are checked against that analyzer's services.
COMPANIONS = {"s3_explorer": "s3"}
# is a <Service>View or <Service>Explorer, and its AWS calls are checked against that analyzer's services. The agent
# builds on lambda_functions: its tools read through LambdaAnalyzer.
COMPANIONS = {"s3_explorer": "s3", "agent": "lambda_functions"}
# Code a few analyzers share that the others must not load: a module of the package beside them -> the analyzers
# that import it. It's checked like _kit (no sections or View), imports _kit and never an analyzer, and the
# analyzers that import it don't define its names again.
SHARED = {"_bedrock": {"bedrock_kb", "bedrock_chat"}}
SHARED = {"_bedrock": {"bedrock_kb", "bedrock_chat", "agent"}} # the agent shows its answers with _bedrock's _Answer
NEWER_STDLIB = {"compression", "annotationlib", "tomllib", "zoneinfo", "graphlib"} # stdlib on newer Pythons
READ_PREFIXES = ("Get", "List", "Describe", "Head", "Scan", "Query", "Select", "BatchGet", "Lookup", "Search",
"Retrieve")
Expand Down
2 changes: 2 additions & 0 deletions .claude/skills/demo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ service is given, run a short tour:
ui.search("how do I get my money back?", index="vectors-prod/support-docs")`
- lambda_functions: `ui.functions(regions="all"); ui.function_info("orders-etl"); ui.errors("orders-etl");
ui.performance("orders-etl")` (the explorer window needs a browser: see `lambda_explorer_shots.py`)
- agent: `ui.ask("Which Lambda failed in the last 24 hours?"); ui.tools()` (a scripted stand-in for the model, on the
Lambda data: see `--help`)
- other services: `ui.help()` and then the service's overview command

`--help` describes the demo data. Most useful:
Expand Down
78 changes: 77 additions & 1 deletion .claude/skills/demo/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,9 @@
hours with WARN lines and unhandled TypeErrors), feature-backfill (python3.10, never called, no triggers)
and support-agent-actions (a Bedrock agent's action group). eu-west-1: gdpr-export (SQS). The run
c0ffee00-1d2e-4f3a-9b8c-7d6e5f4a3b2c is one of orders-etl's KeyErrors.
agent the lambda_functions data, and a scripted stand-in for the model (the agent's packages must be installed):
whatever the question, it calls function_health for the last 24 hours and function_errors for the two
functions that failed most, and answers from what they returned. --live asks your Azure OpenAI deployment.
"""

from __future__ import annotations
Expand Down Expand Up @@ -1906,6 +1909,79 @@ def describe_log_groups(**params):
return {"clients": {"lambda": lambda_client, "logs": logs_client}}


# ----------------------------------------------------------------------------- the agent


def seed_agent() -> dict:
"""The Lambda demo data, and a scripted stand-in for the model (no Azure OpenAI needed, but the agent's packages
are): it calls function_health for the last 24 hours, then function_errors for the two functions that failed most,
and writes its answer from what they returned, so the report shows the demo data's real numbers. It only knows
that one plan; with --live the agent asks your Azure OpenAI deployment instead (AZURE_OPENAI_* variables)."""
seeded = seed_lambda_functions()
return {"lambda_clients": seeded["clients"], "chat_model": _demo_model()}


def _demo_model():
try:
from langchain_core.language_models.chat_models import BaseChatModel
from langchain_core.messages import AIMessage
from langchain_core.outputs import ChatGeneration, ChatResult
except ImportError:
print("(the agent demo needs the agent's packages: pip install 'aws-analyzer[agent]', Python 3.11+)",
file=sys.stderr)
return None

def answer(health: dict, reports: list[dict]) -> str:
failed = health.get("failed") or []
if not failed:
return (f"No function had failed calls in the last 24 hours ({health.get('functions_checked', 0)} "
f"checked in {', '.join(health.get('regions') or [])}).")
causes = {r.get("function"): (r.get("causes") or [{}])[0] for r in reports}
rows = ["| Function | Failed calls | Error rate | Most common cause |", "|---|---|---|---|"]
for row in failed:
cause = causes.get(row["name"]) or {}
what = f"{cause['kind']} ×{cause['count']}" if cause.get("kind") else "not read"
rows.append(f"| {row['name']} | {row['errors']:,} | {row['error_rate_pct']}% | {what} |")
names = " and ".join(f"**{r['name']}**" for r in failed[:2])
window = health.get("window") or {}
return (f"{names} had failed calls in the last 24 hours.\n\n" + "\n".join(rows)
+ f"\n\nChecked {window.get('from', '?')} to {window.get('to', '?')} UTC in "
+ ", ".join(health.get("regions") or []) + ".")

class DemoModel(BaseChatModel):
model_name: str = "demo-model (scripted)"

@property
def _llm_type(self) -> str:
return "demo"

def bind_tools(self, tools, **kwargs):
return self

def _generate(self, messages, stop=None, run_manager=None, **kwargs):
start = max(i for i, m in enumerate(messages) if m.type == "human")
results = [json.loads(m.content) for m in messages[start + 1:] if m.type == "tool"]
usage = {"input_tokens": 2400 + 1500 * len(results), "output_tokens": 90, "total_tokens": 0}
usage["total_tokens"] = usage["input_tokens"] + usage["output_tokens"]

def calls(*steps):
return AIMessage(content="", usage_metadata=usage, tool_calls=[
{"name": name, "args": args, "id": f"{name}-{i}", "type": "tool_call"}
for i, (name, args) in enumerate(steps)])

if not results:
reply = calls(("function_health", {"since": "24h"}))
elif len(results) == 1 and results[0].get("failed"):
reply = calls(*[("function_errors", {"function": row["name"], "since": "24h",
**({"region": row["region"]} if row["region"] != REGION else {})})
for row in results[0]["failed"][:2]])
else:
reply = AIMessage(content=answer(results[0], results[1:]), usage_metadata=usage)
return ChatResult(generations=[ChatGeneration(message=reply)])

return DemoModel()


class _Patched:
"""A moto client with a few operations answered by functions instead (each checked against the service model)."""

Expand Down Expand Up @@ -1939,7 +2015,7 @@ def __getattr__(self, name: str):

SEEDERS = {"s3": seed_s3, "dynamodb": seed_dynamodb, "bedrock_kb": seed_bedrock_kb, "bedrock_chat": seed_bedrock_kb,
"sagemaker_env": seed_sagemaker_env, "opensearch": seed_opensearch,
"lambda_functions": seed_lambda_functions}
"lambda_functions": seed_lambda_functions, "agent": seed_agent}


# ----------------------------------------------------------------------------- run
Expand Down
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ changes what one shows, and a patch (0.1.1) only fixes things. A change that cou

### Added

- **Ask about your account in plain English**: `AgentView().ask("Which Lambda failed last night?")` answers from the
package's own read-only tools, run by an agent (LangChain's deepagents) on your Azure OpenAI deployment. It finds
which functions failed in your night (in `set_timezone()`'s zone, or `AgentView(timezone=...)`'s), why, from their
logs, and the run that shows it,
and every answer says the window and regions it checked, what it couldn't read, the AWS reads, tokens and cost, and
the `LambdaView` commands to look closer. Follow-up questions continue the conversation; `transcript()` and
`tools()` show the questions so far and what it can call. Nothing can change your account: a guard refuses any AWS
call that isn't a read, and each question has a budget (tool calls, time, cost, a 3-day log window) that it stops
at and says so. Version 1 covers Lambda. Install it with `pip install "aws-analyzer[agent]"` (Python 3.11 or newer;
everything else still runs on 3.10 with boto3 alone) ([#87](https://github.com/utkarsh5026/aws-analyzer/pull/87)).

- `set_timezone("Asia/Kolkata")` (from `aws_analyzer`) shows **times in your own time zone** instead of UTC, in every
report and window of every service, and their columns and sentences say which zone (`Last modified (IST)`,
`since 2024-05-01 14:02 IST`). It takes a tz database name, its city alone (`"kolkata"`, `"new york"`), `"local"`
Expand Down
Loading
Loading