crewforge 0.2.0__tar.gz → 0.2.1__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.
Files changed (79) hide show
  1. {crewforge-0.2.0/src/crewforge.egg-info → crewforge-0.2.1}/PKG-INFO +34 -38
  2. {crewforge-0.2.0 → crewforge-0.2.1}/README.md +19 -34
  3. {crewforge-0.2.0 → crewforge-0.2.1}/pyproject.toml +46 -4
  4. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/__init__.py +1 -1
  5. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/citations.py +3 -2
  6. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/cli.py +15 -6
  7. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/language.py +3 -1
  8. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/agents.py +3 -1
  9. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/agents.py +6 -4
  10. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/ingest.py +2 -2
  11. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/pipeline.py +13 -7
  12. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/agents.py +2 -2
  13. {crewforge-0.2.0 → crewforge-0.2.1/src/crewforge.egg-info}/PKG-INFO +34 -38
  14. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/requires.txt +2 -0
  15. {crewforge-0.2.0 → crewforge-0.2.1}/LICENSE +0 -0
  16. {crewforge-0.2.0 → crewforge-0.2.1}/setup.cfg +0 -0
  17. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/__main__.py +0 -0
  18. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/cli.py +0 -0
  19. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/cli_support.py +0 -0
  20. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/__init__.py +0 -0
  21. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/agent.py +0 -0
  22. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/client.py +0 -0
  23. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/config.py +0 -0
  24. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/crew.py +0 -0
  25. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/errors.py +0 -0
  26. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/parsing.py +0 -0
  27. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/providers.py +0 -0
  28. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/usage.py +0 -0
  29. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/py.typed +0 -0
  30. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/__init__.py +0 -0
  31. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/__init__.py +0 -0
  32. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/agents.py +0 -0
  33. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/disclosure.py +0 -0
  34. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/locales/en.yaml +0 -0
  35. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/locales/ms.yaml +0 -0
  36. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/pipeline.py +0 -0
  37. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/report.py +0 -0
  38. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/settings.py +0 -0
  39. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/__init__.py +0 -0
  40. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/analytics.py +0 -0
  41. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/appcheck.py +0 -0
  42. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/cli.py +0 -0
  43. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/formatters.py +0 -0
  44. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/generator.py +0 -0
  45. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/pipeline.py +0 -0
  46. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/scaffold.py +0 -0
  47. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/docker-compose.j2 +0 -0
  48. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/dockerfile.j2 +0 -0
  49. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/env.example.j2 +0 -0
  50. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/fastapi_main.j2 +0 -0
  51. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/model.j2 +0 -0
  52. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/requirements.j2 +0 -0
  53. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/route.j2 +0 -0
  54. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/test.j2 +0 -0
  55. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/utils.py +0 -0
  56. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/__init__.py +0 -0
  57. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/agents.py +0 -0
  58. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/cli.py +0 -0
  59. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/export.py +0 -0
  60. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/pipeline.py +0 -0
  61. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/settings.py +0 -0
  62. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/text.py +0 -0
  63. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/__init__.py +0 -0
  64. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/cli.py +0 -0
  65. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/export.py +0 -0
  66. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/index.py +0 -0
  67. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/retrieve.py +0 -0
  68. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/settings.py +0 -0
  69. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/__init__.py +0 -0
  70. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/cli.py +0 -0
  71. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/pipeline.py +0 -0
  72. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/report.py +0 -0
  73. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/sources.py +0 -0
  74. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/SOURCES.txt +0 -0
  75. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/dependency_links.txt +0 -0
  76. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/entry_points.txt +0 -0
  77. {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/top_level.txt +0 -0
  78. {crewforge-0.2.0 → crewforge-0.2.1}/tests/test_honest_failure_e2e.py +0 -0
  79. {crewforge-0.2.0 → crewforge-0.2.1}/tests/test_readme.py +0 -0
@@ -1,17 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crewforge
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: One crew of LLM agents, five recipes: code review, document Q&A, API scaffolding, content and honest academic-writing help — on any OpenAI-compatible provider.
5
5
  Author: Muhammad Aiman
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/aimanmalib/crewforge
8
- Project-URL: Repository, https://github.com/aimanmalib/crewforge
9
- Project-URL: Issues, https://github.com/aimanmalib/crewforge/issues
8
+ Project-URL: Documentation, https://github.com/aimanmalib/crewforge#readme
10
9
  Project-URL: Changelog, https://github.com/aimanmalib/crewforge/blob/main/CHANGELOG.md
11
- Keywords: llm,agents,multi-agent,openai,code-review,rag,fastapi,content-generation,academic-writing,cli
10
+ Project-URL: Issues, https://github.com/aimanmalib/crewforge/issues
11
+ Project-URL: Source, https://github.com/aimanmalib/crewforge
12
+ Project-URL: Security, https://github.com/aimanmalib/crewforge/security/policy
13
+ Keywords: llm,agents,multi-agent,ai-agents,openai,openai-compatible,code-review,rag,document-qa,fastapi,api-scaffolding,content-generation,academic-writing,cli
12
14
  Classifier: Development Status :: 3 - Alpha
13
15
  Classifier: Environment :: Console
14
16
  Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Education
18
+ Classifier: Natural Language :: English
19
+ Classifier: Natural Language :: Malay
15
20
  Classifier: Operating System :: OS Independent
16
21
  Classifier: Programming Language :: Python :: 3
17
22
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -19,8 +24,12 @@ Classifier: Programming Language :: Python :: 3.10
19
24
  Classifier: Programming Language :: Python :: 3.11
20
25
  Classifier: Programming Language :: Python :: 3.12
21
26
  Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Topic :: Education
22
29
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
30
+ Classifier: Topic :: Software Development :: Code Generators
23
31
  Classifier: Topic :: Software Development :: Quality Assurance
32
+ Classifier: Topic :: Text Processing :: General
24
33
  Classifier: Typing :: Typed
25
34
  Requires-Python: >=3.10
26
35
  Description-Content-Type: text/markdown
@@ -41,6 +50,8 @@ Requires-Dist: pytest>=8.0; extra == "dev"
41
50
  Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
42
51
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
43
52
  Requires-Dist: ruff>=0.6; extra == "dev"
53
+ Requires-Dist: mypy>=1.10; extra == "dev"
54
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
44
55
  Requires-Dist: fastapi>=0.110; extra == "dev"
45
56
  Requires-Dist: httpx>=0.27; extra == "dev"
46
57
  Dynamic: license-file
@@ -51,7 +62,8 @@ Dynamic: license-file
51
62
 
52
63
  [![CI](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml/badge.svg)](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml)
53
64
  [![PyPI](https://img.shields.io/pypi/v/crewforge)](https://pypi.org/project/crewforge/)
54
- ![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue)
65
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/aimanmalib/crewforge/badge)](https://scorecard.dev/viewer/?uri=github.com/aimanmalib/crewforge)
66
+ ![Python 3.10–3.14](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue)
55
67
  ![License: MIT](https://img.shields.io/badge/license-MIT-green)
56
68
 
57
69
  ```bash
@@ -161,6 +173,11 @@ total 7 calls 22,366 tokens (provider-reported)
161
173
 
162
174
  ### `ask` and `summarize`
163
175
 
176
+ <p align="center">
177
+ <img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/demo/ask.gif" alt="Terminal demo: crewforge ask --verify answers two questions about a short project brief with [Source N] citations; the second answer says the document does not give the cost, and both answers are fact-checked claim by claim" width="900">
178
+ </p>
179
+ <p align="center"><sub>A real run of <code>crewforge ask --verify</code>: the answerer cites the passages it used as <code>[Source N]</code>, a second agent checks every claim against those passages, and the question the document does not answer is answered as such. Provider-reported tokens. Recorded with <a href="https://github.com/charmbracelet/vhs">VHS</a> from <a href="https://github.com/aimanmalib/crewforge/blob/main/docs/demo/ask.tape"><code>docs/demo/ask.tape</code></a> (the document is invented) using Gemini 3.8 Flash through an OpenAI-compatible gateway (the <code>custom</code> provider). The wait for the model is shown in full.</sub></p>
180
+
164
181
  ```text
165
182
  $ crewforge ask kestrel.md -q "How many sensors does Project Kestrel use, and who leads it?" --verify --export session.md
166
183
  ✓ ingested kestrel.md (1 chunks)
@@ -232,6 +249,11 @@ Indonesian forms such as *karena* or *kualitas*). Every step is recorded
232
249
  (file names and hashes, never content or keys) so `disclose` can write the
233
250
  AI-use statement many universities now ask for.
234
251
 
252
+ <p align="center">
253
+ <img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/demo/academic.gif" alt="Terminal demo: crewforge academic feedback --language ms comments in Malay on a student's essay draft, quoting the sentences it refers to, then a no-AI citation check lists (Lim, 2024) as cited in the text but missing from the reference list" width="900">
254
+ </p>
255
+ <p align="center"><sub>A real run of <code>crewforge academic feedback --language ms</code> on a short essay draft in Malay. Each comment quotes the student's own sentence and suggests a fix; nothing is rewritten. The citation check after it uses no AI: it finds <em>(Lim, 2024)</em> cited in the text but missing from the reference list. Only the <code>argument</code> reviewer is run so the result fits on one screen. Recorded with <a href="https://github.com/charmbracelet/vhs">VHS</a> from <a href="https://github.com/aimanmalib/crewforge/blob/main/docs/demo/academic.tape"><code>docs/demo/academic.tape</code></a> (draft, brief and reference are invented) using Gemini 3.8 Flash through an OpenAI-compatible gateway (the <code>custom</code> provider). The wait for the model is shown in full.</sub></p>
256
+
235
257
  ```text
236
258
  $ crewforge academic plan brief.md -f markdown # a 1,200-word TSL3013 essay brief
237
259
  # Assignment plan: Regulated Integration over Prohibition: ...
@@ -309,36 +331,9 @@ An unknown provider name is a configuration error, not a silent fallback.
309
331
 
310
332
  ## Architecture
311
333
 
312
- ```mermaid
313
- flowchart LR
314
- CLI["crewforge CLI<br/>review · ask · summarize · ingest<br/>scaffold · validate · write<br/>academic plan · feedback · cite · disclose"] --> R
315
-
316
- subgraph R["Recipes (src/crewforge/recipes)"]
317
- RV[review]
318
- DC[docs]
319
- AP[api]
320
- CT[content]
321
- AC[academic]
322
- end
323
-
324
- R --> CREW
325
-
326
- subgraph CORE["Core (src/crewforge/core)"]
327
- CREW["Crew<br/>step() · parallel()<br/>names failed agents"]
328
- AG["Agent<br/>ask() · ask_json()<br/>rejects empty output"]
329
- PA["parsing<br/>tolerant JSON + schema"]
330
- CL["LLMClient<br/>stream=false · retries 429/5xx only"]
331
- US["UsageTracker<br/>provider-reported tokens"]
332
- CF["LLMConfig + provider presets<br/>keys from env only"]
333
- CREW --> AG
334
- AG --> PA
335
- AG --> CL
336
- AG --> US
337
- CL --> CF
338
- end
339
-
340
- CL -->|"/chat/completions"| P[("Any OpenAI-compatible provider")]
341
- ```
334
+ <p align="center"><img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/architecture.png" alt="Architecture: the CLI dispatches to five recipes; each recipe builds a Crew of Agents on the shared core (parsing, LLMClient, LLMConfig, UsageTracker), which talks to any OpenAI-compatible provider over /chat/completions" width="100%"></p>
335
+
336
+ <sub>Source: [docs/architecture.mmd](https://github.com/aimanmalib/crewforge/blob/main/docs/architecture.mmd) (re-render with `scripts/render_architecture.py`).</sub>
342
337
 
343
338
  - **`core/client.py`** — one async client. It always sends `"stream": false`,
344
339
  retries only HTTP 429, 5xx and network/timeout errors (with capped exponential
@@ -422,14 +417,15 @@ were carried over into this repository (see [docs/porting.md](https://github.com
422
417
  for what was ported, re-targeted at the shared core, or dropped and why).
423
418
 
424
419
  The evidence that it works is meant to be checkable: the test suite (run
425
- `pytest`; it needs no key and no network), CI on Python 3.10–3.13, and the
420
+ `pytest`; it needs no key and no network), CI on Python 3.10–3.14, and the
426
421
  real-provider runs shown above, two of which found bugs that were fixed with
427
422
  tests before the output was recorded.
428
423
 
429
424
  ## Contributing
430
425
 
431
- See [CONTRIBUTING.md](https://github.com/aimanmalib/crewforge/blob/main/CONTRIBUTING.md). In short: `pip install -e ".[dev,docs]"`,
432
- `pytest`, `ruff check . && ruff format --check .`, and every new failure path
426
+ See [CONTRIBUTING.md](https://github.com/aimanmalib/crewforge/blob/main/CONTRIBUTING.md). In short: `uv sync --extra dev --extra docs`
427
+ (versions from the committed `uv.lock`, as in CI), `uv run pytest`,
428
+ `uv run ruff check . && uv run ruff format --check .`, and every new failure path
433
429
  needs a test that fails if the guard is removed.
434
430
 
435
431
  ## Security
@@ -4,7 +4,8 @@
4
4
 
5
5
  [![CI](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml/badge.svg)](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml)
6
6
  [![PyPI](https://img.shields.io/pypi/v/crewforge)](https://pypi.org/project/crewforge/)
7
- ![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue)
7
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/aimanmalib/crewforge/badge)](https://scorecard.dev/viewer/?uri=github.com/aimanmalib/crewforge)
8
+ ![Python 3.10–3.14](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue)
8
9
  ![License: MIT](https://img.shields.io/badge/license-MIT-green)
9
10
 
10
11
  ```bash
@@ -114,6 +115,11 @@ total 7 calls 22,366 tokens (provider-reported)
114
115
 
115
116
  ### `ask` and `summarize`
116
117
 
118
+ <p align="center">
119
+ <img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/demo/ask.gif" alt="Terminal demo: crewforge ask --verify answers two questions about a short project brief with [Source N] citations; the second answer says the document does not give the cost, and both answers are fact-checked claim by claim" width="900">
120
+ </p>
121
+ <p align="center"><sub>A real run of <code>crewforge ask --verify</code>: the answerer cites the passages it used as <code>[Source N]</code>, a second agent checks every claim against those passages, and the question the document does not answer is answered as such. Provider-reported tokens. Recorded with <a href="https://github.com/charmbracelet/vhs">VHS</a> from <a href="https://github.com/aimanmalib/crewforge/blob/main/docs/demo/ask.tape"><code>docs/demo/ask.tape</code></a> (the document is invented) using Gemini 3.8 Flash through an OpenAI-compatible gateway (the <code>custom</code> provider). The wait for the model is shown in full.</sub></p>
122
+
117
123
  ```text
118
124
  $ crewforge ask kestrel.md -q "How many sensors does Project Kestrel use, and who leads it?" --verify --export session.md
119
125
  ✓ ingested kestrel.md (1 chunks)
@@ -185,6 +191,11 @@ Indonesian forms such as *karena* or *kualitas*). Every step is recorded
185
191
  (file names and hashes, never content or keys) so `disclose` can write the
186
192
  AI-use statement many universities now ask for.
187
193
 
194
+ <p align="center">
195
+ <img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/demo/academic.gif" alt="Terminal demo: crewforge academic feedback --language ms comments in Malay on a student's essay draft, quoting the sentences it refers to, then a no-AI citation check lists (Lim, 2024) as cited in the text but missing from the reference list" width="900">
196
+ </p>
197
+ <p align="center"><sub>A real run of <code>crewforge academic feedback --language ms</code> on a short essay draft in Malay. Each comment quotes the student's own sentence and suggests a fix; nothing is rewritten. The citation check after it uses no AI: it finds <em>(Lim, 2024)</em> cited in the text but missing from the reference list. Only the <code>argument</code> reviewer is run so the result fits on one screen. Recorded with <a href="https://github.com/charmbracelet/vhs">VHS</a> from <a href="https://github.com/aimanmalib/crewforge/blob/main/docs/demo/academic.tape"><code>docs/demo/academic.tape</code></a> (draft, brief and reference are invented) using Gemini 3.8 Flash through an OpenAI-compatible gateway (the <code>custom</code> provider). The wait for the model is shown in full.</sub></p>
198
+
188
199
  ```text
189
200
  $ crewforge academic plan brief.md -f markdown # a 1,200-word TSL3013 essay brief
190
201
  # Assignment plan: Regulated Integration over Prohibition: ...
@@ -262,36 +273,9 @@ An unknown provider name is a configuration error, not a silent fallback.
262
273
 
263
274
  ## Architecture
264
275
 
265
- ```mermaid
266
- flowchart LR
267
- CLI["crewforge CLI<br/>review · ask · summarize · ingest<br/>scaffold · validate · write<br/>academic plan · feedback · cite · disclose"] --> R
268
-
269
- subgraph R["Recipes (src/crewforge/recipes)"]
270
- RV[review]
271
- DC[docs]
272
- AP[api]
273
- CT[content]
274
- AC[academic]
275
- end
276
-
277
- R --> CREW
278
-
279
- subgraph CORE["Core (src/crewforge/core)"]
280
- CREW["Crew<br/>step() · parallel()<br/>names failed agents"]
281
- AG["Agent<br/>ask() · ask_json()<br/>rejects empty output"]
282
- PA["parsing<br/>tolerant JSON + schema"]
283
- CL["LLMClient<br/>stream=false · retries 429/5xx only"]
284
- US["UsageTracker<br/>provider-reported tokens"]
285
- CF["LLMConfig + provider presets<br/>keys from env only"]
286
- CREW --> AG
287
- AG --> PA
288
- AG --> CL
289
- AG --> US
290
- CL --> CF
291
- end
292
-
293
- CL -->|"/chat/completions"| P[("Any OpenAI-compatible provider")]
294
- ```
276
+ <p align="center"><img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/architecture.png" alt="Architecture: the CLI dispatches to five recipes; each recipe builds a Crew of Agents on the shared core (parsing, LLMClient, LLMConfig, UsageTracker), which talks to any OpenAI-compatible provider over /chat/completions" width="100%"></p>
277
+
278
+ <sub>Source: [docs/architecture.mmd](https://github.com/aimanmalib/crewforge/blob/main/docs/architecture.mmd) (re-render with `scripts/render_architecture.py`).</sub>
295
279
 
296
280
  - **`core/client.py`** — one async client. It always sends `"stream": false`,
297
281
  retries only HTTP 429, 5xx and network/timeout errors (with capped exponential
@@ -375,14 +359,15 @@ were carried over into this repository (see [docs/porting.md](https://github.com
375
359
  for what was ported, re-targeted at the shared core, or dropped and why).
376
360
 
377
361
  The evidence that it works is meant to be checkable: the test suite (run
378
- `pytest`; it needs no key and no network), CI on Python 3.10–3.13, and the
362
+ `pytest`; it needs no key and no network), CI on Python 3.10–3.14, and the
379
363
  real-provider runs shown above, two of which found bugs that were fixed with
380
364
  tests before the output was recorded.
381
365
 
382
366
  ## Contributing
383
367
 
384
- See [CONTRIBUTING.md](https://github.com/aimanmalib/crewforge/blob/main/CONTRIBUTING.md). In short: `pip install -e ".[dev,docs]"`,
385
- `pytest`, `ruff check . && ruff format --check .`, and every new failure path
368
+ See [CONTRIBUTING.md](https://github.com/aimanmalib/crewforge/blob/main/CONTRIBUTING.md). In short: `uv sync --extra dev --extra docs`
369
+ (versions from the committed `uv.lock`, as in CI), `uv run pytest`,
370
+ `uv run ruff check . && uv run ruff format --check .`, and every new failure path
386
371
  needs a test that fails if the guard is removed.
387
372
 
388
373
  ## Security
@@ -4,18 +4,36 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "crewforge"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "One crew of LLM agents, five recipes: code review, document Q&A, API scaffolding, content and honest academic-writing help — on any OpenAI-compatible provider."
9
9
  readme = "README.md"
10
10
  license = "MIT"
11
11
  license-files = ["LICENSE"]
12
12
  requires-python = ">=3.10"
13
13
  authors = [{ name = "Muhammad Aiman" }]
14
- keywords = ["llm", "agents", "multi-agent", "openai", "code-review", "rag", "fastapi", "content-generation", "academic-writing", "cli"]
14
+ keywords = [
15
+ "llm",
16
+ "agents",
17
+ "multi-agent",
18
+ "ai-agents",
19
+ "openai",
20
+ "openai-compatible",
21
+ "code-review",
22
+ "rag",
23
+ "document-qa",
24
+ "fastapi",
25
+ "api-scaffolding",
26
+ "content-generation",
27
+ "academic-writing",
28
+ "cli",
29
+ ]
15
30
  classifiers = [
16
31
  "Development Status :: 3 - Alpha",
17
32
  "Environment :: Console",
18
33
  "Intended Audience :: Developers",
34
+ "Intended Audience :: Education",
35
+ "Natural Language :: English",
36
+ "Natural Language :: Malay",
19
37
  "Operating System :: OS Independent",
20
38
  "Programming Language :: Python :: 3",
21
39
  "Programming Language :: Python :: 3 :: Only",
@@ -23,8 +41,12 @@ classifiers = [
23
41
  "Programming Language :: Python :: 3.11",
24
42
  "Programming Language :: Python :: 3.12",
25
43
  "Programming Language :: Python :: 3.13",
44
+ "Programming Language :: Python :: 3.14",
45
+ "Topic :: Education",
26
46
  "Topic :: Scientific/Engineering :: Artificial Intelligence",
47
+ "Topic :: Software Development :: Code Generators",
27
48
  "Topic :: Software Development :: Quality Assurance",
49
+ "Topic :: Text Processing :: General",
28
50
  "Typing :: Typed",
29
51
  ]
30
52
  dependencies = [
@@ -44,15 +66,19 @@ dev = [
44
66
  "pytest-asyncio>=0.23",
45
67
  "pytest-cov>=5.0",
46
68
  "ruff>=0.6",
69
+ "mypy>=1.10",
70
+ "types-PyYAML>=6.0",
47
71
  "fastapi>=0.110",
48
72
  "httpx>=0.27",
49
73
  ]
50
74
 
51
75
  [project.urls]
52
76
  Homepage = "https://github.com/aimanmalib/crewforge"
53
- Repository = "https://github.com/aimanmalib/crewforge"
54
- Issues = "https://github.com/aimanmalib/crewforge/issues"
77
+ Documentation = "https://github.com/aimanmalib/crewforge#readme"
55
78
  Changelog = "https://github.com/aimanmalib/crewforge/blob/main/CHANGELOG.md"
79
+ Issues = "https://github.com/aimanmalib/crewforge/issues"
80
+ Source = "https://github.com/aimanmalib/crewforge"
81
+ Security = "https://github.com/aimanmalib/crewforge/security/policy"
56
82
 
57
83
  [project.scripts]
58
84
  crewforge = "crewforge.cli:main"
@@ -80,6 +106,22 @@ select = ["E", "F", "W", "I", "UP", "B"]
80
106
  [tool.ruff.lint.per-file-ignores]
81
107
  "tests/**" = ["B011"]
82
108
 
109
+ [tool.mypy]
110
+ # CI runs `mypy` (lint job). Bodies of unannotated functions are checked too.
111
+ files = ["src/crewforge"]
112
+ python_version = "3.10"
113
+ plugins = ["pydantic.mypy"]
114
+ check_untyped_defs = true
115
+ strict_equality = true
116
+ warn_redundant_casts = true
117
+ warn_unused_ignores = true
118
+ warn_unused_configs = true
119
+
120
+ [[tool.mypy.overrides]]
121
+ # Pre-1.24 PyMuPDF import name, used only as a fallback; it ships no type information.
122
+ module = ["fitz"]
123
+ ignore_missing_imports = true
124
+
83
125
  [tool.coverage.run]
84
126
  source = ["crewforge"]
85
127
  branch = true
@@ -1,3 +1,3 @@
1
1
  """crewforge — one crew of LLM agents, five recipes, any OpenAI-compatible provider."""
2
2
 
3
- __version__ = "0.2.0"
3
+ __version__ = "0.2.1"
@@ -66,12 +66,13 @@ class Identifiers:
66
66
  def identifiers(text: str) -> Identifiers:
67
67
  """Find the identifiers in a reference. URLs pointing at doi.org count as the DOI."""
68
68
  dois = {_strip_trailing(m.group(1)).lower() for m in _DOI.finditer(text)}
69
- urls = set()
69
+ urls: set[str] = set()
70
70
  for m in _URL.finditer(text):
71
71
  url = _strip_trailing(m.group(0)).rstrip("/")
72
72
  if not any(host in url.lower() for host in _DOI_HOSTS):
73
73
  urls.add(url)
74
- valid, invalid = set(), set()
74
+ valid: set[str] = set()
75
+ invalid: set[str] = set()
75
76
  # Springer-style DOIs embed an ISBN ("10.1007/978-3-..."): search the rest only.
76
77
  rest = _URL.sub(" ", _DOI.sub(" ", text))
77
78
  raw_isbns = [m.group(1) for m in _ISBN_LABELLED.finditer(rest)]
@@ -21,6 +21,7 @@ from ...cli_support import (
21
21
  run,
22
22
  )
23
23
  from ...core.errors import ConfigError
24
+ from ...core.usage import UsageTracker
24
25
  from ..docs.ingest import IngestAgent
25
26
  from .agents import FEEDBACK_AGENTS, STYLES
26
27
  from .citations import split_references
@@ -147,6 +148,14 @@ def _record(
147
148
  err_console.print(t(language, "labels.recorded", path=path), highlight=False, soft_wrap=True)
148
149
 
149
150
 
151
+ def _usage(tracker: UsageTracker | None) -> int:
152
+ """Print the provider-reported usage table and return its token total."""
153
+ if tracker is None: # every AcademicCrew result carries its tracker
154
+ raise RuntimeError("academic step returned no usage tracker")
155
+ print_usage(tracker)
156
+ return tracker.total_tokens
157
+
158
+
150
159
  def _header(ctx: click.Context, step: str, target: str, language: str) -> None:
151
160
  config = llm_config(ctx)
152
161
  err_console.print(
@@ -190,7 +199,7 @@ def plan_cmd(ctx, brief, words, language, style, fmt, output, log_path, no_log):
190
199
  result = run(crew.plan(text, words=words, style=style.lower() if style else None))
191
200
  _emit(plan_markdown(result), plan_dict(result), fmt, output)
192
201
  _warn_malay(result.malay_issues, language)
193
- print_usage(result.tracker)
202
+ tokens = _usage(result.tracker)
194
203
  _record(
195
204
  ctx,
196
205
  settings,
@@ -201,7 +210,7 @@ def plan_cmd(ctx, brief, words, language, style, fmt, output, log_path, no_log):
201
210
  inputs=[(brief, text)],
202
211
  counts=result.counts(),
203
212
  style=result.style,
204
- tokens=result.tracker.total_tokens,
213
+ tokens=tokens,
205
214
  )
206
215
 
207
216
 
@@ -252,7 +261,7 @@ def feedback_cmd(
252
261
  )
253
262
  _emit(feedback_markdown(result), feedback_dict(result), fmt, output)
254
263
  _warn_malay(result.malay_issues, language)
255
- print_usage(result.tracker)
264
+ tokens = _usage(result.tracker)
256
265
  inputs = [(draft, text)] + ([(brief_path, brief_text)] if brief_path else [])
257
266
  _record(
258
267
  ctx,
@@ -264,7 +273,7 @@ def feedback_cmd(
264
273
  inputs=inputs,
265
274
  counts=result.counts(),
266
275
  style=result.style,
267
- tokens=result.tracker.total_tokens,
276
+ tokens=tokens,
268
277
  )
269
278
 
270
279
 
@@ -285,7 +294,7 @@ def cite_cmd(ctx, references, language, style, fmt, output, log_path, no_log):
285
294
  crew = AcademicCrew(llm_config(ctx), language=language)
286
295
  result = run(crew.cite(refs, style=style))
287
296
  _emit(cite_markdown(result), cite_dict(result), fmt, output)
288
- print_usage(result.tracker)
297
+ tokens = _usage(result.tracker)
289
298
  _record(
290
299
  ctx,
291
300
  settings,
@@ -296,7 +305,7 @@ def cite_cmd(ctx, references, language, style, fmt, output, log_path, no_log):
296
305
  inputs=[(references, text)],
297
306
  counts=result.counts(),
298
307
  style=style,
299
- tokens=result.tracker.total_tokens,
308
+ tokens=tokens,
300
309
  )
301
310
 
302
311
 
@@ -28,7 +28,9 @@ def locale(lang: str) -> dict[str, Any]:
28
28
  raise ConfigError(f"language must be one of {', '.join(LANGUAGES)}, not '{lang}'")
29
29
  text = (
30
30
  resources.files("crewforge.recipes.academic")
31
- .joinpath("locales", f"{lang}.yaml")
31
+ # One segment per call: Traversable.joinpath takes a single child before 3.11.
32
+ .joinpath("locales")
33
+ .joinpath(f"{lang}.yaml")
32
34
  .read_text(encoding="utf-8")
33
35
  )
34
36
  return yaml.safe_load(text)
@@ -493,7 +493,9 @@ class ValidateAgent:
493
493
  errors.append("Missing paths object")
494
494
  elif not paths:
495
495
  errors.append("No endpoints defined (paths is empty)")
496
- info = spec.get("info") if isinstance(spec.get("info"), dict) else {}
496
+ info = spec.get("info")
497
+ if not isinstance(info, dict):
498
+ info = {}
497
499
  if not info.get("title"):
498
500
  errors.append("Missing API title")
499
501
  if not info.get("version"):
@@ -178,9 +178,10 @@ class FactCheckAgent(Agent):
178
178
  raise OutputParseError(self.name, "no claims and no overall verdict")
179
179
  overall_verdict = _derive_overall(verdicts)
180
180
  else:
181
- overall_verdict = _normalize_verdict(overall)
182
- if overall_verdict is None:
181
+ normalized = _normalize_verdict(overall)
182
+ if normalized is None:
183
183
  raise OutputParseError(self.name, f"unknown overall verdict {overall!r}")
184
+ overall_verdict = normalized
184
185
  issues = [
185
186
  f"{c['verdict'].upper()}: {c.get('claim', '')}".strip()
186
187
  for c in claims
@@ -188,13 +189,14 @@ class FactCheckAgent(Agent):
188
189
  ]
189
190
  else:
190
191
  match = _OVERALL.search(content)
191
- overall_verdict = _normalize_verdict(match.group(1)) if match else None
192
- if overall_verdict is None:
192
+ parsed = _normalize_verdict(match.group(1)) if match else None
193
+ if parsed is None:
193
194
  raise OutputParseError(
194
195
  self.name,
195
196
  "no JSON verdict and no 'OVERALL: <verdict>' line",
196
197
  _excerpt(content),
197
198
  )
199
+ overall_verdict = parsed
198
200
  verdicts, issues = [], []
199
201
  for line in content.splitlines():
200
202
  if _OVERALL.search(line):
@@ -95,14 +95,14 @@ class IngestAgent:
95
95
  import pymupdf
96
96
  except ImportError:
97
97
  try:
98
- import fitz as pymupdf # older PyMuPDF releases
98
+ import fitz as pymupdf # type: ignore[no-redef] # older PyMuPDF releases
99
99
  except ImportError as exc:
100
100
  raise ConfigError(
101
101
  f"reading PDF files needs PyMuPDF: {_DOCS_EXTRA} ({path.name})"
102
102
  ) from exc
103
103
  doc = pymupdf.open(str(path))
104
104
  try:
105
- return "\n\n".join(page.get_text() for page in doc)
105
+ return "\n\n".join(page.get_text() for page in doc.pages())
106
106
  finally:
107
107
  doc.close()
108
108
 
@@ -9,10 +9,11 @@ from __future__ import annotations
9
9
 
10
10
  from dataclasses import dataclass, field
11
11
  from pathlib import Path
12
- from typing import Any
12
+ from typing import Any, TypeVar
13
13
 
14
14
  import httpx
15
15
 
16
+ from ...core.agent import Agent
16
17
  from ...core.client import LLMClient
17
18
  from ...core.config import LLMConfig
18
19
  from ...core.crew import Crew
@@ -34,6 +35,8 @@ from .ingest import Document, IngestAgent, TextChunk
34
35
  from .retrieve import RetrievalResult, RetrieverAgent
35
36
  from .settings import DocsSettings
36
37
 
38
+ A = TypeVar("A", bound=Agent)
39
+
37
40
 
38
41
  @dataclass
39
42
  class PipelineResult:
@@ -62,7 +65,7 @@ class DocsPipeline:
62
65
  self._client = client
63
66
  self._transport = transport
64
67
  self._crew: Crew | None = None
65
- self._llm_agents: dict[str, object] = {}
68
+ self._llm_agents: dict[str, Agent] = {}
66
69
  self.tracker = UsageTracker()
67
70
 
68
71
  self.ingester = IngestAgent(self.settings)
@@ -90,11 +93,12 @@ class DocsPipeline:
90
93
  self.retriever.reranker = self._crew.agent(RerankAgent)
91
94
  return self._crew
92
95
 
93
- def _llm_agent(self, cls, **kwargs):
96
+ def _llm_agent(self, cls: type[A], **kwargs: Any) -> A:
94
97
  agent = self._llm_agents.get(cls.name)
95
- if agent is None:
96
- agent = self._llm_agents[cls.name] = self.crew.agent(cls, **kwargs)
97
- return agent
98
+ if isinstance(agent, cls):
99
+ return agent
100
+ created = self._llm_agents[cls.name] = self.crew.agent(cls, **kwargs)
101
+ return created
98
102
 
99
103
  @property
100
104
  def answerer(self) -> AnswerAgent:
@@ -172,7 +176,9 @@ class DocsPipeline:
172
176
  crew.failures.append(failure)
173
177
  raise CrewFailedError(crew.name, [failure])
174
178
  if rerank:
175
- chunks = await crew.step("reranker", self.retriever.reranker.rerank(question, chunks))
179
+ reranker = self.retriever.reranker
180
+ assert reranker is not None # attached by the crew property above
181
+ chunks = await crew.step("reranker", reranker.rerank(question, chunks))
176
182
  return chunks
177
183
 
178
184
  async def ask(self, question: str, rerank: bool = False) -> Answer:
@@ -312,7 +312,7 @@ REVIEW_AGENTS: list[str] = [
312
312
  "documentation",
313
313
  ]
314
314
 
315
- AGENT_REGISTRY: dict[str, type[Agent]] = {
315
+ AGENT_REGISTRY: dict[str, type[ReviewerAgent] | type[SummaryAgent]] = {
316
316
  "security": SecurityAgent,
317
317
  "style": StyleAgent,
318
318
  "complexity": ComplexityAgent,
@@ -323,7 +323,7 @@ AGENT_REGISTRY: dict[str, type[Agent]] = {
323
323
  }
324
324
 
325
325
 
326
- def get_agent(name: str, **kwargs: Any) -> Agent:
326
+ def get_agent(name: str, **kwargs: Any) -> ReviewerAgent | SummaryAgent:
327
327
  """Instantiate a review agent by name (``client=``, ``tracker=`` keyword args)."""
328
328
  cls = AGENT_REGISTRY.get(name)
329
329
  if cls is None:
@@ -1,17 +1,22 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crewforge
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: One crew of LLM agents, five recipes: code review, document Q&A, API scaffolding, content and honest academic-writing help — on any OpenAI-compatible provider.
5
5
  Author: Muhammad Aiman
6
6
  License-Expression: MIT
7
7
  Project-URL: Homepage, https://github.com/aimanmalib/crewforge
8
- Project-URL: Repository, https://github.com/aimanmalib/crewforge
9
- Project-URL: Issues, https://github.com/aimanmalib/crewforge/issues
8
+ Project-URL: Documentation, https://github.com/aimanmalib/crewforge#readme
10
9
  Project-URL: Changelog, https://github.com/aimanmalib/crewforge/blob/main/CHANGELOG.md
11
- Keywords: llm,agents,multi-agent,openai,code-review,rag,fastapi,content-generation,academic-writing,cli
10
+ Project-URL: Issues, https://github.com/aimanmalib/crewforge/issues
11
+ Project-URL: Source, https://github.com/aimanmalib/crewforge
12
+ Project-URL: Security, https://github.com/aimanmalib/crewforge/security/policy
13
+ Keywords: llm,agents,multi-agent,ai-agents,openai,openai-compatible,code-review,rag,document-qa,fastapi,api-scaffolding,content-generation,academic-writing,cli
12
14
  Classifier: Development Status :: 3 - Alpha
13
15
  Classifier: Environment :: Console
14
16
  Classifier: Intended Audience :: Developers
17
+ Classifier: Intended Audience :: Education
18
+ Classifier: Natural Language :: English
19
+ Classifier: Natural Language :: Malay
15
20
  Classifier: Operating System :: OS Independent
16
21
  Classifier: Programming Language :: Python :: 3
17
22
  Classifier: Programming Language :: Python :: 3 :: Only
@@ -19,8 +24,12 @@ Classifier: Programming Language :: Python :: 3.10
19
24
  Classifier: Programming Language :: Python :: 3.11
20
25
  Classifier: Programming Language :: Python :: 3.12
21
26
  Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Topic :: Education
22
29
  Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
30
+ Classifier: Topic :: Software Development :: Code Generators
23
31
  Classifier: Topic :: Software Development :: Quality Assurance
32
+ Classifier: Topic :: Text Processing :: General
24
33
  Classifier: Typing :: Typed
25
34
  Requires-Python: >=3.10
26
35
  Description-Content-Type: text/markdown
@@ -41,6 +50,8 @@ Requires-Dist: pytest>=8.0; extra == "dev"
41
50
  Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
42
51
  Requires-Dist: pytest-cov>=5.0; extra == "dev"
43
52
  Requires-Dist: ruff>=0.6; extra == "dev"
53
+ Requires-Dist: mypy>=1.10; extra == "dev"
54
+ Requires-Dist: types-PyYAML>=6.0; extra == "dev"
44
55
  Requires-Dist: fastapi>=0.110; extra == "dev"
45
56
  Requires-Dist: httpx>=0.27; extra == "dev"
46
57
  Dynamic: license-file
@@ -51,7 +62,8 @@ Dynamic: license-file
51
62
 
52
63
  [![CI](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml/badge.svg)](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml)
53
64
  [![PyPI](https://img.shields.io/pypi/v/crewforge)](https://pypi.org/project/crewforge/)
54
- ![Python 3.10–3.13](https://img.shields.io/badge/python-3.10%E2%80%933.13-blue)
65
+ [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/aimanmalib/crewforge/badge)](https://scorecard.dev/viewer/?uri=github.com/aimanmalib/crewforge)
66
+ ![Python 3.10–3.14](https://img.shields.io/badge/python-3.10%E2%80%933.14-blue)
55
67
  ![License: MIT](https://img.shields.io/badge/license-MIT-green)
56
68
 
57
69
  ```bash
@@ -161,6 +173,11 @@ total 7 calls 22,366 tokens (provider-reported)
161
173
 
162
174
  ### `ask` and `summarize`
163
175
 
176
+ <p align="center">
177
+ <img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/demo/ask.gif" alt="Terminal demo: crewforge ask --verify answers two questions about a short project brief with [Source N] citations; the second answer says the document does not give the cost, and both answers are fact-checked claim by claim" width="900">
178
+ </p>
179
+ <p align="center"><sub>A real run of <code>crewforge ask --verify</code>: the answerer cites the passages it used as <code>[Source N]</code>, a second agent checks every claim against those passages, and the question the document does not answer is answered as such. Provider-reported tokens. Recorded with <a href="https://github.com/charmbracelet/vhs">VHS</a> from <a href="https://github.com/aimanmalib/crewforge/blob/main/docs/demo/ask.tape"><code>docs/demo/ask.tape</code></a> (the document is invented) using Gemini 3.8 Flash through an OpenAI-compatible gateway (the <code>custom</code> provider). The wait for the model is shown in full.</sub></p>
180
+
164
181
  ```text
165
182
  $ crewforge ask kestrel.md -q "How many sensors does Project Kestrel use, and who leads it?" --verify --export session.md
166
183
  ✓ ingested kestrel.md (1 chunks)
@@ -232,6 +249,11 @@ Indonesian forms such as *karena* or *kualitas*). Every step is recorded
232
249
  (file names and hashes, never content or keys) so `disclose` can write the
233
250
  AI-use statement many universities now ask for.
234
251
 
252
+ <p align="center">
253
+ <img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/demo/academic.gif" alt="Terminal demo: crewforge academic feedback --language ms comments in Malay on a student's essay draft, quoting the sentences it refers to, then a no-AI citation check lists (Lim, 2024) as cited in the text but missing from the reference list" width="900">
254
+ </p>
255
+ <p align="center"><sub>A real run of <code>crewforge academic feedback --language ms</code> on a short essay draft in Malay. Each comment quotes the student's own sentence and suggests a fix; nothing is rewritten. The citation check after it uses no AI: it finds <em>(Lim, 2024)</em> cited in the text but missing from the reference list. Only the <code>argument</code> reviewer is run so the result fits on one screen. Recorded with <a href="https://github.com/charmbracelet/vhs">VHS</a> from <a href="https://github.com/aimanmalib/crewforge/blob/main/docs/demo/academic.tape"><code>docs/demo/academic.tape</code></a> (draft, brief and reference are invented) using Gemini 3.8 Flash through an OpenAI-compatible gateway (the <code>custom</code> provider). The wait for the model is shown in full.</sub></p>
256
+
235
257
  ```text
236
258
  $ crewforge academic plan brief.md -f markdown # a 1,200-word TSL3013 essay brief
237
259
  # Assignment plan: Regulated Integration over Prohibition: ...
@@ -309,36 +331,9 @@ An unknown provider name is a configuration error, not a silent fallback.
309
331
 
310
332
  ## Architecture
311
333
 
312
- ```mermaid
313
- flowchart LR
314
- CLI["crewforge CLI<br/>review · ask · summarize · ingest<br/>scaffold · validate · write<br/>academic plan · feedback · cite · disclose"] --> R
315
-
316
- subgraph R["Recipes (src/crewforge/recipes)"]
317
- RV[review]
318
- DC[docs]
319
- AP[api]
320
- CT[content]
321
- AC[academic]
322
- end
323
-
324
- R --> CREW
325
-
326
- subgraph CORE["Core (src/crewforge/core)"]
327
- CREW["Crew<br/>step() · parallel()<br/>names failed agents"]
328
- AG["Agent<br/>ask() · ask_json()<br/>rejects empty output"]
329
- PA["parsing<br/>tolerant JSON + schema"]
330
- CL["LLMClient<br/>stream=false · retries 429/5xx only"]
331
- US["UsageTracker<br/>provider-reported tokens"]
332
- CF["LLMConfig + provider presets<br/>keys from env only"]
333
- CREW --> AG
334
- AG --> PA
335
- AG --> CL
336
- AG --> US
337
- CL --> CF
338
- end
339
-
340
- CL -->|"/chat/completions"| P[("Any OpenAI-compatible provider")]
341
- ```
334
+ <p align="center"><img src="https://raw.githubusercontent.com/aimanmalib/crewforge/main/docs/architecture.png" alt="Architecture: the CLI dispatches to five recipes; each recipe builds a Crew of Agents on the shared core (parsing, LLMClient, LLMConfig, UsageTracker), which talks to any OpenAI-compatible provider over /chat/completions" width="100%"></p>
335
+
336
+ <sub>Source: [docs/architecture.mmd](https://github.com/aimanmalib/crewforge/blob/main/docs/architecture.mmd) (re-render with `scripts/render_architecture.py`).</sub>
342
337
 
343
338
  - **`core/client.py`** — one async client. It always sends `"stream": false`,
344
339
  retries only HTTP 429, 5xx and network/timeout errors (with capped exponential
@@ -422,14 +417,15 @@ were carried over into this repository (see [docs/porting.md](https://github.com
422
417
  for what was ported, re-targeted at the shared core, or dropped and why).
423
418
 
424
419
  The evidence that it works is meant to be checkable: the test suite (run
425
- `pytest`; it needs no key and no network), CI on Python 3.10–3.13, and the
420
+ `pytest`; it needs no key and no network), CI on Python 3.10–3.14, and the
426
421
  real-provider runs shown above, two of which found bugs that were fixed with
427
422
  tests before the output was recorded.
428
423
 
429
424
  ## Contributing
430
425
 
431
- See [CONTRIBUTING.md](https://github.com/aimanmalib/crewforge/blob/main/CONTRIBUTING.md). In short: `pip install -e ".[dev,docs]"`,
432
- `pytest`, `ruff check . && ruff format --check .`, and every new failure path
426
+ See [CONTRIBUTING.md](https://github.com/aimanmalib/crewforge/blob/main/CONTRIBUTING.md). In short: `uv sync --extra dev --extra docs`
427
+ (versions from the committed `uv.lock`, as in CI), `uv run pytest`,
428
+ `uv run ruff check . && uv run ruff format --check .`, and every new failure path
433
429
  needs a test that fails if the guard is removed.
434
430
 
435
431
  ## Security
@@ -13,6 +13,8 @@ pytest>=8.0
13
13
  pytest-asyncio>=0.23
14
14
  pytest-cov>=5.0
15
15
  ruff>=0.6
16
+ mypy>=1.10
17
+ types-PyYAML>=6.0
16
18
  fastapi>=0.110
17
19
  httpx>=0.27
18
20
 
File without changes
File without changes