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.
- {crewforge-0.2.0/src/crewforge.egg-info → crewforge-0.2.1}/PKG-INFO +34 -38
- {crewforge-0.2.0 → crewforge-0.2.1}/README.md +19 -34
- {crewforge-0.2.0 → crewforge-0.2.1}/pyproject.toml +46 -4
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/__init__.py +1 -1
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/citations.py +3 -2
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/cli.py +15 -6
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/language.py +3 -1
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/agents.py +3 -1
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/agents.py +6 -4
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/ingest.py +2 -2
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/pipeline.py +13 -7
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/agents.py +2 -2
- {crewforge-0.2.0 → crewforge-0.2.1/src/crewforge.egg-info}/PKG-INFO +34 -38
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/requires.txt +2 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/LICENSE +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/setup.cfg +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/__main__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/cli.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/cli_support.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/__init__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/agent.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/client.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/config.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/crew.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/errors.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/parsing.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/providers.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/core/usage.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/py.typed +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/__init__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/__init__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/agents.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/disclosure.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/locales/en.yaml +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/locales/ms.yaml +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/pipeline.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/report.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/academic/settings.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/__init__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/analytics.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/appcheck.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/cli.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/formatters.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/generator.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/pipeline.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/scaffold.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/docker-compose.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/dockerfile.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/env.example.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/fastapi_main.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/model.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/requirements.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/route.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/templates/test.j2 +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/api/utils.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/__init__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/agents.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/cli.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/export.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/pipeline.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/settings.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/content/text.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/__init__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/cli.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/export.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/index.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/retrieve.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/docs/settings.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/__init__.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/cli.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/pipeline.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/report.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge/recipes/review/sources.py +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/SOURCES.txt +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/dependency_links.txt +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/entry_points.txt +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/src/crewforge.egg-info/top_level.txt +0 -0
- {crewforge-0.2.0 → crewforge-0.2.1}/tests/test_honest_failure_e2e.py +0 -0
- {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.
|
|
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:
|
|
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
|
-
|
|
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
|
[](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml)
|
|
53
64
|
[](https://pypi.org/project/crewforge/)
|
|
54
|
-
](https://scorecard.dev/viewer/?uri=github.com/aimanmalib/crewforge)
|
|
66
|
+

|
|
55
67
|

|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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.
|
|
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: `
|
|
432
|
-
|
|
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
|
[](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml)
|
|
6
6
|
[](https://pypi.org/project/crewforge/)
|
|
7
|
-
](https://scorecard.dev/viewer/?uri=github.com/aimanmalib/crewforge)
|
|
8
|
+

|
|
8
9
|

|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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.
|
|
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: `
|
|
385
|
-
|
|
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.
|
|
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 = [
|
|
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
|
-
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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=
|
|
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
|
|
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")
|
|
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
|
-
|
|
182
|
-
if
|
|
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
|
-
|
|
192
|
-
if
|
|
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,
|
|
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
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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[
|
|
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) ->
|
|
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.
|
|
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:
|
|
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
|
-
|
|
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
|
[](https://github.com/aimanmalib/crewforge/actions/workflows/ci.yml)
|
|
53
64
|
[](https://pypi.org/project/crewforge/)
|
|
54
|
-
](https://scorecard.dev/viewer/?uri=github.com/aimanmalib/crewforge)
|
|
66
|
+

|
|
55
67
|

|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
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.
|
|
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: `
|
|
432
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|