downshift 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- downshift-0.1.0/.bob/custom_modes.yaml +46 -0
- downshift-0.1.0/.bob/rules-agent/AGENTS.md +13 -0
- downshift-0.1.0/.bob/rules-ask/AGENTS.md +12 -0
- downshift-0.1.0/.bob/rules-plan/AGENTS.md +13 -0
- downshift-0.1.0/.bob/skills/downshift-audit/SKILL.md +47 -0
- downshift-0.1.0/.bob/skills/downshift-audit/schema-reference.md +82 -0
- downshift-0.1.0/.bob/skills/downshift-evals/SKILL.md +73 -0
- downshift-0.1.0/.bob/skills/downshift-evals/format-reference.md +55 -0
- downshift-0.1.0/.bobignore +17 -0
- downshift-0.1.0/.github/workflows/ci.yml +53 -0
- downshift-0.1.0/.github/workflows/cost-diff.yml +20 -0
- downshift-0.1.0/.github/workflows/publish.yml +36 -0
- downshift-0.1.0/.gitignore +14 -0
- downshift-0.1.0/.pre-commit-config.yaml +7 -0
- downshift-0.1.0/AGENTS.md +71 -0
- downshift-0.1.0/CHANGELOG.md +44 -0
- downshift-0.1.0/CONTRIBUTING.md +20 -0
- downshift-0.1.0/LICENSE +21 -0
- downshift-0.1.0/PKG-INFO +218 -0
- downshift-0.1.0/README.md +175 -0
- downshift-0.1.0/action.yml +117 -0
- downshift-0.1.0/bob_sessions/.gitkeep +0 -0
- downshift-0.1.0/bob_sessions/downshift_task01_init.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task02_architecture.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task03_audit_a.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task03_audit_b.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task04_evals_a.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task04_evals_b.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_a01.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_a02.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_a03.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_b01.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_b02.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_b03.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_b04.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task05_report_b05.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task06_apply_a.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task06_apply_b.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task06_apply_c.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task07_apply3b.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task08_review.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task09_web01.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task09_web02.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task09_web03.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task10_case_study_a.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task10_case_study_b.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task10_case_study_c.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task10_case_study_d.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task11_mem0_a.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task11_mem0_b.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task11_mem0_c.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task11_mem0_d.png +0 -0
- downshift-0.1.0/bob_sessions/downshift_task11_mem0_e.png +0 -0
- downshift-0.1.0/docs/architecture.md +125 -0
- downshift-0.1.0/docs/bob.md +41 -0
- downshift-0.1.0/docs/case-study/mem0/downshift.audit.json +2023 -0
- downshift-0.1.0/docs/case-study/mem0/downshift.scan.json +1989 -0
- downshift-0.1.0/docs/case-study/mem0/downshift.yaml +33 -0
- downshift-0.1.0/docs/case-study/mem0/estimate.md +20 -0
- downshift-0.1.0/docs/case-study/orchestrai/downshift.audit.json +245 -0
- downshift-0.1.0/docs/case-study/orchestrai/downshift.scan.json +54 -0
- downshift-0.1.0/docs/case-study/orchestrai/downshift.yaml +24 -0
- downshift-0.1.0/docs/case-study/orchestrai/estimate.md +20 -0
- downshift-0.1.0/docs/case-study-mem0.md +130 -0
- downshift-0.1.0/docs/case-study.md +112 -0
- downshift-0.1.0/docs/images/bad_pr_cost_diff_a.png +0 -0
- downshift-0.1.0/docs/images/bad_pr_cost_diff_b.png +0 -0
- downshift-0.1.0/docs/plans/report-plan.md +228 -0
- downshift-0.1.0/docs/specs/report.md +171 -0
- downshift-0.1.0/downshift.example.yaml +33 -0
- downshift-0.1.0/examples/supportdesk/README.md +70 -0
- downshift-0.1.0/examples/supportdesk/data/refund_policy.md +19 -0
- downshift-0.1.0/examples/supportdesk/data/tickets.jsonl +40 -0
- downshift-0.1.0/examples/supportdesk/downshift.audit.json +345 -0
- downshift-0.1.0/examples/supportdesk/downshift.report.md +134 -0
- downshift-0.1.0/examples/supportdesk/downshift.scan.after.json +294 -0
- downshift-0.1.0/examples/supportdesk/downshift.scan.json +290 -0
- downshift-0.1.0/examples/supportdesk/downshift.yaml +26 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.agent_assist__draft_reply.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.agent_assist__summarize_for_agent.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.extract__extract_order_info.jsonl +21 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.misc_utils__lang_of.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.policy__decide_refund.jsonl +26 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.triage__classify_category.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.triage__detect_sentiment.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/evals/supportdesk.triage__tag_urgency.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/models.yaml +14 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-0.5b.jsonl +44 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-1.5b.jsonl +44 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-3b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__draft_reply/qwen2.5-7b.jsonl +44 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-0.5b.jsonl +44 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-1.5b.jsonl +44 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-3b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.agent_assist__summarize_for_agent/qwen2.5-7b.jsonl +44 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-0.5b.jsonl +21 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-1.5b.jsonl +21 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-3b.jsonl +21 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.extract__extract_order_info/qwen2.5-7b.jsonl +21 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-0.5b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-1.5b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-3b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.misc_utils__lang_of/qwen2.5-7b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-0.5b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-1.5b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-3b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.policy__decide_refund/qwen2.5-7b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-0.5b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-1.5b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-3b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__classify_category/qwen2.5-7b.jsonl +25 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-0.5b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-1.5b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-3b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__detect_sentiment/qwen2.5-7b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-0.5b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-1.5b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-3b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/results/supportdesk.triage__tag_urgency/qwen2.5-7b.jsonl +22 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/__init__.py +1 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/__main__.py +40 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/agent_assist.py +56 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/extract.py +37 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/llm.py +35 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/misc_utils.py +28 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/models.py +38 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/pipeline.py +63 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/policy.py +65 -0
- downshift-0.1.0/examples/supportdesk/supportdesk/triage.py +59 -0
- downshift-0.1.0/pyproject.toml +90 -0
- downshift-0.1.0/src/downshift/__init__.py +3 -0
- downshift-0.1.0/src/downshift/audit.py +188 -0
- downshift-0.1.0/src/downshift/cli.py +930 -0
- downshift-0.1.0/src/downshift/config.py +348 -0
- downshift-0.1.0/src/downshift/cost.py +212 -0
- downshift-0.1.0/src/downshift/decide.py +220 -0
- downshift-0.1.0/src/downshift/diff.py +387 -0
- downshift-0.1.0/src/downshift/evalgen.py +220 -0
- downshift-0.1.0/src/downshift/evals.py +395 -0
- downshift-0.1.0/src/downshift/export.py +412 -0
- downshift-0.1.0/src/downshift/gitref.py +65 -0
- downshift-0.1.0/src/downshift/llm.py +171 -0
- downshift-0.1.0/src/downshift/py.typed +0 -0
- downshift-0.1.0/src/downshift/report.py +380 -0
- downshift-0.1.0/src/downshift/resolve.py +439 -0
- downshift-0.1.0/src/downshift/runner.py +312 -0
- downshift-0.1.0/src/downshift/scanner.py +334 -0
- downshift-0.1.0/src/downshift/schema.py +356 -0
- downshift-0.1.0/src/downshift/scorer.py +219 -0
- downshift-0.1.0/tests/cli/test_audit_commands.py +152 -0
- downshift-0.1.0/tests/cli/test_check_evals.py +91 -0
- downshift-0.1.0/tests/cli/test_cli_help.py +20 -0
- downshift-0.1.0/tests/cli/test_cli_scan.py +115 -0
- downshift-0.1.0/tests/cli/test_diff_cli.py +152 -0
- downshift-0.1.0/tests/cli/test_e2e_pipeline.py +79 -0
- downshift-0.1.0/tests/cli/test_estimate_cli.py +84 -0
- downshift-0.1.0/tests/cli/test_evalgen_cli.py +127 -0
- downshift-0.1.0/tests/cli/test_export_cli.py +91 -0
- downshift-0.1.0/tests/cli/test_report_cli.py +115 -0
- downshift-0.1.0/tests/cli/test_rescore_cli.py +169 -0
- downshift-0.1.0/tests/cli/test_run_cli.py +142 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/README.md +8 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/__init__.py +1 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/__main__.py +40 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/agent_assist.py +58 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/extract.py +38 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/llm.py +39 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/misc_utils.py +26 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/pipeline.py +63 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/policy.py +66 -0
- downshift-0.1.0/tests/fixtures/supportdesk_v0/supportdesk/triage.py +55 -0
- downshift-0.1.0/tests/integration/test_evalgen_ollama.py +26 -0
- downshift-0.1.0/tests/integration/test_ollama.py +17 -0
- downshift-0.1.0/tests/integration/test_run_ollama.py +29 -0
- downshift-0.1.0/tests/snapshots/supportdesk_report.md +134 -0
- downshift-0.1.0/tests/unit/test_audit.py +252 -0
- downshift-0.1.0/tests/unit/test_config.py +221 -0
- downshift-0.1.0/tests/unit/test_cost.py +237 -0
- downshift-0.1.0/tests/unit/test_decide.py +358 -0
- downshift-0.1.0/tests/unit/test_diff.py +222 -0
- downshift-0.1.0/tests/unit/test_evalgen.py +207 -0
- downshift-0.1.0/tests/unit/test_evals.py +448 -0
- downshift-0.1.0/tests/unit/test_export.py +212 -0
- downshift-0.1.0/tests/unit/test_llm.py +158 -0
- downshift-0.1.0/tests/unit/test_report.py +568 -0
- downshift-0.1.0/tests/unit/test_report_snapshot.py +41 -0
- downshift-0.1.0/tests/unit/test_rescore.py +185 -0
- downshift-0.1.0/tests/unit/test_resolve.py +397 -0
- downshift-0.1.0/tests/unit/test_runner.py +311 -0
- downshift-0.1.0/tests/unit/test_scanner.py +265 -0
- downshift-0.1.0/tests/unit/test_scanner_sdk_detection.py +104 -0
- downshift-0.1.0/tests/unit/test_schema.py +221 -0
- downshift-0.1.0/tests/unit/test_scorer.py +298 -0
- downshift-0.1.0/web/.gitignore +5 -0
- downshift-0.1.0/web/SPEC.md +796 -0
- downshift-0.1.0/web/app/audit/page.tsx +117 -0
- downshift-0.1.0/web/app/callsites/[slug]/CallSiteDetail.tsx +229 -0
- downshift-0.1.0/web/app/callsites/[slug]/page.tsx +163 -0
- downshift-0.1.0/web/app/callsites/page.tsx +82 -0
- downshift-0.1.0/web/app/case-study/page.tsx +158 -0
- downshift-0.1.0/web/app/docs/page.tsx +131 -0
- downshift-0.1.0/web/app/globals.css +87 -0
- downshift-0.1.0/web/app/how-it-works/page.tsx +94 -0
- downshift-0.1.0/web/app/layout.tsx +37 -0
- downshift-0.1.0/web/app/not-found.tsx +14 -0
- downshift-0.1.0/web/app/page.tsx +226 -0
- downshift-0.1.0/web/components/Badge.tsx +47 -0
- downshift-0.1.0/web/components/Button.tsx +31 -0
- downshift-0.1.0/web/components/CodeBlock.tsx +17 -0
- downshift-0.1.0/web/components/Disclaimer.tsx +10 -0
- downshift-0.1.0/web/components/Footer.tsx +33 -0
- downshift-0.1.0/web/components/Nav.tsx +68 -0
- downshift-0.1.0/web/components/Panel.tsx +41 -0
- downshift-0.1.0/web/components/StatCard.tsx +45 -0
- downshift-0.1.0/web/components/Table.tsx +32 -0
- downshift-0.1.0/web/components/ThemeToggle.tsx +50 -0
- downshift-0.1.0/web/eslint.config.mjs +13 -0
- downshift-0.1.0/web/lib/__tests__/content.test.ts +37 -0
- downshift-0.1.0/web/lib/__tests__/data.test.ts +51 -0
- downshift-0.1.0/web/lib/__tests__/format.test.ts +86 -0
- downshift-0.1.0/web/lib/__tests__/select.test.ts +142 -0
- downshift-0.1.0/web/lib/content.ts +315 -0
- downshift-0.1.0/web/lib/data.ts +28 -0
- downshift-0.1.0/web/lib/format.ts +62 -0
- downshift-0.1.0/web/lib/select.ts +53 -0
- downshift-0.1.0/web/lib/types.ts +140 -0
- downshift-0.1.0/web/next-env.d.ts +6 -0
- downshift-0.1.0/web/next.config.ts +11 -0
- downshift-0.1.0/web/package-lock.json +7213 -0
- downshift-0.1.0/web/package.json +33 -0
- downshift-0.1.0/web/postcss.config.mjs +2 -0
- downshift-0.1.0/web/public/data/audit.json +205 -0
- downshift-0.1.0/web/public/data/callsites.json +1067 -0
- downshift-0.1.0/web/public/data/evals_summary.json +2824 -0
- downshift-0.1.0/web/public/data/report.md +134 -0
- downshift-0.1.0/web/public/data/summary.json +75 -0
- downshift-0.1.0/web/tsconfig.json +21 -0
- downshift-0.1.0/web/vitest.config.ts +12 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
customModes:
|
|
2
|
+
- slug: downshift-auditor
|
|
3
|
+
name: Downshift Auditor
|
|
4
|
+
description: Audits LLM call sites from downshift scan and writes downshift.audit.json.
|
|
5
|
+
roleDefinition: >-
|
|
6
|
+
You are the Downshift Auditor. You read the output of `downshift scan` and close the
|
|
7
|
+
gaps static analysis cannot: models and prompts only known at runtime, and shared LLM
|
|
8
|
+
helpers that serve several features. You also describe each call site precisely enough
|
|
9
|
+
that evals can be written for it. You are careful, you never guess, and you read only
|
|
10
|
+
the files you need.
|
|
11
|
+
whenToUse: >-
|
|
12
|
+
Use after running `downshift scan`, to resolve unresolved call sites, split shared LLM
|
|
13
|
+
helpers into one call site per feature, and add purpose, output contract, difficulty
|
|
14
|
+
and grading.
|
|
15
|
+
customInstructions: >-
|
|
16
|
+
Load the downshift-audit skill first and follow it exactly. Never edit source code.
|
|
17
|
+
The only file you may write is downshift.audit.json. Do not run commands.
|
|
18
|
+
groups:
|
|
19
|
+
- read
|
|
20
|
+
- - edit
|
|
21
|
+
- fileRegex: "(^|/)downshift\\.audit\\.json$"
|
|
22
|
+
description: Only the audit output file
|
|
23
|
+
- skill
|
|
24
|
+
- todo
|
|
25
|
+
- slug: downshift-eval-writer
|
|
26
|
+
name: Downshift Eval Writer
|
|
27
|
+
description: Writes eval sets (JSONL test cases) for audited LLM call sites.
|
|
28
|
+
roleDefinition: >-
|
|
29
|
+
You are the Downshift Eval Writer. For one audited LLM call site you write 20 to 25
|
|
30
|
+
test cases whose expected answers are provably correct from the prompt, the output
|
|
31
|
+
contract and the refund policy. You prefer fewer, unambiguous cases over clever ones,
|
|
32
|
+
and you never guess a label.
|
|
33
|
+
whenToUse: >-
|
|
34
|
+
Use after downshift.audit.json exists, to write one eval file per call site in the
|
|
35
|
+
evals folder next to the audit.
|
|
36
|
+
customInstructions: >-
|
|
37
|
+
Load the downshift-evals skill first and follow it exactly. Never edit source code or
|
|
38
|
+
the audit. The only files you may write are .jsonl files in an evals folder. Do not run
|
|
39
|
+
commands.
|
|
40
|
+
groups:
|
|
41
|
+
- read
|
|
42
|
+
- - edit
|
|
43
|
+
- fileRegex: "(^|/)evals/[^/]+\\.jsonl$"
|
|
44
|
+
description: Only eval set files
|
|
45
|
+
- skill
|
|
46
|
+
- todo
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# AGENTS.md: Agent (coding) mode
|
|
2
|
+
|
|
3
|
+
## Non-obvious coding rules
|
|
4
|
+
|
|
5
|
+
- **Scanner unit tests use `scan_source(textwrap.dedent(src), "app.py")`**, not `scan_module` or temp files.
|
|
6
|
+
- **Extend the `_MISSING` sentinel pattern** when adding optional fields to `_field()` in `schema.py`. `None` is a valid value for nullable fields, so it cannot mean "not provided".
|
|
7
|
+
- **Call `_reject_unknown()` at the start of every `from_dict`** in `schema.py`; omitting it silently accepts unknown fields.
|
|
8
|
+
- **`frozenset`, not `Enum`**, for valid-value sets. Add new values to the existing constants (`MODEL_SOURCES`, `PRODUCERS`, ...).
|
|
9
|
+
- **Bob-produced call sites** set `found_by="bob"` on each `CallSite` and `generated_by="bob"` on the `ScanResult`. Every file Bob writes must load cleanly with `ScanResult.load`.
|
|
10
|
+
- **`API_PATTERNS`** in `scanner.py` controls which call chains are detected. Add an LLM API by appending a `(suffix_tuple, api_name, needs_model_kwarg)` entry.
|
|
11
|
+
- **Call site IDs are stable keys** used in `downshift.yaml` `volume.per_call_site`; changing the ID format is a breaking change.
|
|
12
|
+
- **Config dataclasses are `frozen=True`**: construct a new instance instead of mutating.
|
|
13
|
+
- **Run checks before finishing**: `ruff format . && ruff check . && mypy src && python -m pytest`. Never bare `pytest`.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# AGENTS.md: Ask mode
|
|
2
|
+
|
|
3
|
+
## Non-obvious documentation context
|
|
4
|
+
|
|
5
|
+
- **`schema.py` is the shared contract** between the AST scanner and the Bob auditor. Both produce files in the same format.
|
|
6
|
+
- **Implemented vs stub commands**: check `cli.py`. Stubs call `_not_implemented()` and exit with code 1.
|
|
7
|
+
- **`examples/supportdesk/`** is the demo target and integration fixture, referenced in tests as `SUPPORTDESK`. Its data is synthetic.
|
|
8
|
+
- **Integration tests need local Ollama** and are excluded from the default run; use `python -m pytest -m integration`.
|
|
9
|
+
- **`downshift.example.yaml`** is the config template users copy to `downshift.yaml`.
|
|
10
|
+
- **`.bob/` ships with the product**: `custom_modes.yaml` defines the Downshift Auditor mode and `skills/downshift-audit/` holds its workflow. Install guide: `docs/bob.md`.
|
|
11
|
+
- **`bob_sessions/`** holds screenshots of Bob tasks (hackathon evidence), not code.
|
|
12
|
+
- **Architecture overview**: `docs/architecture.md`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# AGENTS.md: Plan mode
|
|
2
|
+
|
|
3
|
+
## Non-obvious architectural constraints
|
|
4
|
+
|
|
5
|
+
- **`schema.py` is forward-only**: `schema_version` is checked on load and an unsupported version raises. No migration path.
|
|
6
|
+
- **`Resolver` is stateful within one `_scan_modules` call** (it holds a `ModuleIndex`); do not share one across `scan_path` invocations.
|
|
7
|
+
- **Cross-module resolution is one-pass**: `ModuleIndex` is built once before resolution. Circular imports are not followed.
|
|
8
|
+
- **Caller attribution is name-based**: `_attach_callers` matches function names as strings.
|
|
9
|
+
- **`ScanResult.write()` overwrites silently**: no merging, no locking.
|
|
10
|
+
- **`via`** is not set by the AST scanner. The Bob auditor sets it when it splits a shared helper (e.g. `supportdesk/llm.py::ask`) into one logical call site per feature; `via` holds the helper's call site id.
|
|
11
|
+
- **Bob auditor output is a separate file**, e.g. `examples/supportdesk/downshift.audit.json`. It never overwrites the scan file; `downshift compare` reads both.
|
|
12
|
+
- **`.bob/custom_modes.yaml` and `.bob/skills/` are shipped product files** (the Downshift Auditor mode and its skill), not scratch space.
|
|
13
|
+
- **Plans stay small**: one job per task, touch only the files named in the request.
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: downshift-audit
|
|
3
|
+
description: Audit LLM call sites from a downshift scan file. Resolve models and prompts the static scanner missed, split shared LLM helpers into one call site per feature, add purpose, output_contract, difficulty and grading, and write downshift.audit.json.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Downshift audit
|
|
7
|
+
|
|
8
|
+
Input: a scan file written by `downshift scan`, given as an @mention (for example `examples/supportdesk/downshift.scan.json`).
|
|
9
|
+
Output: `downshift.audit.json` in the same folder as the scan file.
|
|
10
|
+
Field reference: `schema-reference.md` in this skill folder. Read it before writing.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. Read the scan file and `schema-reference.md`. Do not re-scan the repo.
|
|
15
|
+
2. Build a worklist. A call site needs inspection if any of these is true:
|
|
16
|
+
- `model.value` is null, or `model.source` is `kwargs`, `env`, `dynamic` or `missing`
|
|
17
|
+
- `messages` is null, or any message has `resolved: false`
|
|
18
|
+
- it has 2 or more `callers` and its prompt is built by the callers
|
|
19
|
+
3. For each worklist call site, read only its own file and the files of its `callers`. Read nothing else.
|
|
20
|
+
4. Resolve models. Set `model.value` to the real model string, `model.source` to `manual`, keep `expression` and `env_var`, set `defined_in` to the file where the model string lives, set `found_by` to `bob`, and add a note starting with `Bob:` that says how you resolved it.
|
|
21
|
+
5. Resolve prompts. Set `messages` to the full list as it is sent to the model. Write runtime values as `{placeholder}` with a plain name: `{ticket['body']}` becomes `{body}`, an expression like `{json.dumps(order_info)}` becomes `{order_info_json}`, and a module constant is written out as its value. Set `resolved: true` on each message. Add a `Bob:` note.
|
|
22
|
+
6. Split shared helpers. If one helper serves several features with different prompts, replace the helper entry with one entry per calling feature:
|
|
23
|
+
- `id`: the caller's id in the form `path/to/file.py::qualname`
|
|
24
|
+
- `file`, `line`, `end_line`, `function`: the caller's call to the helper
|
|
25
|
+
- `via`: the helper's id from the scan file
|
|
26
|
+
- `api`, `model`, `is_async`, `temperature`, `max_tokens`: from the helper call, as this caller uses it
|
|
27
|
+
- `messages`: the prompt this caller sends
|
|
28
|
+
- `callers`: empty list. `found_by`: `bob`. Add a `Bob:` note naming the helper.
|
|
29
|
+
The helper does not stay in the output as its own entry.
|
|
30
|
+
7. Enrich every call site, including ones that were already resolved: `purpose`, `output_contract`, `difficulty`, `grading` (rules below).
|
|
31
|
+
8. Write `downshift.audit.json`. Copy every top-level key and value from the scan file unchanged, except set `generated_by` to `bob` and replace `call_sites` with your list. Include every CallSite field on every entry. Sort call sites by `id`.
|
|
32
|
+
9. Finish with a short table: id, model, what changed (resolved model, resolved prompt, split from helper, metadata only). Do not run commands. The user validates with `downshift validate`.
|
|
33
|
+
|
|
34
|
+
## Enrichment rules
|
|
35
|
+
|
|
36
|
+
- `purpose`: one sentence naming the product feature. Example: "Classify a support ticket into one of five categories."
|
|
37
|
+
- `output_contract`: concrete, checkable rules taken from the prompt and from the code that parses the output. Exact JSON keys and allowed values, allowed labels, length limits, forbidden content. Evals will test exactly these rules.
|
|
38
|
+
- `difficulty`: `easy` for a single label or short extraction; `medium` for structured JSON or tightly constrained short text; `hard` for open-ended writing or policy reasoning.
|
|
39
|
+
- `grading`: `exact` for a single label or short fixed answer; `json_fields` when `output_format` is `json`; `judge` for free text.
|
|
40
|
+
|
|
41
|
+
## Hard rules
|
|
42
|
+
|
|
43
|
+
- Never edit source code or any file other than `downshift.audit.json`.
|
|
44
|
+
- Never invent. If something cannot be determined from the code, leave it null, keep the original `source`, and add a `Bob:` note saying what is missing.
|
|
45
|
+
- Use only the fields in `schema-reference.md`. Unknown fields fail validation.
|
|
46
|
+
- Keep the ids of call sites you do not split exactly as they are. They are keys in `downshift.yaml`.
|
|
47
|
+
- Call sites that were already fully resolved keep `found_by: "ast"`. Only add the enrichment fields.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# downshift.audit.json field reference
|
|
2
|
+
|
|
3
|
+
Same format as the scan file. Top-level keys: copy them from the scan file; set `generated_by` to `bob`.
|
|
4
|
+
|
|
5
|
+
## CallSite (every field required on every entry)
|
|
6
|
+
|
|
7
|
+
| Field | Type | Notes |
|
|
8
|
+
|---|---|---|
|
|
9
|
+
| id | string | `path/to/file.py::qualname`, duplicates get `#2` |
|
|
10
|
+
| file | string | relative POSIX path |
|
|
11
|
+
| line | int | >= 1 |
|
|
12
|
+
| end_line | int or null | |
|
|
13
|
+
| function | string | qualified name, `<module>` for top level |
|
|
14
|
+
| api | string | copy from the scan file, never invent new values |
|
|
15
|
+
| model | ModelRef | below |
|
|
16
|
+
| is_async | bool | |
|
|
17
|
+
| messages | list of PromptMessage, or null | |
|
|
18
|
+
| output_format | string | `text` or `json` |
|
|
19
|
+
| temperature | number or null | |
|
|
20
|
+
| max_tokens | int or null | |
|
|
21
|
+
| via | string or null | id of the shared helper, only on split entries |
|
|
22
|
+
| callers | list of strings | call site ids |
|
|
23
|
+
| notes | list of strings | Bob notes start with `Bob:` |
|
|
24
|
+
| purpose | string or null | |
|
|
25
|
+
| output_contract | string or null | |
|
|
26
|
+
| difficulty | string or null | `easy`, `medium`, `hard` |
|
|
27
|
+
| grading | string or null | `exact`, `json_fields`, `judge` |
|
|
28
|
+
| found_by | string | `ast`, `bob`, `manual` |
|
|
29
|
+
|
|
30
|
+
## ModelRef
|
|
31
|
+
|
|
32
|
+
| Field | Type | Notes |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| value | string or null | the model name |
|
|
35
|
+
| source | string | `literal`, `constant`, `env_default`, `env`, `dict_lookup`, `parameter_default`, `kwargs`, `missing`, `dynamic`, `manual` (use `manual` when Bob resolves it) |
|
|
36
|
+
| expression | string | source text of the model argument, keep as scanned |
|
|
37
|
+
| env_var | string or null | |
|
|
38
|
+
| defined_in | string or null | file where the model string lives |
|
|
39
|
+
|
|
40
|
+
## PromptMessage
|
|
41
|
+
|
|
42
|
+
| Field | Type | Notes |
|
|
43
|
+
|---|---|---|
|
|
44
|
+
| role | string | `system`, `user`, `assistant` |
|
|
45
|
+
| content | string | template text, runtime values as `{placeholder}` |
|
|
46
|
+
| resolved | bool | true once Bob has resolved it |
|
|
47
|
+
|
|
48
|
+
## Example: one entry split from a shared helper
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"id": "app/billing.py::summarize_invoice",
|
|
53
|
+
"file": "app/billing.py",
|
|
54
|
+
"line": 42,
|
|
55
|
+
"end_line": 45,
|
|
56
|
+
"function": "summarize_invoice",
|
|
57
|
+
"api": "openai.chat.completions",
|
|
58
|
+
"model": {
|
|
59
|
+
"value": "gpt-4o-mini",
|
|
60
|
+
"source": "manual",
|
|
61
|
+
"expression": "MODEL",
|
|
62
|
+
"env_var": null,
|
|
63
|
+
"defined_in": "app/llm.py"
|
|
64
|
+
},
|
|
65
|
+
"is_async": false,
|
|
66
|
+
"messages": [
|
|
67
|
+
{"role": "system", "content": "Summarize the invoice in one sentence.", "resolved": true},
|
|
68
|
+
{"role": "user", "content": "{invoice_text}", "resolved": true}
|
|
69
|
+
],
|
|
70
|
+
"output_format": "text",
|
|
71
|
+
"temperature": 0.0,
|
|
72
|
+
"max_tokens": null,
|
|
73
|
+
"via": "app/llm.py::complete",
|
|
74
|
+
"callers": [],
|
|
75
|
+
"notes": ["Bob: split from shared helper app/llm.py::complete; this caller builds its own prompt."],
|
|
76
|
+
"purpose": "Summarize an invoice for the billing dashboard.",
|
|
77
|
+
"output_contract": "One sentence, under 30 words, mentions the total amount, no speculation.",
|
|
78
|
+
"difficulty": "easy",
|
|
79
|
+
"grading": "judge",
|
|
80
|
+
"found_by": "bob"
|
|
81
|
+
}
|
|
82
|
+
```
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: downshift-evals
|
|
3
|
+
description: Write eval sets for audited LLM call sites. One JSONL file of 20 to 25 test cases per call site, with inputs matching the prompt placeholders and expected answers that follow the output contract and the refund policy.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Downshift evals
|
|
7
|
+
|
|
8
|
+
Input: `downshift.audit.json` and the call site(s) you are assigned.
|
|
9
|
+
Output: one file per call site in the `evals/` folder next to the audit, for example `examples/supportdesk/evals/supportdesk.triage__classify_category.jsonl`.
|
|
10
|
+
Format reference: `format-reference.md` in this skill folder. Read it before writing.
|
|
11
|
+
|
|
12
|
+
## Steps
|
|
13
|
+
|
|
14
|
+
1. Read `format-reference.md`, your call site's entry in the audit (`messages`, `output_contract`, `grading`), `data/refund_policy.md` and `data/tickets.jsonl`. Read nothing else.
|
|
15
|
+
2. Plan 20 to 25 cases in a todo list before writing: which label or rule each case covers, and whether its input comes from `tickets.jsonl` or is new.
|
|
16
|
+
3. Write the file. One JSON object per line, no trailing commas, no comments.
|
|
17
|
+
4. Finish with a short table: file, case count, cases per label (or per rule). Do not run commands. The user checks with `downshift check-evals`.
|
|
18
|
+
|
|
19
|
+
## Rules for every call site
|
|
20
|
+
|
|
21
|
+
- Inputs: about half adapted from `tickets.jsonl` (use each ticket at most once per file), the rest new. New tickets look like real customer mail: same store, order IDs like `ORD-10xxx`, dates in 2026.
|
|
22
|
+
- `ticket_text` is always `subject + "\n\n" + body`, exactly as the app builds it.
|
|
23
|
+
- Cover every allowed label or rule at least 3 times when the site has a label set.
|
|
24
|
+
- Include these edge cases where they apply: very short text, non-English text (es, fr, de, hi, pt), mixed language, sarcasm, typos or all caps, missing order ID, dates a day or two either side of a policy limit.
|
|
25
|
+
- Only write cases a careful human would label the same way. If a case could reasonably have two answers, drop it. Never put a date exactly on a policy limit (exactly 14, 30 or 60 days); use 1 or 2 days either side.
|
|
26
|
+
- `notes` on every case: one line saying what the case tests.
|
|
27
|
+
- `id`: short prefix plus number, for example `cat-01`. Unique within the file.
|
|
28
|
+
- `grading` on every case equals the call site's `grading` in the audit.
|
|
29
|
+
|
|
30
|
+
## Per call site
|
|
31
|
+
|
|
32
|
+
### triage.py::classify_category (exact)
|
|
33
|
+
Labels: billing, shipping, technical, account, refund, other.
|
|
34
|
+
- refund: the customer asks for money back or a return for money, whatever the reason.
|
|
35
|
+
- billing: charges, duplicate charges, invoices, payment methods, with no refund or return ask.
|
|
36
|
+
- shipping: delivery status, tracking, address, delays, with no money-back ask.
|
|
37
|
+
- technical: app or website bugs, crashes, errors.
|
|
38
|
+
- account: login problems that are not bugs, password reset, email or profile changes, account closure.
|
|
39
|
+
- other: product questions, feedback, anything else.
|
|
40
|
+
|
|
41
|
+
### triage.py::detect_sentiment (exact)
|
|
42
|
+
Labels: positive, neutral, negative. Overall tone of the whole ticket. Sarcasm ("Great, lost again. Love it.") is negative. A plain question with no emotion is neutral.
|
|
43
|
+
|
|
44
|
+
### triage.py::tag_urgency (exact)
|
|
45
|
+
Labels: low, medium, high, by the prompt's rules. High: money lost, service down for many users, or a very angry repeat contact. Medium: an order or account problem that blocks the customer. Low: questions, feedback, requests with no time pressure.
|
|
46
|
+
Input `category` must be the correct category of that ticket under the classify_category rules above.
|
|
47
|
+
|
|
48
|
+
### misc_utils.py::lang_of (exact)
|
|
49
|
+
Expected: two-letter lowercase ISO 639-1 code. Input `text` is `subject + "\n\n" + body`. Cover at least en, es, fr, de, hi, pt. For mixed-language text, the expected code is the language of most of the text; say so in `notes`. Skip texts too short to tell.
|
|
50
|
+
|
|
51
|
+
### extract.py::extract_order_info (json_fields)
|
|
52
|
+
Expected keys: `order_id`, `order_date`.
|
|
53
|
+
- `order_id`: the `ORD-` ID as written, or null. Avoid tickets with two different order IDs.
|
|
54
|
+
- `order_date`: the date the order was placed, as YYYY-MM-DD, or null. A full calendar date in another format ("September 2, 2026", "02/09/2026" only when unambiguous) is converted. Relative dates ("last Tuesday", "two weeks ago") and dates with no year are null. Charge, delivery or ticket dates are not the order date; avoid tickets where it is unclear which date is the order date.
|
|
55
|
+
|
|
56
|
+
### policy.py::decide_refund (json_fields)
|
|
57
|
+
Expected keys: `is_refund_request`, `eligible`, `policy_section`. `reasoning` is not graded and must not appear in `expected`.
|
|
58
|
+
Inputs: `policy` via the shared first line (see format reference), `today` (YYYY-MM-DD, the ticket date), `order_info_json` (an object `{"order_id": ..., "order_date": ...}` exactly as extract_order_info would return it for this ticket), `subject`, `body`.
|
|
59
|
+
Days = calendar days from `order_date` to `today`. Apply in this order:
|
|
60
|
+
1. Not asking for money back: `false`, `"no"`, `null`.
|
|
61
|
+
2. `order_id` or `order_date` is null: `true`, `"need_info"`, `"4.1"`.
|
|
62
|
+
3. Digital product already downloaded or activated: `true`, `"no"`, `"3.1"`.
|
|
63
|
+
4. Clearance item (keep days at 30 or under): `true`, `"store_credit"`, `"3.2"`.
|
|
64
|
+
5. Not delivered: days 14 or more `"yes"`, under 14 `"no"`; section `"3.3"`.
|
|
65
|
+
6. Damaged or defective: days 60 or under `"yes"` with `"2.1"`; over 60 `"no"` with `"2.2"`.
|
|
66
|
+
7. Change of mind: used or not in original packaging `"no"` with `"1.2"`; otherwise days 30 or under `"yes"`, over 30 `"no"`, both `"1.1"`.
|
|
67
|
+
Aim for at least 2 cases per rule and at least 4 near a day limit. Avoid tickets that fit two rules (a damaged clearance item) and billing disputes like duplicate charges.
|
|
68
|
+
|
|
69
|
+
### agent_assist.py::summarize_for_agent (judge)
|
|
70
|
+
Inputs: `subject`, `body`. `expected` is a rubric string: "Exactly 2 short English sentences. Sentence 1 says the customer wants <X>. Sentence 2 mentions <every order ID, date and amount in the ticket, listed>. No details that are not in the ticket." Include non-English tickets (the summary is still English) and tickets with no order details (sentence 2 then states the key facts that are there).
|
|
71
|
+
|
|
72
|
+
### agent_assist.py::draft_reply (judge)
|
|
73
|
+
Inputs: `language` (ISO code of the ticket), `summary` (a correct 2-sentence English summary you write), `subject`, `body`. `expected` is a rubric string: "Written in <language name>. Under 120 words. Addresses <the specific issue>. Says a teammate will confirm next steps. Does not promise a refund, credit, delivery date or any timeframe." Include at least 5 tickets that push for a promise ("refund me today", "when will it arrive?") and at least 4 non-English languages.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# Eval file format
|
|
2
|
+
|
|
3
|
+
One file per call site: `<slug>.jsonl` in the `evals/` folder next to `downshift.audit.json`.
|
|
4
|
+
|
|
5
|
+
Slug: take the call site id, drop `.py`, replace `/` with `.` and `::` with `__`.
|
|
6
|
+
`supportdesk/triage.py::classify_category` becomes `supportdesk.triage__classify_category.jsonl`.
|
|
7
|
+
|
|
8
|
+
## Lines
|
|
9
|
+
|
|
10
|
+
Each line is one JSON object. Blank lines are ignored.
|
|
11
|
+
|
|
12
|
+
Optional first line, only for long inputs that every case shares:
|
|
13
|
+
|
|
14
|
+
```json
|
|
15
|
+
{"shared": {"policy": {"file": "../data/refund_policy.md"}}}
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
A shared value is any JSON value, or `{"file": "path"}` relative to the eval file. A case's own inputs override shared ones.
|
|
19
|
+
|
|
20
|
+
Every other line is a case with exactly these keys:
|
|
21
|
+
|
|
22
|
+
| key | type | rule |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `id` | string | unique in the file |
|
|
25
|
+
| `inputs` | object | keys are exactly the prompt's `{placeholders}`, minus shared ones |
|
|
26
|
+
| `expected` | depends on grading | see below |
|
|
27
|
+
| `grading` | string | same as the call site's `grading` in the audit |
|
|
28
|
+
| `notes` | string | what this case tests |
|
|
29
|
+
|
|
30
|
+
## expected by grading
|
|
31
|
+
|
|
32
|
+
- `exact`: a string. When the output contract says "from the set {...}", it must be one of those labels, spelled exactly.
|
|
33
|
+
- `json_fields`: an object whose keys are exactly the fields after "Graded fields:" in the output contract. No other keys.
|
|
34
|
+
- `judge`: a rubric string a grader can check point by point.
|
|
35
|
+
|
|
36
|
+
## Inputs per call site
|
|
37
|
+
|
|
38
|
+
| file | inputs |
|
|
39
|
+
|---|---|
|
|
40
|
+
| supportdesk.triage__classify_category | ticket_text |
|
|
41
|
+
| supportdesk.triage__detect_sentiment | ticket_text |
|
|
42
|
+
| supportdesk.triage__tag_urgency | category, ticket_text |
|
|
43
|
+
| supportdesk.misc_utils__lang_of | text |
|
|
44
|
+
| supportdesk.extract__extract_order_info | ticket_text |
|
|
45
|
+
| supportdesk.policy__decide_refund | policy (shared), today, order_info_json, subject, body |
|
|
46
|
+
| supportdesk.agent_assist__summarize_for_agent | subject, body |
|
|
47
|
+
| supportdesk.agent_assist__draft_reply | language, summary, subject, body |
|
|
48
|
+
|
|
49
|
+
## Examples
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{"id": "cat-01", "inputs": {"ticket_text": "Charged twice\n\nI was charged twice for ORD-10733. Please fix it."}, "expected": "billing", "grading": "exact", "notes": "duplicate charge, no refund ask"}
|
|
53
|
+
{"id": "ext-01", "inputs": {"ticket_text": "Late order\n\nOrder ORD-10588 placed on August 28, 2026 hasn't arrived."}, "expected": {"order_id": "ORD-10588", "order_date": "2026-08-28"}, "grading": "json_fields", "notes": "written-out date converted"}
|
|
54
|
+
{"id": "ref-01", "inputs": {"today": "2026-09-19", "order_info_json": {"order_id": "ORD-10588", "order_date": "2026-08-28"}, "subject": "Where is my package??", "body": "Order ORD-10588 placed on 2026-08-28 still hasn't arrived. I want my money back."}, "expected": {"is_refund_request": true, "eligible": "yes", "policy_section": "3.3"}, "grading": "json_fields", "notes": "not delivered after 22 days"}
|
|
55
|
+
```
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Keep Bob's context small (every file read costs Bobcoins)
|
|
2
|
+
.venv/
|
|
3
|
+
.git/
|
|
4
|
+
results/
|
|
5
|
+
**/.downshift/
|
|
6
|
+
web/node_modules/
|
|
7
|
+
web/.next/
|
|
8
|
+
dist/
|
|
9
|
+
build/
|
|
10
|
+
*.egg-info/
|
|
11
|
+
__pycache__/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
htmlcov/
|
|
16
|
+
.coverage
|
|
17
|
+
bob_sessions/
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v5
|
|
17
|
+
- uses: actions/setup-python@v6
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
cache: pip
|
|
21
|
+
- name: Install
|
|
22
|
+
run: pip install -e ".[dev]"
|
|
23
|
+
- name: Lint
|
|
24
|
+
run: |
|
|
25
|
+
ruff check .
|
|
26
|
+
ruff format --check .
|
|
27
|
+
- name: Type check
|
|
28
|
+
run: mypy src
|
|
29
|
+
- name: Test
|
|
30
|
+
run: pytest --cov=downshift --cov-report=term-missing --cov-fail-under=80
|
|
31
|
+
|
|
32
|
+
web:
|
|
33
|
+
runs-on: ubuntu-latest
|
|
34
|
+
defaults:
|
|
35
|
+
run:
|
|
36
|
+
working-directory: web
|
|
37
|
+
steps:
|
|
38
|
+
- uses: actions/checkout@v5
|
|
39
|
+
- uses: actions/setup-node@v7
|
|
40
|
+
with:
|
|
41
|
+
node-version: "22"
|
|
42
|
+
cache: npm
|
|
43
|
+
cache-dependency-path: web/package-lock.json
|
|
44
|
+
- name: Install
|
|
45
|
+
run: npm ci
|
|
46
|
+
- name: Lint
|
|
47
|
+
run: npm run lint
|
|
48
|
+
- name: Type check
|
|
49
|
+
run: npm run typecheck
|
|
50
|
+
- name: Test
|
|
51
|
+
run: npm test
|
|
52
|
+
- name: Build
|
|
53
|
+
run: npm run build
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: Cost diff
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
pull-requests: write
|
|
9
|
+
issues: write
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
cost-diff:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v5
|
|
16
|
+
- name: Downshift cost diff (SupportDesk)
|
|
17
|
+
uses: ./
|
|
18
|
+
with:
|
|
19
|
+
path: examples/supportdesk
|
|
20
|
+
package: .
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
release:
|
|
5
|
+
types: [published]
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
build:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v5
|
|
12
|
+
- uses: actions/setup-python@v6
|
|
13
|
+
with:
|
|
14
|
+
python-version: "3.12"
|
|
15
|
+
- name: Build
|
|
16
|
+
run: |
|
|
17
|
+
pip install build twine
|
|
18
|
+
python -m build
|
|
19
|
+
twine check dist/*
|
|
20
|
+
- uses: actions/upload-artifact@v7
|
|
21
|
+
with:
|
|
22
|
+
name: dist
|
|
23
|
+
path: dist/
|
|
24
|
+
|
|
25
|
+
publish:
|
|
26
|
+
needs: build
|
|
27
|
+
runs-on: ubuntu-latest
|
|
28
|
+
environment: pypi
|
|
29
|
+
permissions:
|
|
30
|
+
id-token: write
|
|
31
|
+
steps:
|
|
32
|
+
- uses: actions/download-artifact@v8
|
|
33
|
+
with:
|
|
34
|
+
name: dist
|
|
35
|
+
path: dist/
|
|
36
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to agents when working with code in this repository.
|
|
4
|
+
|
|
5
|
+
## Project
|
|
6
|
+
|
|
7
|
+
Python CLI tool (`downshift`) that finds LLM call sites in Python repos via AST analysis, evaluates cheaper models per call site, and projects cost impact per PR. Package lives in `src/downshift/`, installed editably from `pyproject.toml`. Architecture overview: `docs/architecture.md`.
|
|
8
|
+
|
|
9
|
+
## Commands
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Install (dev)
|
|
13
|
+
pip install -e ".[dev]"
|
|
14
|
+
|
|
15
|
+
# Checks before every commit, in this order
|
|
16
|
+
ruff format . && ruff check . && mypy src
|
|
17
|
+
|
|
18
|
+
# All tests (excludes integration by default). Always python -m pytest, never bare pytest.
|
|
19
|
+
python -m pytest
|
|
20
|
+
|
|
21
|
+
# Single file / single test
|
|
22
|
+
python -m pytest tests/unit/test_scanner.py
|
|
23
|
+
python -m pytest tests/unit/test_scanner.py::test_finds_direct_openai_call
|
|
24
|
+
|
|
25
|
+
# Integration tests (requires local Ollama)
|
|
26
|
+
python -m pytest -m integration
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Critical patterns
|
|
30
|
+
|
|
31
|
+
- **`from __future__ import annotations`** in every source module under `src/downshift/`.
|
|
32
|
+
- **Validation collects all problems**: gather issues into a `list[str]`, then raise one exception with all of them. Never raise on the first error. See `parse_config()` and `CallSite.from_dict()`.
|
|
33
|
+
- **`_MISSING` sentinel**: `schema.py` uses `_MISSING: Any = object()` instead of `None` as the not-provided signal in `_field()`, because `None` is a valid value for nullable fields.
|
|
34
|
+
- **`frozenset` for allowed values**: `MODEL_SOURCES`, `OUTPUT_FORMATS`, `DIFFICULTIES`, `GRADINGS`, `PRODUCERS` are module-level `frozenset[str]`, not enums.
|
|
35
|
+
- **Dataclasses only**: all data objects are `@dataclass` (often `frozen=True`). No Pydantic, attrs or TypedDict.
|
|
36
|
+
|
|
37
|
+
## Testing
|
|
38
|
+
|
|
39
|
+
- Tests ship in the same commit as the code they cover. Coverage target >= 80% on `src/downshift/`.
|
|
40
|
+
- Default test run has no network and no models: use `FakeLLMClient` from `llm.py`. Anything needing Ollama is marked `@pytest.mark.integration`.
|
|
41
|
+
- Scanner unit tests use `scanner.scan_source(textwrap.dedent(src), "app.py")` on inline strings. Use `tmp_path` only where real files are needed (CLI tests, temp git repos).
|
|
42
|
+
- CLI commands are tested with typer's `CliRunner`. Report markdown uses snapshot tests.
|
|
43
|
+
|
|
44
|
+
## Code style
|
|
45
|
+
|
|
46
|
+
- Line length 100. Ruff rules `E, F, I, B, UP, SIM`. Use `zip(..., strict=True)`.
|
|
47
|
+
- `typer.Option` / `typer.Argument` are in `extend-immutable-calls` so bugbear won't flag them.
|
|
48
|
+
- Every source module has a docstring describing its purpose and who produces/consumes its data.
|
|
49
|
+
- Private helpers are prefixed `_`; keep the public API minimal.
|
|
50
|
+
- Conventional commits: `feat`, `fix`, `test`, `docs`, `ci`, `chore`.
|
|
51
|
+
|
|
52
|
+
## Architecture
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
CLI (cli.py) -> resolve_config() -> scan_path() -> ScanResult (schema.py)
|
|
56
|
+
|
|
|
57
|
+
Module / ModuleIndex / Resolver (resolve.py)
|
|
58
|
+
|
|
|
59
|
+
_CallFinder (AST visitor, scanner.py)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- `schema.py` is the shared data contract: the AST scanner and the Bob auditor both write and read through it.
|
|
63
|
+
- `ScanResult.write()` auto-creates parent directories. Scan output defaults to `<scanned-dir>/.downshift/callsites.json` (gitignored).
|
|
64
|
+
- `SKIP_DIRS` in `scanner.py` lists directories the walker never enters (`.venv`, `__pycache__`, `.downshift`, ...).
|
|
65
|
+
- Call site IDs are `<rel_path>::<qualname>` (e.g. `app/triage.py::classify`); duplicates get `#2`, `#3`.
|
|
66
|
+
|
|
67
|
+
## Config (`downshift.yaml`)
|
|
68
|
+
|
|
69
|
+
- Optional; auto-discovered next to the scanned path. Absent config means all defaults.
|
|
70
|
+
- `schema_version: 1` (callsites JSON) and `version: 1` (config YAML) are separate fields.
|
|
71
|
+
- Pricing uses `input`/`output` keys in YAML; the dataclass fields are `input_per_mtok`/`output_per_mtok`.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
5
|
+
and this project uses [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-09-26
|
|
10
|
+
|
|
11
|
+
First public release.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
- `downshift scan`: AST scanner for OpenAI (v1 SDK) chat/responses and Anthropic messages calls. Resolves models through constants, imports, env var defaults, dict lookups and parameter defaults; recovers prompt templates; records callers.
|
|
15
|
+
- `callsites.json` schema with strict validation, shared by the scanner and Bob.
|
|
16
|
+
- `downshift validate` and `downshift compare`: check a Bob audit and compare it with the ast scan.
|
|
17
|
+
- IBM Bob integration in `.bob/`: Downshift Auditor and Downshift Eval Writer custom modes with the `downshift-audit` and `downshift-evals` skills.
|
|
18
|
+
- Eval sets (`evals/<call_site>.jsonl`), `downshift check-evals`, and `downshift evalgen` to write evals with any configured model.
|
|
19
|
+
- `downshift run`: runs each call site's evals on the baseline and candidate models. Resumable, with warm-up and token and latency tracking.
|
|
20
|
+
- Scoring by exact match, JSON fields, or an LLM judge, plus `downshift rescore` with a separate hosted judge.
|
|
21
|
+
- Decisions (cheapest model that keeps the quality threshold and minimum pass rate), monthly cost projections, and `downshift report`.
|
|
22
|
+
- `downshift diff` and a composite GitHub Action (`action.yml`) that comments the projected monthly cost change on every PR, with an optional `fail-above` limit.
|
|
23
|
+
- `downshift estimate`: price a scan or audit file, or compare two, without running evals.
|
|
24
|
+
- `downshift export` and a demo web app (Next.js, deployed at https://downshift-llm.vercel.app/).
|
|
25
|
+
- `examples/supportdesk`: demo app with 8 LLM features, 40 synthetic tickets and a refund policy document.
|
|
26
|
+
- Case studies on OrchestrAI (MIT) and mem0 (Apache-2.0).
|
|
27
|
+
- End-to-end pipeline smoke test; CI fails below 80% coverage.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
- Scanner reads files with a UTF-8 BOM.
|
|
31
|
+
- Scanner detects SDK calls that pass the model through `**kwargs` when the SDK is imported.
|
|
32
|
+
|
|
33
|
+
### Notes
|
|
34
|
+
- `diff` and `estimate` are static projections for per-PR deltas, not absolute spend.
|
|
35
|
+
- The GitHub Action now installs `downshift>=0.1.0` by default.
|
|
36
|
+
|
|
37
|
+
## [0.1.0.dev0] - 2026-09-26
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
- Pre-release to claim the PyPI name and test Trusted Publishing.
|
|
41
|
+
|
|
42
|
+
[Unreleased]: https://github.com/ana-lan/downshift/compare/v0.1.0...HEAD
|
|
43
|
+
[0.1.0]: https://github.com/ana-lan/downshift/compare/v0.1.0.dev0...v0.1.0
|
|
44
|
+
[0.1.0.dev0]: https://github.com/ana-lan/downshift/releases/tag/v0.1.0.dev0
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
```bash
|
|
5
|
+
python3.12 -m venv .venv && source .venv/bin/activate
|
|
6
|
+
pip install -e ".[dev]"
|
|
7
|
+
pre-commit install
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
## Checks
|
|
11
|
+
```bash
|
|
12
|
+
ruff check . && ruff format --check .
|
|
13
|
+
mypy src
|
|
14
|
+
pytest
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Integration tests need a local Ollama server: `pytest -m integration`.
|
|
18
|
+
|
|
19
|
+
## Workflow
|
|
20
|
+
Branch per change, open a PR, CI must pass. Use conventional commits (`feat:`, `fix:`, `test:`, `docs:`, `ci:`, `chore:`).
|