Add scripts for modal deployments
#1
by RCaz - opened
- .gitignore +0 -7
- AGENTS.md +0 -100
- README.md +4 -4
- TODO_ME.md +0 -11
- config/agent.yaml +0 -19
- crew2.py +52 -105
- dev.ipynb +0 -63
- doc/doc_crewai_agent.md +0 -669
- doc/doc_crewai_production.md +0 -145
- doc/doc_crewai_skills.md +0 -223
- doc/doc_crewai_tool.md +0 -256
- doc/doc_crewai_training.md +0 -173
- doc/doc_phoenix_tracing.md +0 -93
- requirements.txt +1 -2
- setup_env.sh +0 -78
- skills/gemma4-image-prompting.md +0 -30
- skills/semantic-clarification.md +0 -28
- tests/__init__.py +0 -0
- tests/test_app.py +0 -84
- tests/test_crew2.py +0 -207
- tests/test_flux_generator.py +0 -72
- tests/test_transcribe_generator.py +0 -104
- tests/test_unsloth_finetune.py +0 -201
- tests/test_vllm_inference.py +0 -91
- tests/test_voxcpm_generator.py +0 -92
- todo.md +0 -12
- transcribe_generator.py +0 -125
- vllm_inference.py +0 -165
- voxcpm_generator.py +0 -108
.gitignore
DELETED
|
@@ -1,7 +0,0 @@
|
|
| 1 |
-
.env
|
| 2 |
-
.DS_Store
|
| 3 |
-
__pycache__/
|
| 4 |
-
Outputs/
|
| 5 |
-
output.txt
|
| 6 |
-
*.png
|
| 7 |
-
*.wav
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
AGENTS.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
| 1 |
-
# CrewAI Hack — Agent Guide
|
| 2 |
-
|
| 3 |
-
## Env
|
| 4 |
-
|
| 5 |
-
- **Python**: `/Applications/anaconda3/envs/crewai_test/bin/python` (3.12). The `crewai_hack` env (3.14) breaks packages.
|
| 6 |
-
- **Setup**: `setup_env.sh` — a 6-step dance (`--no-deps` for conflicting openai, litellm, OTel, phoenix). `requirements.txt` lists only 8 packages; the full env is much larger.
|
| 7 |
-
- `.env` provides `SERPER_API_KEY` (loaded via `python-dotenv` in `crew2.py`). Duplicate key in file is harmless.
|
| 8 |
-
|
| 9 |
-
## Architecture
|
| 10 |
-
|
| 11 |
-
`crew2.py` is the main pipeline. 3 crews (all local, no Modal wrapper), then 3 direct HTTP calls to Modal endpoints:
|
| 12 |
-
|
| 13 |
-
1. **Crew 1** — Corroborative web research via `SerperDevTool`
|
| 14 |
-
2. **Crew 2** — Opposite/complementary web research
|
| 15 |
-
3. **Crew 3** — Synthesize both findings → `IMAGE PROMPT:` (parsed by `startswith("IMAGE PROMPT:")`)
|
| 16 |
-
4. `generate_image(prompt)` → Flux LoRA (A100)
|
| 17 |
-
5. `generate_caption(corroborate, opposite, prompt)` → direct vLLM call → `CAPTION:` + `VOICE_STYLE:`
|
| 18 |
-
6. `generate_voice(style, script)` → VoxCPM TTS (T4)
|
| 19 |
-
|
| 20 |
-
`app.py` is a Gradio UI entrypoint (`python app.py`).
|
| 21 |
-
|
| 22 |
-
## Commands
|
| 23 |
-
|
| 24 |
-
```sh
|
| 25 |
-
# Run full pipeline (CLI)
|
| 26 |
-
python crew2.py "your statement here"
|
| 27 |
-
python crew2.py --audio /path/to/speech.wav
|
| 28 |
-
|
| 29 |
-
# Run Gradio UI
|
| 30 |
-
python app.py
|
| 31 |
-
|
| 32 |
-
# Tests
|
| 33 |
-
python -m pytest tests/ # all 47 tests
|
| 34 |
-
python -m pytest tests/test_crew2.py # single file
|
| 35 |
-
python -m pytest -k "test_name" # focused
|
| 36 |
-
|
| 37 |
-
# Lint + format
|
| 38 |
-
python -m ruff check . # zero warnings expected
|
| 39 |
-
python -m ruff format . # idempotent
|
| 40 |
-
|
| 41 |
-
# Deploy Modal services
|
| 42 |
-
modal deploy vllm_inference.py # H200, slow cold start
|
| 43 |
-
modal deploy flux_generator.py # A100
|
| 44 |
-
modal deploy voxcpm_generator.py # T4
|
| 45 |
-
modal deploy transcribe_generator.py # T4
|
| 46 |
-
modal deploy unsloth_finetune.py # L40S
|
| 47 |
-
|
| 48 |
-
# Run Modal services standalone
|
| 49 |
-
modal run flux_generator.py --prompt "..." --steps 30
|
| 50 |
-
modal run voxcpm_generator.py --text "..." --voice-style girl
|
| 51 |
-
modal run transcribe_generator.py --audio-path /path/to/speech.wav
|
| 52 |
-
```
|
| 53 |
-
|
| 54 |
-
## LLM & Observability
|
| 55 |
-
|
| 56 |
-
- **LLM**: Gemma 4 26B via Modal vLLM (OpenAI-compatible). Base: `https://rcaz33--example-vllm-inference-serve.modal.run/v1`
|
| 57 |
-
- **Two model name constants** (swap them → 404):
|
| 58 |
-
- `_VLLM_MODEL = "openai/google/gemma-4-26B-A4B-it"` — LiteLLM prefix, for crewai `LLM()` objects
|
| 59 |
-
- `_VLLM_SERVED_MODEL = "google/gemma-4-26B-A4B-it"` — actual vLLM served name, for raw `httpx.post()` calls
|
| 60 |
-
- **Phoenix tracing**: configured in `crew2.py` — tries/except guarded (silently skips when pkgs not installed). Auth via `build_hf_headers()` (HF token). Endpoint: `https://RCaz-phoenix-arize-observability.hf.space/v1/traces`. Session tracking via `_using_session(session_id)`.
|
| 61 |
-
|
| 62 |
-
## Modal Services
|
| 63 |
-
|
| 64 |
-
|File|App Name|GPU|Endpoint|Purpose|
|
| 65 |
-
|---|---|---|---|---|
|
| 66 |
-
|`vllm_inference.py`|`example-vllm-inference`|H200|`/v1/chat/completions`|Gemma 4 LLM|
|
| 67 |
-
|`flux_generator.py`|`flux-image-generator`|A100|`POST /generate`|Flux LoRA image|
|
| 68 |
-
|`voxcpm_generator.py`|`voxcpm-generator`|T4|`POST /synthesize`|VoxCPM TTS|
|
| 69 |
-
|`transcribe_generator.py`|`cohere-transcriber`|T4|`POST /transcribe`|Cohere ASR|
|
| 70 |
-
|`unsloth_finetune.py`|`example-unsloth-finetune`|L40S|—|LoRA finetuning|
|
| 71 |
-
|
| 72 |
-
All use `@modal.fastapi_endpoint(method="POST")`. Call via `.get_web_url()` + `httpx.post` (NOT `.remote()`).
|
| 73 |
-
|
| 74 |
-
## Known Issues
|
| 75 |
-
|
| 76 |
-
- **Serper 403**: `SERPER_API_KEY` free quota exhausted. `SerperDevTool` hangs without surfacing the error. `_run_with_timeout()` wrapper (300s clock timeout) added as mitigation.
|
| 77 |
-
- **vLLM cold start**: H200 container scales down after 15 min idle. Next request takes 10–20 min.
|
| 78 |
-
- **Transcription 500**: cohere-transcriber needs `modal.Secret.from_name("huggingface")` with `HF_TOKEN` set. Create via `modal secret create huggingface HF_TOKEN=hf_...`.
|
| 79 |
-
- **Heavy ML deps not in test env**: `torch`, `diffusers`, `transformers`, `soundfile`, `librosa`, `datasets`, `voxcpm` only exist inside Modal containers. Tests mock via `sys.modules` + `@app.cls`/`@app.function` no‑op patches.
|
| 80 |
-
- **Flux LoRA adapter**: `CodeGoat24/FLUX.2-klein-base-9B-UnifiedReward-Flex-lora` is adapter-only (no `model_index.json`). `load_lora_weights()` needs `weight_name="pytorch_lora_weights.safetensors"`.
|
| 81 |
-
- **Modal image rebuild**: cached if hash unchanged. Add `.run_commands("echo <tag>")` to force rebuild.
|
| 82 |
-
|
| 83 |
-
## Testing
|
| 84 |
-
|
| 85 |
-
- 47 tests across 8 files in `tests/`, all pass, zero ruff warnings.
|
| 86 |
-
- Modal service classes: tested by patching `@app.cls` + `@app.function` as identity decorators, then testing the underlying Python class directly. Heavy imports mocked via `patch.dict("sys.modules", ...)`.
|
| 87 |
-
- Async tests use `@pytest.mark.asyncio` + `MockAsyncContextManager` for `async with`.
|
| 88 |
-
- No `pytest.ini` or `pyproject.toml` config — vanilla pytest.
|
| 89 |
-
|
| 90 |
-
## Skills
|
| 91 |
-
|
| 92 |
-
- `skills/gemma4-image-prompting.md` — photorealism, composition, lighting guidelines
|
| 93 |
-
- `skills/semantic-clarification.md` — ambiguity detection
|
| 94 |
-
|
| 95 |
-
## Code Style
|
| 96 |
-
|
| 97 |
-
- No comments in source code. Only `# ------` section headers allowed.
|
| 98 |
-
- Crew outputs parsed by exact `startswith("KEY:")` — output format must be exact.
|
| 99 |
-
- All Modal GPU imports happen inside `__init__` / function bodies (lazy loading).
|
| 100 |
-
- `ruff check .` and `ruff format .` — must pass before commit.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
README.md
CHANGED
|
@@ -1,15 +1,15 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 👀
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.18.0
|
| 8 |
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
-
short_description: '
|
| 13 |
---
|
| 14 |
|
| 15 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
|
|
|
| 1 |
---
|
| 2 |
+
title: Build Small Hackathon Thousand Tokens World
|
| 3 |
emoji: 👀
|
| 4 |
+
colorFrom: indigo
|
| 5 |
+
colorTo: pink
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.18.0
|
| 8 |
python_version: '3.13'
|
| 9 |
app_file: app.py
|
| 10 |
pinned: false
|
| 11 |
license: mit
|
| 12 |
+
short_description: 'Generate image/sound from text/audio '
|
| 13 |
---
|
| 14 |
|
| 15 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
TODO_ME.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
| 1 |
-
change the dataset for finetuning it uses mlabonne/FineTome-100k instead of European commission
|
| 2 |
-
|
| 3 |
-
check the tracing of the crew
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
add human feedback : i like / regenerate
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
use cohere transcribe to get input by voice
|
| 10 |
-
|
| 11 |
-
https://huggingface.co/CohereLabs/cohere-transcribe-03-2026
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
config/agent.yaml
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
researcher:
|
| 2 |
-
role: >
|
| 3 |
-
{topic} Senior Data Researcher
|
| 4 |
-
goal: >
|
| 5 |
-
Uncover cutting-edge developments in {topic}
|
| 6 |
-
backstory: >
|
| 7 |
-
You're a seasoned researcher with a knack for uncovering the latest
|
| 8 |
-
developments in {topic}. Known for your ability to find the most relevant
|
| 9 |
-
information and present it in a clear and concise manner.
|
| 10 |
-
|
| 11 |
-
reporting_analyst:
|
| 12 |
-
role: >
|
| 13 |
-
{topic} Reporting Analyst
|
| 14 |
-
goal: >
|
| 15 |
-
Create detailed reports based on {topic} data analysis and research findings
|
| 16 |
-
backstory: >
|
| 17 |
-
You're a meticulous analyst with a keen eye for detail. You're known for
|
| 18 |
-
your ability to turn complex data into clear and concise reports, making
|
| 19 |
-
it easy for others to understand and act on the information you provide.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
crew2.py
CHANGED
|
@@ -1,8 +1,7 @@
|
|
| 1 |
import concurrent.futures
|
| 2 |
-
import contextvars
|
| 3 |
import sys
|
| 4 |
import uuid
|
| 5 |
-
from contextlib import contextmanager
|
| 6 |
from dotenv import load_dotenv
|
| 7 |
|
| 8 |
from crewai import Agent, Crew, LLM, Process, Task
|
|
@@ -15,19 +14,15 @@ load_dotenv()
|
|
| 15 |
try:
|
| 16 |
from openinference.instrumentation import using_session as _using_session
|
| 17 |
except ImportError:
|
| 18 |
-
|
| 19 |
@contextmanager
|
| 20 |
def _using_session(session_id): # noqa: F811
|
| 21 |
yield
|
| 22 |
|
| 23 |
-
|
| 24 |
try:
|
| 25 |
from openinference.semconv.trace import SpanAttributes
|
| 26 |
except ImportError:
|
| 27 |
-
|
| 28 |
class _SpanAttributes:
|
| 29 |
SESSION_ID = "session_id"
|
| 30 |
-
|
| 31 |
SpanAttributes = _SpanAttributes() # type: ignore
|
| 32 |
|
| 33 |
# ------ Phoenix / OpenTelemetry tracing ------
|
|
@@ -35,11 +30,12 @@ except ImportError:
|
|
| 35 |
try:
|
| 36 |
from phoenix.otel import register
|
| 37 |
from huggingface_hub.utils import build_hf_headers
|
| 38 |
-
from openinference.instrumentation.
|
| 39 |
|
| 40 |
from opentelemetry import trace
|
| 41 |
from openinference.instrumentation.crewai import CrewAIInstrumentor
|
| 42 |
|
|
|
|
| 43 |
tp = trace.get_tracer_provider()
|
| 44 |
if tp and hasattr(tp, "shutdown"):
|
| 45 |
tp.shutdown()
|
|
@@ -50,11 +46,9 @@ try:
|
|
| 50 |
endpoint="https://RCaz-phoenix-arize-observability.hf.space/v1/traces",
|
| 51 |
headers=build_hf_headers(),
|
| 52 |
)
|
| 53 |
-
LiteLLMInstrumentor().instrument(tracer_provider=tracer_provider)
|
| 54 |
CrewAIInstrumentor().instrument(tracer_provider=tracer_provider)
|
| 55 |
-
tracer = trace.get_tracer("crewai")
|
| 56 |
except ImportError:
|
| 57 |
-
|
| 58 |
|
| 59 |
|
| 60 |
###### The agentic app
|
|
@@ -116,21 +110,18 @@ def _get_transcribe_url() -> str:
|
|
| 116 |
|
| 117 |
|
| 118 |
CREW_TIMEOUT = 300 # seconds per crew
|
| 119 |
-
_POOL = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
| 120 |
|
| 121 |
|
| 122 |
def _run_with_timeout(fn, *, timeout=CREW_TIMEOUT):
|
| 123 |
-
"""Run a callable with a hard timeout. Returns result or fallback str.
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
print(f"\n⚠ Crew timed out after {timeout}s — using fallback\n")
|
| 133 |
-
return "The search timed out. Using general knowledge as fallback."
|
| 134 |
|
| 135 |
|
| 136 |
# ------ Research Pipeline ------
|
|
@@ -241,7 +232,7 @@ Two research crews investigated it. Here are their findings:
|
|
| 241 |
{result_opposite}
|
| 242 |
|
| 243 |
Your job:
|
| 244 |
-
Create a highly detailed visual scene description that captures
|
| 245 |
|
| 246 |
Output exactly in this format:
|
| 247 |
|
|
@@ -290,26 +281,21 @@ def generate_image(prompt: str) -> bytes:
|
|
| 290 |
|
| 291 |
import httpx as _httpx
|
| 292 |
|
| 293 |
-
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
)
|
| 303 |
-
resp.raise_for_status()
|
| 304 |
-
return resp.content
|
| 305 |
|
| 306 |
|
| 307 |
# ------ Caption Generation (direct LLM call with image prompt context) ------
|
| 308 |
|
| 309 |
|
| 310 |
-
def generate_caption(
|
| 311 |
-
corroborate: str, opposite: str, image_prompt: str, session_id: str | None = None
|
| 312 |
-
) -> dict:
|
| 313 |
"""
|
| 314 |
Send the research + image prompt to Gemma 4 and get a 30-second
|
| 315 |
spoken caption + voice style back.
|
|
@@ -357,34 +343,15 @@ VOICE_STYLE: <voice style description>""",
|
|
| 357 |
}
|
| 358 |
|
| 359 |
with _using_session(session_id):
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
|
|
|
| 364 |
)
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
json=payload,
|
| 369 |
-
headers={"Authorization": "Bearer sk-dummy-key-not-needed"},
|
| 370 |
-
timeout=300,
|
| 371 |
-
)
|
| 372 |
-
resp.raise_for_status()
|
| 373 |
-
body = resp.json()
|
| 374 |
-
text = body["choices"][0]["message"]["content"]
|
| 375 |
-
|
| 376 |
-
if tracer and "usage" in body:
|
| 377 |
-
usage = body["usage"]
|
| 378 |
-
_span.set_attribute(
|
| 379 |
-
"llm.token_count.prompt", usage.get("prompt_tokens", 0)
|
| 380 |
-
)
|
| 381 |
-
_span.set_attribute(
|
| 382 |
-
"llm.token_count.completion", usage.get("completion_tokens", 0)
|
| 383 |
-
)
|
| 384 |
-
_span.set_attribute(
|
| 385 |
-
"llm.token_count.total", usage.get("total_tokens", 0)
|
| 386 |
-
)
|
| 387 |
-
_span.set_attribute("llm.model_name", _VLLM_SERVED_MODEL)
|
| 388 |
|
| 389 |
print(f"Gemma response:\n{text}\n")
|
| 390 |
|
|
@@ -402,9 +369,7 @@ VOICE_STYLE: <voice style description>""",
|
|
| 402 |
# ------ Voice Generation (VoxCPM on Modal) ------
|
| 403 |
|
| 404 |
|
| 405 |
-
def generate_voice(
|
| 406 |
-
voice_style: str, voice_script: str, session_id: str | None = None
|
| 407 |
-
) -> bytes:
|
| 408 |
"""Send script to VoxCPM on Modal T4, return WAV bytes."""
|
| 409 |
if session_id is None:
|
| 410 |
session_id = str(uuid.uuid4())
|
|
@@ -416,20 +381,15 @@ def generate_voice(
|
|
| 416 |
import httpx as _httpx
|
| 417 |
|
| 418 |
with _using_session(session_id):
|
| 419 |
-
|
| 420 |
-
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
|
| 424 |
-
|
| 425 |
-
|
| 426 |
-
|
| 427 |
-
|
| 428 |
-
timeout=600,
|
| 429 |
-
follow_redirects=True,
|
| 430 |
-
)
|
| 431 |
-
resp.raise_for_status()
|
| 432 |
-
return resp.content
|
| 433 |
|
| 434 |
|
| 435 |
# ------ Audio Transcription (Cohere Transcribe on Modal) ------
|
|
@@ -445,19 +405,14 @@ def transcribe_audio(audio_path: str, session_id: str | None = None) -> str:
|
|
| 445 |
url = _get_transcribe_url()
|
| 446 |
|
| 447 |
with _using_session(session_id):
|
| 448 |
-
with (
|
| 449 |
-
|
| 450 |
-
if tracer
|
| 451 |
-
else nullcontext()
|
| 452 |
-
):
|
| 453 |
-
with open(audio_path, "rb") as f:
|
| 454 |
-
audio_b64 = base64.b64encode(f.read()).decode()
|
| 455 |
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
|
| 460 |
-
|
| 461 |
|
| 462 |
|
| 463 |
# ------ CLI Entry Point ------
|
|
@@ -506,9 +461,7 @@ if __name__ == "__main__":
|
|
| 506 |
caption_result = None
|
| 507 |
if prompt and corroborate and opposite:
|
| 508 |
try:
|
| 509 |
-
caption_result = generate_caption(
|
| 510 |
-
corroborate, opposite, prompt, session_id=session_id
|
| 511 |
-
)
|
| 512 |
if caption_result["caption"]:
|
| 513 |
print(f"\n✓ Caption: {caption_result['caption']}")
|
| 514 |
if caption_result["voice_style"]:
|
|
@@ -519,16 +472,10 @@ if __name__ == "__main__":
|
|
| 519 |
print("(Skipping caption generation — missing prompt or research)")
|
| 520 |
|
| 521 |
# Generate voice
|
| 522 |
-
if
|
| 523 |
-
caption_result
|
| 524 |
-
and caption_result["voice_style"]
|
| 525 |
-
and caption_result["caption"]
|
| 526 |
-
):
|
| 527 |
try:
|
| 528 |
audio_bytes = generate_voice(
|
| 529 |
-
caption_result["voice_style"],
|
| 530 |
-
caption_result["caption"],
|
| 531 |
-
session_id=session_id,
|
| 532 |
)
|
| 533 |
filename = "crew_voice_output.wav"
|
| 534 |
with open(filename, "wb") as f:
|
|
|
|
| 1 |
import concurrent.futures
|
|
|
|
| 2 |
import sys
|
| 3 |
import uuid
|
| 4 |
+
from contextlib import contextmanager
|
| 5 |
from dotenv import load_dotenv
|
| 6 |
|
| 7 |
from crewai import Agent, Crew, LLM, Process, Task
|
|
|
|
| 14 |
try:
|
| 15 |
from openinference.instrumentation import using_session as _using_session
|
| 16 |
except ImportError:
|
|
|
|
| 17 |
@contextmanager
|
| 18 |
def _using_session(session_id): # noqa: F811
|
| 19 |
yield
|
| 20 |
|
|
|
|
| 21 |
try:
|
| 22 |
from openinference.semconv.trace import SpanAttributes
|
| 23 |
except ImportError:
|
|
|
|
| 24 |
class _SpanAttributes:
|
| 25 |
SESSION_ID = "session_id"
|
|
|
|
| 26 |
SpanAttributes = _SpanAttributes() # type: ignore
|
| 27 |
|
| 28 |
# ------ Phoenix / OpenTelemetry tracing ------
|
|
|
|
| 30 |
try:
|
| 31 |
from phoenix.otel import register
|
| 32 |
from huggingface_hub.utils import build_hf_headers
|
| 33 |
+
from openinference.instrumentation.openai import OpenAIInstrumentor
|
| 34 |
|
| 35 |
from opentelemetry import trace
|
| 36 |
from openinference.instrumentation.crewai import CrewAIInstrumentor
|
| 37 |
|
| 38 |
+
OpenAIInstrumentor().uninstrument()
|
| 39 |
tp = trace.get_tracer_provider()
|
| 40 |
if tp and hasattr(tp, "shutdown"):
|
| 41 |
tp.shutdown()
|
|
|
|
| 46 |
endpoint="https://RCaz-phoenix-arize-observability.hf.space/v1/traces",
|
| 47 |
headers=build_hf_headers(),
|
| 48 |
)
|
|
|
|
| 49 |
CrewAIInstrumentor().instrument(tracer_provider=tracer_provider)
|
|
|
|
| 50 |
except ImportError:
|
| 51 |
+
pass
|
| 52 |
|
| 53 |
|
| 54 |
###### The agentic app
|
|
|
|
| 110 |
|
| 111 |
|
| 112 |
CREW_TIMEOUT = 300 # seconds per crew
|
|
|
|
| 113 |
|
| 114 |
|
| 115 |
def _run_with_timeout(fn, *, timeout=CREW_TIMEOUT):
|
| 116 |
+
"""Run a callable with a hard timeout. Returns result or fallback str."""
|
| 117 |
+
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
| 118 |
+
fut = pool.submit(fn)
|
| 119 |
+
try:
|
| 120 |
+
return fut.result(timeout=timeout)
|
| 121 |
+
except concurrent.futures.TimeoutError:
|
| 122 |
+
print(f"\n⚠ Crew timed out after {timeout}s — using fallback\n")
|
| 123 |
+
fut.cancel()
|
| 124 |
+
return "The search timed out. Using general knowledge as fallback."
|
|
|
|
|
|
|
| 125 |
|
| 126 |
|
| 127 |
# ------ Research Pipeline ------
|
|
|
|
| 232 |
{result_opposite}
|
| 233 |
|
| 234 |
Your job:
|
| 235 |
+
Create a highly detailed visual scene description that captures the dialogue, contrast, or tension between these two perspectives. Describe the composition, colors, lighting, mood, subjects, setting, and visual metaphor in vivid detail — as if instructing an artist or image generation model.
|
| 236 |
|
| 237 |
Output exactly in this format:
|
| 238 |
|
|
|
|
| 281 |
|
| 282 |
import httpx as _httpx
|
| 283 |
|
| 284 |
+
url = _get_flux_url()
|
| 285 |
+
resp = _httpx.post(
|
| 286 |
+
url,
|
| 287 |
+
json={"prompt": prompt, "steps": 30},
|
| 288 |
+
timeout=600,
|
| 289 |
+
follow_redirects=True,
|
| 290 |
+
)
|
| 291 |
+
resp.raise_for_status()
|
| 292 |
+
return resp.content
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
|
| 295 |
# ------ Caption Generation (direct LLM call with image prompt context) ------
|
| 296 |
|
| 297 |
|
| 298 |
+
def generate_caption(corroborate: str, opposite: str, image_prompt: str, session_id: str | None = None) -> dict:
|
|
|
|
|
|
|
| 299 |
"""
|
| 300 |
Send the research + image prompt to Gemma 4 and get a 30-second
|
| 301 |
spoken caption + voice style back.
|
|
|
|
| 343 |
}
|
| 344 |
|
| 345 |
with _using_session(session_id):
|
| 346 |
+
resp = _httpx.post(
|
| 347 |
+
f"{_VLLM_BASE_URL}/chat/completions",
|
| 348 |
+
json=payload,
|
| 349 |
+
headers={"Authorization": "Bearer sk-dummy-key-not-needed"},
|
| 350 |
+
timeout=300,
|
| 351 |
)
|
| 352 |
+
resp.raise_for_status()
|
| 353 |
+
body = resp.json()
|
| 354 |
+
text = body["choices"][0]["message"]["content"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 355 |
|
| 356 |
print(f"Gemma response:\n{text}\n")
|
| 357 |
|
|
|
|
| 369 |
# ------ Voice Generation (VoxCPM on Modal) ------
|
| 370 |
|
| 371 |
|
| 372 |
+
def generate_voice(voice_style: str, voice_script: str, session_id: str | None = None) -> bytes:
|
|
|
|
|
|
|
| 373 |
"""Send script to VoxCPM on Modal T4, return WAV bytes."""
|
| 374 |
if session_id is None:
|
| 375 |
session_id = str(uuid.uuid4())
|
|
|
|
| 381 |
import httpx as _httpx
|
| 382 |
|
| 383 |
with _using_session(session_id):
|
| 384 |
+
url = _get_vox_url()
|
| 385 |
+
resp = _httpx.post(
|
| 386 |
+
url,
|
| 387 |
+
json={"text": voice_script, "voice_style": voice_style},
|
| 388 |
+
timeout=600,
|
| 389 |
+
follow_redirects=True,
|
| 390 |
+
)
|
| 391 |
+
resp.raise_for_status()
|
| 392 |
+
return resp.content
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
|
| 395 |
# ------ Audio Transcription (Cohere Transcribe on Modal) ------
|
|
|
|
| 405 |
url = _get_transcribe_url()
|
| 406 |
|
| 407 |
with _using_session(session_id):
|
| 408 |
+
with open(audio_path, "rb") as f:
|
| 409 |
+
audio_b64 = base64.b64encode(f.read()).decode()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 410 |
|
| 411 |
+
resp = _httpx.post(url, json={"audio": audio_b64}, timeout=600)
|
| 412 |
+
resp.raise_for_status()
|
| 413 |
+
text = resp.json()["transcription"]
|
| 414 |
+
print(f'Transcribed: "{text}"\n')
|
| 415 |
+
return text
|
| 416 |
|
| 417 |
|
| 418 |
# ------ CLI Entry Point ------
|
|
|
|
| 461 |
caption_result = None
|
| 462 |
if prompt and corroborate and opposite:
|
| 463 |
try:
|
| 464 |
+
caption_result = generate_caption(corroborate, opposite, prompt, session_id=session_id)
|
|
|
|
|
|
|
| 465 |
if caption_result["caption"]:
|
| 466 |
print(f"\n✓ Caption: {caption_result['caption']}")
|
| 467 |
if caption_result["voice_style"]:
|
|
|
|
| 472 |
print("(Skipping caption generation — missing prompt or research)")
|
| 473 |
|
| 474 |
# Generate voice
|
| 475 |
+
if caption_result and caption_result["voice_style"] and caption_result["caption"]:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 476 |
try:
|
| 477 |
audio_bytes = generate_voice(
|
| 478 |
+
caption_result["voice_style"], caption_result["caption"], session_id=session_id
|
|
|
|
|
|
|
| 479 |
)
|
| 480 |
filename = "crew_voice_output.wav"
|
| 481 |
with open(filename, "wb") as f:
|
dev.ipynb
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
{
|
| 2 |
-
"cells": [
|
| 3 |
-
{
|
| 4 |
-
"cell_type": "markdown",
|
| 5 |
-
"id": "91ed2fdf",
|
| 6 |
-
"metadata": {},
|
| 7 |
-
"source": [
|
| 8 |
-
"# Setp up"
|
| 9 |
-
]
|
| 10 |
-
},
|
| 11 |
-
{
|
| 12 |
-
"cell_type": "code",
|
| 13 |
-
"execution_count": null,
|
| 14 |
-
"id": "dbc83c2b",
|
| 15 |
-
"metadata": {
|
| 16 |
-
"vscode": {
|
| 17 |
-
"languageId": "plaintext"
|
| 18 |
-
}
|
| 19 |
-
},
|
| 20 |
-
"outputs": [],
|
| 21 |
-
"source": [
|
| 22 |
-
"!conda activate crewai"
|
| 23 |
-
]
|
| 24 |
-
},
|
| 25 |
-
{
|
| 26 |
-
"cell_type": "code",
|
| 27 |
-
"execution_count": null,
|
| 28 |
-
"id": "f39481c7",
|
| 29 |
-
"metadata": {
|
| 30 |
-
"vscode": {
|
| 31 |
-
"languageId": "plaintext"
|
| 32 |
-
}
|
| 33 |
-
},
|
| 34 |
-
"outputs": [],
|
| 35 |
-
"source": [
|
| 36 |
-
"!pip install -q openinference-instrumentation-crewai crewai crewai-tools crewai[litellm]"
|
| 37 |
-
]
|
| 38 |
-
},
|
| 39 |
-
{
|
| 40 |
-
"cell_type": "markdown",
|
| 41 |
-
"id": "190587d1",
|
| 42 |
-
"metadata": {},
|
| 43 |
-
"source": [
|
| 44 |
-
"# Define the crew"
|
| 45 |
-
]
|
| 46 |
-
},
|
| 47 |
-
{
|
| 48 |
-
"cell_type": "markdown",
|
| 49 |
-
"id": "81fb47d5",
|
| 50 |
-
"metadata": {},
|
| 51 |
-
"source": [
|
| 52 |
-
"# Register Phoenix tracer provider and instrument the application code"
|
| 53 |
-
]
|
| 54 |
-
}
|
| 55 |
-
],
|
| 56 |
-
"metadata": {
|
| 57 |
-
"language_info": {
|
| 58 |
-
"name": "python"
|
| 59 |
-
}
|
| 60 |
-
},
|
| 61 |
-
"nbformat": 4,
|
| 62 |
-
"nbformat_minor": 5
|
| 63 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc/doc_crewai_agent.md
DELETED
|
@@ -1,669 +0,0 @@
|
|
| 1 |
-
Core Concepts
|
| 2 |
-
Agents
|
| 3 |
-
|
| 4 |
-
Detailed guide on creating and managing agents within the CrewAI framework.
|
| 5 |
-
|
| 6 |
-
Overview of an Agent
|
| 7 |
-
In the CrewAI framework, an Agent is an autonomous unit that can:
|
| 8 |
-
|
| 9 |
-
Perform specific tasks
|
| 10 |
-
Make decisions based on its role and goal
|
| 11 |
-
Use tools to accomplish objectives
|
| 12 |
-
Communicate and collaborate with other agents
|
| 13 |
-
Maintain memory of interactions
|
| 14 |
-
Delegate tasks when allowed
|
| 15 |
-
|
| 16 |
-
Think of an agent as a specialized team member with specific skills, expertise, and responsibilities. For example, a Researcher agent might excel at gathering and analyzing information, while a Writer agent might be better at creating content.
|
| 17 |
-
CrewAI AMP includes a Visual Agent Builder that simplifies agent creation and configuration without writing code. Design your agents visually and test them in real-time.Visual Agent Builder ScreenshotThe Visual Agent Builder enables:
|
| 18 |
-
|
| 19 |
-
Intuitive agent configuration with form-based interfaces
|
| 20 |
-
Real-time testing and validation
|
| 21 |
-
Template library with pre-configured agent types
|
| 22 |
-
Easy customization of agent attributes and behaviors
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
Agent Attributes
|
| 26 |
-
Attribute Parameter Type Description
|
| 27 |
-
Role role str Defines the agent’s function and expertise within the crew.
|
| 28 |
-
Goal goal str The individual objective that guides the agent’s decision-making.
|
| 29 |
-
Backstory backstory str Provides context and personality to the agent, enriching interactions.
|
| 30 |
-
LLM (optional) llm Union[str, LLM, Any] Language model that powers the agent. Defaults to the model specified in OPENAI_MODEL_NAME or “gpt-4”.
|
| 31 |
-
Tools (optional) tools List[BaseTool] Capabilities or functions available to the agent. Defaults to an empty list.
|
| 32 |
-
Function Calling LLM (optional) function_calling_llm Optional[Any] Language model for tool calling, overrides crew’s LLM if specified.
|
| 33 |
-
Max Iterations (optional) max_iter int Maximum iterations before the agent must provide its best answer. Default is 20.
|
| 34 |
-
Max RPM (optional) max_rpm Optional[int] Maximum requests per minute to avoid rate limits.
|
| 35 |
-
Max Execution Time (optional) max_execution_time Optional[int] Maximum time (in seconds) for task execution.
|
| 36 |
-
Verbose (optional) verbose bool Enable detailed execution logs for debugging. Default is False.
|
| 37 |
-
Allow Delegation (optional) allow_delegation bool Allow the agent to delegate tasks to other agents. Default is False.
|
| 38 |
-
Step Callback (optional) step_callback Optional[Any] Function called after each agent step, overrides crew callback.
|
| 39 |
-
Cache (optional) cache bool Enable caching for tool usage. Default is True.
|
| 40 |
-
System Template (optional) system_template Optional[str] Custom system prompt template for the agent.
|
| 41 |
-
Prompt Template (optional) prompt_template Optional[str] Custom prompt template for the agent.
|
| 42 |
-
Response Template (optional) response_template Optional[str] Custom response template for the agent.
|
| 43 |
-
Allow Code Execution (optional) allow_code_execution Optional[bool] Enable code execution for the agent. Default is False.
|
| 44 |
-
Max Retry Limit (optional) max_retry_limit int Maximum number of retries when an error occurs. Default is 2.
|
| 45 |
-
Respect Context Window (optional) respect_context_window bool Keep messages under context window size by summarizing. Default is True.
|
| 46 |
-
Code Execution Mode (optional) code_execution_mode Literal["safe", "unsafe"] Mode for code execution: ‘safe’ (using Docker) or ‘unsafe’ (direct). Default is ‘safe’.
|
| 47 |
-
Multimodal (optional) multimodal bool Whether the agent supports multimodal capabilities. Default is False.
|
| 48 |
-
Inject Date (optional) inject_date bool Whether to automatically inject the current date into tasks. Default is False.
|
| 49 |
-
Date Format (optional) date_format str Format string for date when inject_date is enabled. Default is “%Y-%m-%d” (ISO format).
|
| 50 |
-
Reasoning (optional) reasoning bool Whether the agent should reflect and create a plan before executing a task. Default is False.
|
| 51 |
-
Max Reasoning Attempts (optional) max_reasoning_attempts Optional[int] Maximum number of reasoning attempts before executing the task. If None, will try until ready.
|
| 52 |
-
Embedder (optional) embedder Optional[Dict[str, Any]] Configuration for the embedder used by the agent.
|
| 53 |
-
Knowledge Sources (optional) knowledge_sources Optional[List[BaseKnowledgeSource]] Knowledge sources available to the agent.
|
| 54 |
-
Use System Prompt (optional) use_system_prompt Optional[bool] Whether to use system prompt (for o1 model support). Default is True.
|
| 55 |
-
|
| 56 |
-
Creating Agents
|
| 57 |
-
There are two ways to create agents in CrewAI: using YAML configuration (recommended) or defining them directly in code.
|
| 58 |
-
|
| 59 |
-
YAML Configuration (Recommended)
|
| 60 |
-
Using YAML configuration provides a cleaner, more maintainable way to define agents. We strongly recommend using this approach in your CrewAI projects. After creating your CrewAI project as outlined in the Installation section, navigate to the src/latest_ai_development/config/agents.yaml file and modify the template to match your requirements.
|
| 61 |
-
Variables in your YAML files (like {topic}) will be replaced with values from your inputs when running the crew:
|
| 62 |
-
Code
|
| 63 |
-
|
| 64 |
-
crew.kickoff(inputs={'topic': 'AI Agents'})
|
| 65 |
-
|
| 66 |
-
Here’s an example of how to configure agents using YAML:
|
| 67 |
-
agents.yaml
|
| 68 |
-
|
| 69 |
-
# src/latest_ai_development/config/agents.yaml
|
| 70 |
-
researcher:
|
| 71 |
-
role: >
|
| 72 |
-
{topic} Senior Data Researcher
|
| 73 |
-
goal: >
|
| 74 |
-
Uncover cutting-edge developments in {topic}
|
| 75 |
-
backstory: >
|
| 76 |
-
You're a seasoned researcher with a knack for uncovering the latest
|
| 77 |
-
developments in {topic}. Known for your ability to find the most relevant
|
| 78 |
-
information and present it in a clear and concise manner.
|
| 79 |
-
|
| 80 |
-
reporting_analyst:
|
| 81 |
-
role: >
|
| 82 |
-
{topic} Reporting Analyst
|
| 83 |
-
goal: >
|
| 84 |
-
Create detailed reports based on {topic} data analysis and research findings
|
| 85 |
-
backstory: >
|
| 86 |
-
You're a meticulous analyst with a keen eye for detail. You're known for
|
| 87 |
-
your ability to turn complex data into clear and concise reports, making
|
| 88 |
-
it easy for others to understand and act on the information you provide.
|
| 89 |
-
|
| 90 |
-
To use this YAML configuration in your code, create a crew class that inherits from CrewBase:
|
| 91 |
-
Code
|
| 92 |
-
|
| 93 |
-
# src/latest_ai_development/crew.py
|
| 94 |
-
from crewai import Agent, Crew, Process
|
| 95 |
-
from crewai.project import CrewBase, agent, crew
|
| 96 |
-
from crewai_tools import SerperDevTool
|
| 97 |
-
|
| 98 |
-
@CrewBase
|
| 99 |
-
class LatestAiDevelopmentCrew():
|
| 100 |
-
"""LatestAiDevelopment crew"""
|
| 101 |
-
|
| 102 |
-
agents_config = "config/agents.yaml"
|
| 103 |
-
|
| 104 |
-
@agent
|
| 105 |
-
def researcher(self) -> Agent:
|
| 106 |
-
return Agent(
|
| 107 |
-
config=self.agents_config['researcher'], # type: ignore[index]
|
| 108 |
-
verbose=True,
|
| 109 |
-
tools=[SerperDevTool()]
|
| 110 |
-
)
|
| 111 |
-
|
| 112 |
-
@agent
|
| 113 |
-
def reporting_analyst(self) -> Agent:
|
| 114 |
-
return Agent(
|
| 115 |
-
config=self.agents_config['reporting_analyst'], # type: ignore[index]
|
| 116 |
-
verbose=True
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
The names you use in your YAML files (agents.yaml) should match the method names in your Python code.
|
| 120 |
-
|
| 121 |
-
Direct Code Definition
|
| 122 |
-
You can create agents directly in code by instantiating the Agent class. Here’s a comprehensive example showing all available parameters:
|
| 123 |
-
Code
|
| 124 |
-
|
| 125 |
-
from crewai import Agent
|
| 126 |
-
from crewai_tools import SerperDevTool
|
| 127 |
-
|
| 128 |
-
# Create an agent with all available parameters
|
| 129 |
-
agent = Agent(
|
| 130 |
-
role="Senior Data Scientist",
|
| 131 |
-
goal="Analyze and interpret complex datasets to provide actionable insights",
|
| 132 |
-
backstory="With over 10 years of experience in data science and machine learning, "
|
| 133 |
-
"you excel at finding patterns in complex datasets.",
|
| 134 |
-
llm="gpt-4", # Default: OPENAI_MODEL_NAME or "gpt-4"
|
| 135 |
-
function_calling_llm=None, # Optional: Separate LLM for tool calling
|
| 136 |
-
verbose=False, # Default: False
|
| 137 |
-
allow_delegation=False, # Default: False
|
| 138 |
-
max_iter=20, # Default: 20 iterations
|
| 139 |
-
max_rpm=None, # Optional: Rate limit for API calls
|
| 140 |
-
max_execution_time=None, # Optional: Maximum execution time in seconds
|
| 141 |
-
max_retry_limit=2, # Default: 2 retries on error
|
| 142 |
-
allow_code_execution=False, # Default: False
|
| 143 |
-
code_execution_mode="safe", # Default: "safe" (options: "safe", "unsafe")
|
| 144 |
-
respect_context_window=True, # Default: True
|
| 145 |
-
use_system_prompt=True, # Default: True
|
| 146 |
-
multimodal=False, # Default: False
|
| 147 |
-
inject_date=False, # Default: False
|
| 148 |
-
date_format="%Y-%m-%d", # Default: ISO format
|
| 149 |
-
reasoning=False, # Default: False
|
| 150 |
-
max_reasoning_attempts=None, # Default: None
|
| 151 |
-
tools=[SerperDevTool()], # Optional: List of tools
|
| 152 |
-
knowledge_sources=None, # Optional: List of knowledge sources
|
| 153 |
-
embedder=None, # Optional: Custom embedder configuration
|
| 154 |
-
system_template=None, # Optional: Custom system prompt template
|
| 155 |
-
prompt_template=None, # Optional: Custom prompt template
|
| 156 |
-
response_template=None, # Optional: Custom response template
|
| 157 |
-
step_callback=None, # Optional: Callback function for monitoring
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
Let’s break down some key parameter combinations for common use cases:
|
| 161 |
-
|
| 162 |
-
Basic Research Agent
|
| 163 |
-
Code
|
| 164 |
-
|
| 165 |
-
research_agent = Agent(
|
| 166 |
-
role="Research Analyst",
|
| 167 |
-
goal="Find and summarize information about specific topics",
|
| 168 |
-
backstory="You are an experienced researcher with attention to detail",
|
| 169 |
-
tools=[SerperDevTool()],
|
| 170 |
-
verbose=True # Enable logging for debugging
|
| 171 |
-
)
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
Code Development Agent
|
| 175 |
-
Code
|
| 176 |
-
|
| 177 |
-
dev_agent = Agent(
|
| 178 |
-
role="Senior Python Developer",
|
| 179 |
-
goal="Write and debug Python code",
|
| 180 |
-
backstory="Expert Python developer with 10 years of experience",
|
| 181 |
-
allow_code_execution=True,
|
| 182 |
-
code_execution_mode="safe", # Uses Docker for safety
|
| 183 |
-
max_execution_time=300, # 5-minute timeout
|
| 184 |
-
max_retry_limit=3 # More retries for complex code tasks
|
| 185 |
-
)
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
Long-Running Analysis Agent
|
| 189 |
-
Code
|
| 190 |
-
|
| 191 |
-
analysis_agent = Agent(
|
| 192 |
-
role="Data Analyst",
|
| 193 |
-
goal="Perform deep analysis of large datasets",
|
| 194 |
-
backstory="Specialized in big data analysis and pattern recognition",
|
| 195 |
-
memory=True,
|
| 196 |
-
respect_context_window=True,
|
| 197 |
-
max_rpm=10, # Limit API calls
|
| 198 |
-
function_calling_llm="gpt-4o-mini" # Cheaper model for tool calls
|
| 199 |
-
)
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
Custom Template Agent
|
| 203 |
-
Code
|
| 204 |
-
|
| 205 |
-
custom_agent = Agent(
|
| 206 |
-
role="Customer Service Representative",
|
| 207 |
-
goal="Assist customers with their inquiries",
|
| 208 |
-
backstory="Experienced in customer support with a focus on satisfaction",
|
| 209 |
-
system_template="""<|start_header_id|>system<|end_header_id|>
|
| 210 |
-
{{ .System }}<|eot_id|>""",
|
| 211 |
-
prompt_template="""<|start_header_id|>user<|end_header_id|>
|
| 212 |
-
{{ .Prompt }}<|eot_id|>""",
|
| 213 |
-
response_template="""<|start_header_id|>assistant<|end_header_id|>
|
| 214 |
-
{{ .Response }}<|eot_id|>""",
|
| 215 |
-
)
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
Date-Aware Agent with Reasoning
|
| 219 |
-
Code
|
| 220 |
-
|
| 221 |
-
strategic_agent = Agent(
|
| 222 |
-
role="Market Analyst",
|
| 223 |
-
goal="Track market movements with precise date references and strategic planning",
|
| 224 |
-
backstory="Expert in time-sensitive financial analysis and strategic reporting",
|
| 225 |
-
inject_date=True, # Automatically inject current date into tasks
|
| 226 |
-
date_format="%B %d, %Y", # Format as "May 21, 2025"
|
| 227 |
-
reasoning=True, # Enable strategic planning
|
| 228 |
-
max_reasoning_attempts=2, # Limit planning iterations
|
| 229 |
-
verbose=True
|
| 230 |
-
)
|
| 231 |
-
|
| 232 |
-
|
| 233 |
-
Reasoning Agent
|
| 234 |
-
Code
|
| 235 |
-
|
| 236 |
-
reasoning_agent = Agent(
|
| 237 |
-
role="Strategic Planner",
|
| 238 |
-
goal="Analyze complex problems and create detailed execution plans",
|
| 239 |
-
backstory="Expert strategic planner who methodically breaks down complex challenges",
|
| 240 |
-
reasoning=True, # Enable reasoning and planning
|
| 241 |
-
max_reasoning_attempts=3, # Limit reasoning attempts
|
| 242 |
-
max_iter=30, # Allow more iterations for complex planning
|
| 243 |
-
verbose=True
|
| 244 |
-
)
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
Multimodal Agent
|
| 248 |
-
Code
|
| 249 |
-
|
| 250 |
-
multimodal_agent = Agent(
|
| 251 |
-
role="Visual Content Analyst",
|
| 252 |
-
goal="Analyze and process both text and visual content",
|
| 253 |
-
backstory="Specialized in multimodal analysis combining text and image understanding",
|
| 254 |
-
multimodal=True, # Enable multimodal capabilities
|
| 255 |
-
verbose=True
|
| 256 |
-
)
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
Parameter Details
|
| 260 |
-
|
| 261 |
-
Critical Parameters
|
| 262 |
-
|
| 263 |
-
role, goal, and backstory are required and shape the agent’s behavior
|
| 264 |
-
llm determines the language model used (default: OpenAI’s GPT-4)
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
Memory and Context
|
| 268 |
-
|
| 269 |
-
memory: Enable to maintain conversation history
|
| 270 |
-
respect_context_window: Prevents token limit issues
|
| 271 |
-
knowledge_sources: Add domain-specific knowledge bases
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
Execution Control
|
| 275 |
-
|
| 276 |
-
max_iter: Maximum attempts before giving best answer
|
| 277 |
-
max_execution_time: Timeout in seconds
|
| 278 |
-
max_rpm: Rate limiting for API calls
|
| 279 |
-
max_retry_limit: Retries on error
|
| 280 |
-
|
| 281 |
-
|
| 282 |
-
Code Execution
|
| 283 |
-
allow_code_execution and code_execution_mode are deprecated. CodeInterpreterTool has been removed from crewai-tools. Use a dedicated sandbox service such as E2B or Modal for secure code execution.
|
| 284 |
-
|
| 285 |
-
allow_code_execution (deprecated): Previously enabled built-in code execution via CodeInterpreterTool.
|
| 286 |
-
code_execution_mode (deprecated): Previously controlled execution mode ("safe" for Docker, "unsafe" for direct execution).
|
| 287 |
-
|
| 288 |
-
|
| 289 |
-
Advanced Features
|
| 290 |
-
|
| 291 |
-
multimodal: Enable multimodal capabilities for processing text and visual content
|
| 292 |
-
reasoning: Enable agent to reflect and create plans before executing tasks
|
| 293 |
-
inject_date: Automatically inject current date into task descriptions
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
Templates
|
| 297 |
-
|
| 298 |
-
system_template: Defines agent’s core behavior
|
| 299 |
-
prompt_template: Structures input format
|
| 300 |
-
response_template: Formats agent responses
|
| 301 |
-
|
| 302 |
-
When using custom templates, ensure that both system_template and prompt_template are defined. The response_template is optional but recommended for consistent output formatting.
|
| 303 |
-
When using custom templates, you can use variables like {role}, {goal}, and {backstory} in your templates. These will be automatically populated during execution.
|
| 304 |
-
|
| 305 |
-
Agent Tools
|
| 306 |
-
Agents can be equipped with various tools to enhance their capabilities. CrewAI supports tools from:
|
| 307 |
-
|
| 308 |
-
CrewAI Toolkit
|
| 309 |
-
LangChain Tools
|
| 310 |
-
|
| 311 |
-
Here’s how to add tools to an agent:
|
| 312 |
-
Code
|
| 313 |
-
|
| 314 |
-
from crewai import Agent
|
| 315 |
-
from crewai_tools import SerperDevTool, WikipediaTools
|
| 316 |
-
|
| 317 |
-
# Create tools
|
| 318 |
-
search_tool = SerperDevTool()
|
| 319 |
-
wiki_tool = WikipediaTools()
|
| 320 |
-
|
| 321 |
-
# Add tools to agent
|
| 322 |
-
researcher = Agent(
|
| 323 |
-
role="AI Technology Researcher",
|
| 324 |
-
goal="Research the latest AI developments",
|
| 325 |
-
tools=[search_tool, wiki_tool],
|
| 326 |
-
verbose=True
|
| 327 |
-
)
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
Agent Memory and Context
|
| 331 |
-
Agents can maintain memory of their interactions and use context from previous tasks. This is particularly useful for complex workflows where information needs to be retained across multiple tasks.
|
| 332 |
-
Code
|
| 333 |
-
|
| 334 |
-
from crewai import Agent
|
| 335 |
-
|
| 336 |
-
analyst = Agent(
|
| 337 |
-
role="Data Analyst",
|
| 338 |
-
goal="Analyze and remember complex data patterns",
|
| 339 |
-
memory=True, # Enable memory
|
| 340 |
-
verbose=True
|
| 341 |
-
)
|
| 342 |
-
|
| 343 |
-
When memory is enabled, the agent will maintain context across multiple interactions, improving its ability to handle complex, multi-step tasks.
|
| 344 |
-
|
| 345 |
-
Context Window Management
|
| 346 |
-
CrewAI includes sophisticated automatic context window management to handle situations where conversations exceed the language model’s token limits. This powerful feature is controlled by the respect_context_window parameter.
|
| 347 |
-
|
| 348 |
-
How Context Window Management Works
|
| 349 |
-
When an agent’s conversation history grows too large for the LLM’s context window, CrewAI automatically detects this situation and can either:
|
| 350 |
-
|
| 351 |
-
Automatically summarize content (when respect_context_window=True)
|
| 352 |
-
Stop execution with an error (when respect_context_window=False)
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
Automatic Context Handling (respect_context_window=True)
|
| 356 |
-
This is the default and recommended setting for most use cases. When enabled, CrewAI will:
|
| 357 |
-
Code
|
| 358 |
-
|
| 359 |
-
# Agent with automatic context management (default)
|
| 360 |
-
smart_agent = Agent(
|
| 361 |
-
role="Research Analyst",
|
| 362 |
-
goal="Analyze large documents and datasets",
|
| 363 |
-
backstory="Expert at processing extensive information",
|
| 364 |
-
respect_context_window=True, # 🔑 Default: auto-handle context limits
|
| 365 |
-
verbose=True
|
| 366 |
-
)
|
| 367 |
-
|
| 368 |
-
What happens when context limits are exceeded:
|
| 369 |
-
|
| 370 |
-
⚠️ Warning message: "Context length exceeded. Summarizing content to fit the model context window."
|
| 371 |
-
🔄 Automatic summarization: CrewAI intelligently summarizes the conversation history
|
| 372 |
-
✅ Continued execution: Task execution continues seamlessly with the summarized context
|
| 373 |
-
📝 Preserved information: Key information is retained while reducing token count
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
Strict Context Limits (respect_context_window=False)
|
| 377 |
-
When you need precise control and prefer execution to stop rather than lose any information:
|
| 378 |
-
Code
|
| 379 |
-
|
| 380 |
-
# Agent with strict context limits
|
| 381 |
-
strict_agent = Agent(
|
| 382 |
-
role="Legal Document Reviewer",
|
| 383 |
-
goal="Provide precise legal analysis without information loss",
|
| 384 |
-
backstory="Legal expert requiring complete context for accurate analysis",
|
| 385 |
-
respect_context_window=False, # ❌ Stop execution on context limit
|
| 386 |
-
verbose=True
|
| 387 |
-
)
|
| 388 |
-
|
| 389 |
-
What happens when context limits are exceeded:
|
| 390 |
-
|
| 391 |
-
❌ Error message: "Context length exceeded. Consider using smaller text or RAG tools from crewai_tools."
|
| 392 |
-
🛑 Execution stops: Task execution halts immediately
|
| 393 |
-
🔧 Manual intervention required: You need to modify your approach
|
| 394 |
-
|
| 395 |
-
|
| 396 |
-
Choosing the Right Setting
|
| 397 |
-
|
| 398 |
-
Use respect_context_window=True (Default) when:
|
| 399 |
-
|
| 400 |
-
Processing large documents that might exceed context limits
|
| 401 |
-
Long-running conversations where some summarization is acceptable
|
| 402 |
-
Research tasks where general context is more important than exact details
|
| 403 |
-
Prototyping and development where you want robust execution
|
| 404 |
-
|
| 405 |
-
Code
|
| 406 |
-
|
| 407 |
-
# Perfect for document processing
|
| 408 |
-
document_processor = Agent(
|
| 409 |
-
role="Document Analyst",
|
| 410 |
-
goal="Extract insights from large research papers",
|
| 411 |
-
backstory="Expert at analyzing extensive documentation",
|
| 412 |
-
respect_context_window=True, # Handle large documents gracefully
|
| 413 |
-
max_iter=50, # Allow more iterations for complex analysis
|
| 414 |
-
verbose=True
|
| 415 |
-
)
|
| 416 |
-
|
| 417 |
-
|
| 418 |
-
Use respect_context_window=False when:
|
| 419 |
-
|
| 420 |
-
Precision is critical and information loss is unacceptable
|
| 421 |
-
Legal or medical tasks requiring complete context
|
| 422 |
-
Code review where missing details could introduce bugs
|
| 423 |
-
Financial analysis where accuracy is paramount
|
| 424 |
-
|
| 425 |
-
Code
|
| 426 |
-
|
| 427 |
-
# Perfect for precision tasks
|
| 428 |
-
precision_agent = Agent(
|
| 429 |
-
role="Code Security Auditor",
|
| 430 |
-
goal="Identify security vulnerabilities in code",
|
| 431 |
-
backstory="Security expert requiring complete code context",
|
| 432 |
-
respect_context_window=False, # Prefer failure over incomplete analysis
|
| 433 |
-
max_retry_limit=1, # Fail fast on context issues
|
| 434 |
-
verbose=True
|
| 435 |
-
)
|
| 436 |
-
|
| 437 |
-
|
| 438 |
-
Alternative Approaches for Large Data
|
| 439 |
-
When dealing with very large datasets, consider these strategies:
|
| 440 |
-
|
| 441 |
-
1. Use RAG Tools
|
| 442 |
-
Code
|
| 443 |
-
|
| 444 |
-
from crewai_tools import RagTool
|
| 445 |
-
|
| 446 |
-
# Create RAG tool for large document processing
|
| 447 |
-
rag_tool = RagTool()
|
| 448 |
-
|
| 449 |
-
rag_agent = Agent(
|
| 450 |
-
role="Research Assistant",
|
| 451 |
-
goal="Query large knowledge bases efficiently",
|
| 452 |
-
backstory="Expert at using RAG tools for information retrieval",
|
| 453 |
-
tools=[rag_tool], # Use RAG instead of large context windows
|
| 454 |
-
respect_context_window=True,
|
| 455 |
-
verbose=True
|
| 456 |
-
)
|
| 457 |
-
|
| 458 |
-
|
| 459 |
-
2. Use Knowledge Sources
|
| 460 |
-
Code
|
| 461 |
-
|
| 462 |
-
# Use knowledge sources instead of large prompts
|
| 463 |
-
knowledge_agent = Agent(
|
| 464 |
-
role="Knowledge Expert",
|
| 465 |
-
goal="Answer questions using curated knowledge",
|
| 466 |
-
backstory="Expert at leveraging structured knowledge sources",
|
| 467 |
-
knowledge_sources=[your_knowledge_sources], # Pre-processed knowledge
|
| 468 |
-
respect_context_window=True,
|
| 469 |
-
verbose=True
|
| 470 |
-
)
|
| 471 |
-
|
| 472 |
-
|
| 473 |
-
Context Window Best Practices
|
| 474 |
-
|
| 475 |
-
Monitor Context Usage: Enable verbose=True to see context management in action
|
| 476 |
-
Design for Efficiency: Structure tasks to minimize context accumulation
|
| 477 |
-
Use Appropriate Models: Choose LLMs with context windows suitable for your tasks
|
| 478 |
-
Test Both Settings: Try both True and False to see which works better for your use case
|
| 479 |
-
Combine with RAG: Use RAG tools for very large datasets instead of relying solely on context windows
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
Troubleshooting Context Issues
|
| 483 |
-
If you’re getting context limit errors:
|
| 484 |
-
Code
|
| 485 |
-
|
| 486 |
-
# Quick fix: Enable automatic handling
|
| 487 |
-
agent.respect_context_window = True
|
| 488 |
-
|
| 489 |
-
# Better solution: Use RAG tools for large data
|
| 490 |
-
from crewai_tools import RagTool
|
| 491 |
-
agent.tools = [RagTool()]
|
| 492 |
-
|
| 493 |
-
# Alternative: Break tasks into smaller pieces
|
| 494 |
-
# Or use knowledge sources instead of large prompts
|
| 495 |
-
|
| 496 |
-
If automatic summarization loses important information:
|
| 497 |
-
Code
|
| 498 |
-
|
| 499 |
-
# Disable auto-summarization and use RAG instead
|
| 500 |
-
agent = Agent(
|
| 501 |
-
role="Detailed Analyst",
|
| 502 |
-
goal="Maintain complete information accuracy",
|
| 503 |
-
backstory="Expert requiring full context",
|
| 504 |
-
respect_context_window=False, # No summarization
|
| 505 |
-
tools=[RagTool()], # Use RAG for large data
|
| 506 |
-
verbose=True
|
| 507 |
-
)
|
| 508 |
-
|
| 509 |
-
The context window management feature works automatically in the background. You don’t need to call any special functions - just set respect_context_window to your preferred behavior and CrewAI handles the rest!
|
| 510 |
-
|
| 511 |
-
Direct Agent Interaction with kickoff()
|
| 512 |
-
Agents can be used directly without going through a task or crew workflow using the kickoff() method. This provides a simpler way to interact with an agent when you don’t need the full crew orchestration capabilities.
|
| 513 |
-
|
| 514 |
-
How kickoff() Works
|
| 515 |
-
The kickoff() method allows you to send messages directly to an agent and get a response, similar to how you would interact with an LLM but with all the agent’s capabilities (tools, reasoning, etc.).
|
| 516 |
-
Code
|
| 517 |
-
|
| 518 |
-
from crewai import Agent
|
| 519 |
-
from crewai_tools import SerperDevTool
|
| 520 |
-
|
| 521 |
-
# Create an agent
|
| 522 |
-
researcher = Agent(
|
| 523 |
-
role="AI Technology Researcher",
|
| 524 |
-
goal="Research the latest AI developments",
|
| 525 |
-
tools=[SerperDevTool()],
|
| 526 |
-
verbose=True
|
| 527 |
-
)
|
| 528 |
-
|
| 529 |
-
# Use kickoff() to interact directly with the agent
|
| 530 |
-
result = researcher.kickoff("What are the latest developments in language models?")
|
| 531 |
-
|
| 532 |
-
# Access the raw response
|
| 533 |
-
print(result.raw)
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
Parameters and Return Values
|
| 537 |
-
Parameter Type Description
|
| 538 |
-
messages Union[str, List[Dict[str, str]]] Either a string query or a list of message dictionaries with role/content
|
| 539 |
-
response_format Optional[Type[Any]] Optional Pydantic model for structured output
|
| 540 |
-
The method returns a LiteAgentOutput object with the following properties:
|
| 541 |
-
|
| 542 |
-
raw: String containing the raw output text
|
| 543 |
-
pydantic: Parsed Pydantic model (if a response_format was provided)
|
| 544 |
-
agent_role: Role of the agent that produced the output
|
| 545 |
-
usage_metrics: Token usage metrics for the execution
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
Structured Output
|
| 549 |
-
You can get structured output by providing a Pydantic model as the response_format:
|
| 550 |
-
Code
|
| 551 |
-
|
| 552 |
-
from pydantic import BaseModel
|
| 553 |
-
from typing import List
|
| 554 |
-
|
| 555 |
-
class ResearchFindings(BaseModel):
|
| 556 |
-
main_points: List[str]
|
| 557 |
-
key_technologies: List[str]
|
| 558 |
-
future_predictions: str
|
| 559 |
-
|
| 560 |
-
# Get structured output
|
| 561 |
-
result = researcher.kickoff(
|
| 562 |
-
"Summarize the latest developments in AI for 2025",
|
| 563 |
-
response_format=ResearchFindings
|
| 564 |
-
)
|
| 565 |
-
|
| 566 |
-
# Access structured data
|
| 567 |
-
print(result.pydantic.main_points)
|
| 568 |
-
print(result.pydantic.future_predictions)
|
| 569 |
-
|
| 570 |
-
|
| 571 |
-
Multiple Messages
|
| 572 |
-
You can also provide a conversation history as a list of message dictionaries:
|
| 573 |
-
Code
|
| 574 |
-
|
| 575 |
-
messages = [
|
| 576 |
-
{"role": "user", "content": "I need information about large language models"},
|
| 577 |
-
{"role": "assistant", "content": "I'd be happy to help with that! What specifically would you like to know?"},
|
| 578 |
-
{"role": "user", "content": "What are the latest developments in 2025?"}
|
| 579 |
-
]
|
| 580 |
-
|
| 581 |
-
result = researcher.kickoff(messages)
|
| 582 |
-
|
| 583 |
-
|
| 584 |
-
Async Support
|
| 585 |
-
An asynchronous version is available via kickoff_async() with the same parameters:
|
| 586 |
-
Code
|
| 587 |
-
|
| 588 |
-
import asyncio
|
| 589 |
-
|
| 590 |
-
async def main():
|
| 591 |
-
result = await researcher.kickoff_async("What are the latest developments in AI?")
|
| 592 |
-
print(result.raw)
|
| 593 |
-
|
| 594 |
-
asyncio.run(main())
|
| 595 |
-
|
| 596 |
-
The kickoff() method uses a LiteAgent internally, which provides a simpler execution flow while preserving all of the agent’s configuration (role, goal, backstory, tools, etc.).
|
| 597 |
-
|
| 598 |
-
Important Considerations and Best Practices
|
| 599 |
-
|
| 600 |
-
Security and Code Execution
|
| 601 |
-
allow_code_execution and code_execution_mode are deprecated and CodeInterpreterTool has been removed. Use a dedicated sandbox service such as E2B or Modal for secure code execution.
|
| 602 |
-
|
| 603 |
-
Performance Optimization
|
| 604 |
-
|
| 605 |
-
Use respect_context_window: true to prevent token limit issues
|
| 606 |
-
Set appropriate max_rpm to avoid rate limiting
|
| 607 |
-
Enable cache: true to improve performance for repetitive tasks
|
| 608 |
-
Adjust max_iter and max_retry_limit based on task complexity
|
| 609 |
-
|
| 610 |
-
|
| 611 |
-
Memory and Context Management
|
| 612 |
-
|
| 613 |
-
Leverage knowledge_sources for domain-specific information
|
| 614 |
-
Configure embedder when using custom embedding models
|
| 615 |
-
Use custom templates (system_template, prompt_template, response_template) for fine-grained control over agent behavior
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
Advanced Features
|
| 619 |
-
|
| 620 |
-
Enable reasoning: true for agents that need to plan and reflect before executing complex tasks
|
| 621 |
-
Set appropriate max_reasoning_attempts to control planning iterations (None for unlimited attempts)
|
| 622 |
-
Use inject_date: true to provide agents with current date awareness for time-sensitive tasks
|
| 623 |
-
Customize the date format with date_format using standard Python datetime format codes
|
| 624 |
-
Enable multimodal: true for agents that need to process both text and visual content
|
| 625 |
-
|
| 626 |
-
|
| 627 |
-
Agent Collaboration
|
| 628 |
-
|
| 629 |
-
Enable allow_delegation: true when agents need to work together
|
| 630 |
-
Use step_callback to monitor and log agent interactions
|
| 631 |
-
Consider using different LLMs for different purposes:
|
| 632 |
-
Main llm for complex reasoning
|
| 633 |
-
function_calling_llm for efficient tool usage
|
| 634 |
-
|
| 635 |
-
|
| 636 |
-
Date Awareness and Reasoning
|
| 637 |
-
|
| 638 |
-
Use inject_date: true to provide agents with current date awareness for time-sensitive tasks
|
| 639 |
-
Customize the date format with date_format using standard Python datetime format codes
|
| 640 |
-
Valid format codes include: %Y (year), %m (month), %d (day), %B (full month name), etc.
|
| 641 |
-
Invalid date formats will be logged as warnings and will not modify the task description
|
| 642 |
-
Enable reasoning: true for complex tasks that benefit from upfront planning and reflection
|
| 643 |
-
|
| 644 |
-
|
| 645 |
-
Model Compatibility
|
| 646 |
-
|
| 647 |
-
Set use_system_prompt: false for older models that don’t support system messages
|
| 648 |
-
Ensure your chosen llm supports the features you need (like function calling)
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
Troubleshooting Common Issues
|
| 652 |
-
|
| 653 |
-
Rate Limiting: If you’re hitting API rate limits:
|
| 654 |
-
Implement appropriate max_rpm
|
| 655 |
-
Use caching for repetitive operations
|
| 656 |
-
Consider batching requests
|
| 657 |
-
Context Window Errors: If you’re exceeding context limits:
|
| 658 |
-
Enable respect_context_window
|
| 659 |
-
Use more efficient prompts
|
| 660 |
-
Clear agent memory periodically
|
| 661 |
-
Code Execution Issues: If code execution fails:
|
| 662 |
-
Verify Docker is installed for safe mode
|
| 663 |
-
Check execution permissions
|
| 664 |
-
Review code sandbox settings
|
| 665 |
-
Memory Issues: If agent responses seem inconsistent:
|
| 666 |
-
Check knowledge source configuration
|
| 667 |
-
Review conversation history management
|
| 668 |
-
|
| 669 |
-
Remember that agents are most effective when configured according to their specific use case. Take time to understand your requirements and adjust these parameters accordingly.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc/doc_crewai_production.md
DELETED
|
@@ -1,145 +0,0 @@
|
|
| 1 |
-
Core Concepts
|
| 2 |
-
Production Architecture
|
| 3 |
-
|
| 4 |
-
Best practices for building production-ready AI applications with CrewAI
|
| 5 |
-
|
| 6 |
-
The Flow-First Mindset
|
| 7 |
-
When building production AI applications with CrewAI, we recommend starting with a Flow. While it’s possible to run individual Crews or Agents, wrapping them in a Flow provides the necessary structure for a robust, scalable application.
|
| 8 |
-
|
| 9 |
-
Why Flows?
|
| 10 |
-
|
| 11 |
-
State Management: Flows provide a built-in way to manage state across different steps of your application. This is crucial for passing data between Crews, maintaining context, and handling user inputs.
|
| 12 |
-
Control: Flows allow you to define precise execution paths, including loops, conditionals, and branching logic. This is essential for handling edge cases and ensuring your application behaves predictably.
|
| 13 |
-
Observability: Flows provide a clear structure that makes it easier to trace execution, debug issues, and monitor performance. We recommend using CrewAI Tracing for detailed insights. Simply run crewai login to enable free observability features.
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
The Architecture
|
| 17 |
-
A typical production CrewAI application looks like this:
|
| 18 |
-
|
| 19 |
-
Valid
|
| 20 |
-
|
| 21 |
-
Invalid
|
| 22 |
-
|
| 23 |
-
Start
|
| 24 |
-
|
| 25 |
-
Flow Orchestrator
|
| 26 |
-
|
| 27 |
-
State Management
|
| 28 |
-
|
| 29 |
-
Step 1: Data Gathering
|
| 30 |
-
|
| 31 |
-
Research Crew
|
| 32 |
-
|
| 33 |
-
Condition Check
|
| 34 |
-
|
| 35 |
-
Step 3: Execution
|
| 36 |
-
|
| 37 |
-
Action Crew
|
| 38 |
-
|
| 39 |
-
End
|
| 40 |
-
|
| 41 |
-
1. The Flow Class
|
| 42 |
-
Your Flow class is the entry point. It defines the state schema and the methods that execute your logic.
|
| 43 |
-
|
| 44 |
-
from crewai.flow.flow import Flow, listen, start
|
| 45 |
-
from pydantic import BaseModel
|
| 46 |
-
|
| 47 |
-
class AppState(BaseModel):
|
| 48 |
-
user_input: str = ""
|
| 49 |
-
research_results: str = ""
|
| 50 |
-
final_report: str = ""
|
| 51 |
-
|
| 52 |
-
class ProductionFlow(Flow[AppState]):
|
| 53 |
-
@start()
|
| 54 |
-
def gather_input(self):
|
| 55 |
-
# ... logic to get input ...
|
| 56 |
-
pass
|
| 57 |
-
|
| 58 |
-
@listen(gather_input)
|
| 59 |
-
def run_research_crew(self):
|
| 60 |
-
# ... trigger a Crew ...
|
| 61 |
-
pass
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
2. State Management
|
| 65 |
-
Use Pydantic models to define your state. This ensures type safety and makes it clear what data is available at each step.
|
| 66 |
-
|
| 67 |
-
Keep it minimal: Store only what you need to persist between steps.
|
| 68 |
-
Use structured data: Avoid unstructured dictionaries when possible.
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
3. Crews as Units of Work
|
| 72 |
-
Delegate complex tasks to Crews. A Crew should be focused on a specific goal (e.g., “Research a topic”, “Write a blog post”).
|
| 73 |
-
|
| 74 |
-
Don’t over-engineer Crews: Keep them focused.
|
| 75 |
-
Pass state explicitly: Pass the necessary data from the Flow state to the Crew inputs.
|
| 76 |
-
|
| 77 |
-
@listen(gather_input)
|
| 78 |
-
def run_research_crew(self):
|
| 79 |
-
crew = ResearchCrew()
|
| 80 |
-
result = crew.kickoff(inputs={"topic": self.state.user_input})
|
| 81 |
-
self.state.research_results = result.raw
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
Control Primitives
|
| 85 |
-
Leverage CrewAI’s control primitives to add robustness and control to your Crews.
|
| 86 |
-
|
| 87 |
-
1. Task Guardrails
|
| 88 |
-
Use Task Guardrails to validate task outputs before they are accepted. This ensures that your agents produce high-quality results.
|
| 89 |
-
|
| 90 |
-
def validate_content(result: TaskOutput) -> Tuple[bool, Any]:
|
| 91 |
-
if len(result.raw) < 100:
|
| 92 |
-
return (False, "Content is too short. Please expand.")
|
| 93 |
-
return (True, result.raw)
|
| 94 |
-
|
| 95 |
-
task = Task(
|
| 96 |
-
...,
|
| 97 |
-
guardrail=validate_content
|
| 98 |
-
)
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
2. Structured Outputs
|
| 102 |
-
Always use structured outputs (output_pydantic or output_json) when passing data between tasks or to your application. This prevents parsing errors and ensures type safety.
|
| 103 |
-
|
| 104 |
-
class ResearchResult(BaseModel):
|
| 105 |
-
summary: str
|
| 106 |
-
sources: List[str]
|
| 107 |
-
|
| 108 |
-
task = Task(
|
| 109 |
-
...,
|
| 110 |
-
output_pydantic=ResearchResult
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
3. LLM Hooks
|
| 115 |
-
Use LLM Hooks to inspect or modify messages before they are sent to the LLM, or to sanitize responses.
|
| 116 |
-
|
| 117 |
-
@before_llm_call
|
| 118 |
-
def log_request(context):
|
| 119 |
-
print(f"Agent {context.agent.role} is calling the LLM...")
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
Deployment Patterns
|
| 123 |
-
When deploying your Flow, consider the following:
|
| 124 |
-
|
| 125 |
-
CrewAI Enterprise
|
| 126 |
-
The easiest way to deploy your Flow is using CrewAI Enterprise. It handles the infrastructure, authentication, and monitoring for you. Check out the Deployment Guide to get started.
|
| 127 |
-
|
| 128 |
-
crewai deploy create
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
Async Execution
|
| 132 |
-
For long-running tasks, use kickoff_async to avoid blocking your API.
|
| 133 |
-
|
| 134 |
-
Persistence
|
| 135 |
-
Use the @persist decorator to save the state of your Flow to a database. This allows you to resume execution if the process crashes or if you need to wait for human input.
|
| 136 |
-
|
| 137 |
-
@persist
|
| 138 |
-
class ProductionFlow(Flow[AppState]):
|
| 139 |
-
# ...
|
| 140 |
-
|
| 141 |
-
By default, @persist resumes a flow when kickoff(inputs={"id": <uuid>}) is supplied, extending the same flow_uuid history. To fork a persisted flow into a new lineage — hydrate state from a previous run but write under a fresh state.id — pass restore_from_state_id:
|
| 142 |
-
|
| 143 |
-
flow.kickoff(restore_from_state_id="<previous-run-state-id>")
|
| 144 |
-
|
| 145 |
-
The new run gets a fresh state.id (auto-generated, or inputs["id"] if pinned) so its @persist writes don’t extend the source’s history. Combining with from_checkpoint raises a ValueError; pick one hydration source.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc/doc_crewai_skills.md
DELETED
|
@@ -1,223 +0,0 @@
|
|
| 1 |
-
Core Concepts
|
| 2 |
-
Skills
|
| 3 |
-
|
| 4 |
-
Filesystem-based skill packages that inject domain expertise and instructions into agent prompts.
|
| 5 |
-
|
| 6 |
-
Overview
|
| 7 |
-
Skills are self-contained directories that provide agents with domain-specific instructions, guidelines, and reference material. Each skill is defined by a SKILL.md file with YAML frontmatter and a markdown body. When activated, a skill’s instructions are injected directly into the agent’s task prompt — giving the agent expertise without requiring any code changes.
|
| 8 |
-
Skills are NOT tools. This is the most common point of confusion.
|
| 9 |
-
|
| 10 |
-
Skills inject instructions and context into the agent’s prompt. They tell the agent how to think about a problem.
|
| 11 |
-
Tools give the agent callable functions to take action (search, read files, call APIs).
|
| 12 |
-
|
| 13 |
-
You often need both: skills for expertise, tools for action. They are configured independently and complement each other.
|
| 14 |
-
|
| 15 |
-
Quick Start
|
| 16 |
-
|
| 17 |
-
1. Create a Skill Directory
|
| 18 |
-
|
| 19 |
-
skills/
|
| 20 |
-
└── code-review/
|
| 21 |
-
├── SKILL.md # Required — instructions
|
| 22 |
-
├── references/ # Optional — reference docs
|
| 23 |
-
│ └── style-guide.md
|
| 24 |
-
└── scripts/ # Optional — executable scripts
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
2. Write Your SKILL.md
|
| 28 |
-
|
| 29 |
-
---
|
| 30 |
-
name: code-review
|
| 31 |
-
description: Guidelines for conducting thorough code reviews with focus on security and performance.
|
| 32 |
-
metadata:
|
| 33 |
-
author: your-team
|
| 34 |
-
version: "1.0"
|
| 35 |
-
---
|
| 36 |
-
|
| 37 |
-
## Code Review Guidelines
|
| 38 |
-
|
| 39 |
-
When reviewing code, follow this checklist:
|
| 40 |
-
|
| 41 |
-
1. **Security**: Check for injection vulnerabilities, auth bypasses, and data exposure
|
| 42 |
-
2. **Performance**: Look for N+1 queries, unnecessary allocations, and blocking calls
|
| 43 |
-
3. **Readability**: Ensure clear naming, appropriate comments, and consistent style
|
| 44 |
-
4. **Testing**: Verify adequate test coverage for new functionality
|
| 45 |
-
|
| 46 |
-
### Severity Levels
|
| 47 |
-
- **Critical**: Security vulnerabilities, data loss risks → block merge
|
| 48 |
-
- **Major**: Performance issues, logic errors → request changes
|
| 49 |
-
- **Minor**: Style issues, naming suggestions → approve with comments
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
3. Attach to an Agent
|
| 53 |
-
|
| 54 |
-
from crewai import Agent
|
| 55 |
-
from crewai_tools import GithubSearchTool, FileReadTool
|
| 56 |
-
|
| 57 |
-
reviewer = Agent(
|
| 58 |
-
role="Senior Code Reviewer",
|
| 59 |
-
goal="Review pull requests for quality and security issues",
|
| 60 |
-
backstory="Staff engineer with expertise in secure coding practices.",
|
| 61 |
-
skills=["./skills"], # Injects review guidelines
|
| 62 |
-
tools=[GithubSearchTool(), FileReadTool()], # Lets agent read code
|
| 63 |
-
)
|
| 64 |
-
|
| 65 |
-
The agent now has both expertise (from the skill) and capabilities (from the tools).
|
| 66 |
-
|
| 67 |
-
Skills + Tools: Working Together
|
| 68 |
-
Here are common patterns showing how skills and tools complement each other:
|
| 69 |
-
|
| 70 |
-
Pattern 1: Skills Only (Domain Expertise, No Actions Needed)
|
| 71 |
-
Use when the agent needs specific instructions but doesn’t need to call external services:
|
| 72 |
-
|
| 73 |
-
agent = Agent(
|
| 74 |
-
role="Technical Writer",
|
| 75 |
-
goal="Write clear API documentation",
|
| 76 |
-
backstory="Expert technical writer",
|
| 77 |
-
skills=["./skills/api-docs-style"], # Writing guidelines and templates
|
| 78 |
-
# No tools needed — agent writes based on provided context
|
| 79 |
-
)
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
Pattern 2: Tools Only (Actions, No Special Expertise)
|
| 83 |
-
Use when the agent needs to take action but doesn’t need domain-specific instructions:
|
| 84 |
-
|
| 85 |
-
from crewai_tools import SerperDevTool, ScrapeWebsiteTool
|
| 86 |
-
|
| 87 |
-
agent = Agent(
|
| 88 |
-
role="Web Researcher",
|
| 89 |
-
goal="Find information about a topic",
|
| 90 |
-
backstory="Skilled at finding information online",
|
| 91 |
-
tools=[SerperDevTool(), ScrapeWebsiteTool()], # Can search and scrape
|
| 92 |
-
# No skills needed — general research doesn't need special guidelines
|
| 93 |
-
)
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
Pattern 3: Skills + Tools (Expertise AND Actions)
|
| 97 |
-
The most common real-world pattern. The skill provides how to approach the work; tools provide what the agent can do:
|
| 98 |
-
|
| 99 |
-
from crewai_tools import SerperDevTool, FileReadTool, CodeInterpreterTool
|
| 100 |
-
|
| 101 |
-
analyst = Agent(
|
| 102 |
-
role="Security Analyst",
|
| 103 |
-
goal="Audit infrastructure for vulnerabilities",
|
| 104 |
-
backstory="Expert in cloud security and compliance",
|
| 105 |
-
skills=["./skills/security-audit"], # Audit methodology and checklists
|
| 106 |
-
tools=[
|
| 107 |
-
SerperDevTool(), # Research known vulnerabilities
|
| 108 |
-
FileReadTool(), # Read config files
|
| 109 |
-
CodeInterpreterTool(), # Run analysis scripts
|
| 110 |
-
],
|
| 111 |
-
)
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
Pattern 4: Skills + MCPs
|
| 115 |
-
Skills work alongside MCP servers the same way they work with tools:
|
| 116 |
-
|
| 117 |
-
agent = Agent(
|
| 118 |
-
role="Data Analyst",
|
| 119 |
-
goal="Analyze customer data and generate reports",
|
| 120 |
-
backstory="Expert data analyst with strong statistical background",
|
| 121 |
-
skills=["./skills/data-analysis"], # Analysis methodology
|
| 122 |
-
mcps=["https://data-warehouse.example.com/sse"], # Remote data access
|
| 123 |
-
)
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
Pattern 5: Skills + Apps
|
| 127 |
-
Skills can guide how an agent uses platform integrations:
|
| 128 |
-
|
| 129 |
-
agent = Agent(
|
| 130 |
-
role="Customer Support Agent",
|
| 131 |
-
goal="Respond to customer inquiries professionally",
|
| 132 |
-
backstory="Experienced support representative",
|
| 133 |
-
skills=["./skills/support-playbook"], # Response templates and escalation rules
|
| 134 |
-
apps=["gmail", "zendesk"], # Can send emails and update tickets
|
| 135 |
-
)
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
Crew-Level Skills
|
| 139 |
-
Skills can be set on a crew to apply to all agents:
|
| 140 |
-
|
| 141 |
-
from crewai import Crew
|
| 142 |
-
|
| 143 |
-
crew = Crew(
|
| 144 |
-
agents=[researcher, writer, reviewer],
|
| 145 |
-
tasks=[research_task, write_task, review_task],
|
| 146 |
-
skills=["./skills"], # All agents get these skills
|
| 147 |
-
)
|
| 148 |
-
|
| 149 |
-
Agent-level skills take priority — if the same skill is discovered at both levels, the agent’s version is used.
|
| 150 |
-
|
| 151 |
-
SKILL.md Format
|
| 152 |
-
|
| 153 |
-
---
|
| 154 |
-
name: my-skill
|
| 155 |
-
description: Short description of what this skill does and when to use it.
|
| 156 |
-
license: Apache-2.0 # optional
|
| 157 |
-
compatibility: crewai>=0.1.0 # optional
|
| 158 |
-
metadata: # optional
|
| 159 |
-
author: your-name
|
| 160 |
-
version: "1.0"
|
| 161 |
-
allowed-tools: web-search file-read # optional, experimental
|
| 162 |
-
---
|
| 163 |
-
|
| 164 |
-
Instructions for the agent go here. This markdown body is injected
|
| 165 |
-
into the agent's prompt when the skill is activated.
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
Frontmatter Fields
|
| 169 |
-
Field Required Description
|
| 170 |
-
name Yes 1–64 chars. Lowercase alphanumeric and hyphens. Must match directory name.
|
| 171 |
-
description Yes 1–1024 chars. Describes what the skill does and when to use it.
|
| 172 |
-
license No License name or reference to a bundled license file.
|
| 173 |
-
compatibility No Max 500 chars. Environment requirements (products, packages, network).
|
| 174 |
-
metadata No Arbitrary string key-value mapping.
|
| 175 |
-
allowed-tools No Space-delimited list of pre-approved tools. Experimental.
|
| 176 |
-
|
| 177 |
-
Directory Structure
|
| 178 |
-
|
| 179 |
-
my-skill/
|
| 180 |
-
├── SKILL.md # Required — frontmatter + instructions
|
| 181 |
-
├── scripts/ # Optional — executable scripts
|
| 182 |
-
├── references/ # Optional — reference documents
|
| 183 |
-
└── assets/ # Optional — static files (configs, data)
|
| 184 |
-
|
| 185 |
-
The directory name must match the name field in SKILL.md. The scripts/, references/, and assets/ directories are available on the skill’s path for agents that need to reference files directly.
|
| 186 |
-
|
| 187 |
-
Pre-loading Skills
|
| 188 |
-
For more control, you can discover and activate skills programmatically:
|
| 189 |
-
|
| 190 |
-
from pathlib import Path
|
| 191 |
-
from crewai.skills import discover_skills, activate_skill
|
| 192 |
-
|
| 193 |
-
# Discover all skills in a directory
|
| 194 |
-
skills = discover_skills(Path("./skills"))
|
| 195 |
-
|
| 196 |
-
# Activate them (loads full SKILL.md body)
|
| 197 |
-
activated = [activate_skill(s) for s in skills]
|
| 198 |
-
|
| 199 |
-
# Pass to an agent
|
| 200 |
-
agent = Agent(
|
| 201 |
-
role="Researcher",
|
| 202 |
-
goal="Find relevant information",
|
| 203 |
-
backstory="An expert researcher.",
|
| 204 |
-
skills=activated,
|
| 205 |
-
)
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
How Skills Are Loaded
|
| 209 |
-
Skills use progressive disclosure — only loading what’s needed at each stage:
|
| 210 |
-
Stage What’s loaded When
|
| 211 |
-
Discovery Name, description, frontmatter fields discover_skills()
|
| 212 |
-
Activation Full SKILL.md body text activate_skill()
|
| 213 |
-
During normal agent execution (passing directory paths via skills=["./skills"]), skills are automatically discovered and activated. The progressive loading only matters when using the programmatic API.
|
| 214 |
-
|
| 215 |
-
Skills vs Knowledge
|
| 216 |
-
Both skills and knowledge modify the agent’s prompt, but they serve different purposes:
|
| 217 |
-
Aspect Skills Knowledge
|
| 218 |
-
What it provides Instructions, procedures, guidelines Facts, data, information
|
| 219 |
-
How it’s stored Markdown files (SKILL.md) Embedded in vector store (ChromaDB)
|
| 220 |
-
How it’s retrieved Entire body injected into prompt Semantic search finds relevant chunks
|
| 221 |
-
Best for Methodology, checklists, style guides Company docs, product info, reference data
|
| 222 |
-
Set via skills=["./skills"] knowledge_sources=[source]
|
| 223 |
-
Rule of thumb: If the agent needs to follow a process, use a skill. If the agent needs to reference data, use knowledge.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc/doc_crewai_tool.md
DELETED
|
@@ -1,256 +0,0 @@
|
|
| 1 |
-
Core Concepts
|
| 2 |
-
Tools
|
| 3 |
-
|
| 4 |
-
Understanding and leveraging tools within the CrewAI framework for agent collaboration and task execution.
|
| 5 |
-
|
| 6 |
-
Overview
|
| 7 |
-
CrewAI tools empower agents with capabilities ranging from web searching and data analysis to collaboration and delegating tasks among coworkers. This documentation outlines how to create, integrate, and leverage these tools within the CrewAI framework, including a new focus on collaboration tools.
|
| 8 |
-
Tools give agents callable functions to take action. They work alongside MCPs (remote tool servers), Apps (platform integrations), Skills (domain expertise), and Knowledge (retrieved facts). See the Agent Capabilities overview to understand when to use each.
|
| 9 |
-
|
| 10 |
-
What is a Tool?
|
| 11 |
-
A tool in CrewAI is a skill or function that agents can utilize to perform various actions. This includes tools from the CrewAI Toolkit and LangChain Tools, enabling everything from simple searches to complex interactions and effective teamwork among agents.
|
| 12 |
-
CrewAI AMP provides a comprehensive Tools Repository with pre-built integrations for common business systems and APIs. Deploy agents with enterprise tools in minutes instead of days.The Enterprise Tools Repository includes:
|
| 13 |
-
|
| 14 |
-
Pre-built connectors for popular enterprise systems
|
| 15 |
-
Custom tool creation interface
|
| 16 |
-
Version control and sharing capabilities
|
| 17 |
-
Security and compliance features
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
Key Characteristics of Tools
|
| 21 |
-
|
| 22 |
-
Utility: Crafted for tasks such as web searching, data analysis, content generation, and agent collaboration.
|
| 23 |
-
Integration: Boosts agent capabilities by seamlessly integrating tools into their workflow.
|
| 24 |
-
Customizability: Provides the flexibility to develop custom tools or utilize existing ones, catering to the specific needs of agents.
|
| 25 |
-
Error Handling: Incorporates robust error handling mechanisms to ensure smooth operation.
|
| 26 |
-
Caching Mechanism: Features intelligent caching to optimize performance and reduce redundant operations.
|
| 27 |
-
Asynchronous Support: Handles both synchronous and asynchronous tools, enabling non-blocking operations.
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
Using CrewAI Tools
|
| 31 |
-
To enhance your agents’ capabilities with crewAI tools, begin by installing our extra tools package:
|
| 32 |
-
|
| 33 |
-
pip install 'crewai[tools]'
|
| 34 |
-
|
| 35 |
-
Here’s an example demonstrating their use:
|
| 36 |
-
Code
|
| 37 |
-
|
| 38 |
-
import os
|
| 39 |
-
from crewai import Agent, Task, Crew
|
| 40 |
-
# Importing crewAI tools
|
| 41 |
-
from crewai_tools import (
|
| 42 |
-
DirectoryReadTool,
|
| 43 |
-
FileReadTool,
|
| 44 |
-
SerperDevTool,
|
| 45 |
-
WebsiteSearchTool
|
| 46 |
-
)
|
| 47 |
-
|
| 48 |
-
# Set up API keys
|
| 49 |
-
os.environ["SERPER_API_KEY"] = "Your Key" # serper.dev API key
|
| 50 |
-
os.environ["OPENAI_API_KEY"] = "Your Key"
|
| 51 |
-
|
| 52 |
-
# Instantiate tools
|
| 53 |
-
docs_tool = DirectoryReadTool(directory='./blog-posts')
|
| 54 |
-
file_tool = FileReadTool()
|
| 55 |
-
search_tool = SerperDevTool()
|
| 56 |
-
web_rag_tool = WebsiteSearchTool()
|
| 57 |
-
|
| 58 |
-
# Create agents
|
| 59 |
-
researcher = Agent(
|
| 60 |
-
role='Market Research Analyst',
|
| 61 |
-
goal='Provide up-to-date market analysis of the AI industry',
|
| 62 |
-
backstory='An expert analyst with a keen eye for market trends.',
|
| 63 |
-
tools=[search_tool, web_rag_tool],
|
| 64 |
-
verbose=True
|
| 65 |
-
)
|
| 66 |
-
|
| 67 |
-
writer = Agent(
|
| 68 |
-
role='Content Writer',
|
| 69 |
-
goal='Craft engaging blog posts about the AI industry',
|
| 70 |
-
backstory='A skilled writer with a passion for technology.',
|
| 71 |
-
tools=[docs_tool, file_tool],
|
| 72 |
-
verbose=True
|
| 73 |
-
)
|
| 74 |
-
|
| 75 |
-
# Define tasks
|
| 76 |
-
research = Task(
|
| 77 |
-
description='Research the latest trends in the AI industry and provide a summary.',
|
| 78 |
-
expected_output='A summary of the top 3 trending developments in the AI industry with a unique perspective on their significance.',
|
| 79 |
-
agent=researcher
|
| 80 |
-
)
|
| 81 |
-
|
| 82 |
-
write = Task(
|
| 83 |
-
description='Write an engaging blog post about the AI industry, based on the research analyst's summary. Draw inspiration from the latest blog posts in the directory.',
|
| 84 |
-
expected_output='A 4-paragraph blog post formatted in markdown with engaging, informative, and accessible content, avoiding complex jargon.',
|
| 85 |
-
agent=writer,
|
| 86 |
-
output_file='blog-posts/new_post.md' # The final blog post will be saved here
|
| 87 |
-
)
|
| 88 |
-
|
| 89 |
-
# Assemble a crew with planning enabled
|
| 90 |
-
crew = Crew(
|
| 91 |
-
agents=[researcher, writer],
|
| 92 |
-
tasks=[research, write],
|
| 93 |
-
verbose=True,
|
| 94 |
-
planning=True, # Enable planning feature
|
| 95 |
-
)
|
| 96 |
-
|
| 97 |
-
# Execute tasks
|
| 98 |
-
crew.kickoff()
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
Available CrewAI Tools
|
| 102 |
-
|
| 103 |
-
Error Handling: All tools are built with error handling capabilities, allowing agents to gracefully manage exceptions and continue their tasks.
|
| 104 |
-
Caching Mechanism: All tools support caching, enabling agents to efficiently reuse previously obtained results, reducing the load on external resources and speeding up the execution time. You can also define finer control over the caching mechanism using the cache_function attribute on the tool.
|
| 105 |
-
|
| 106 |
-
Here is a list of the available tools and their descriptions:
|
| 107 |
-
Tool Description
|
| 108 |
-
ApifyActorsTool A tool that integrates Apify Actors with your workflows for web scraping and automation tasks.
|
| 109 |
-
BrowserbaseLoadTool A tool for interacting with and extracting data from web browsers.
|
| 110 |
-
CodeDocsSearchTool A RAG tool optimized for searching through code documentation and related technical documents.
|
| 111 |
-
CodeInterpreterTool A tool for interpreting python code.
|
| 112 |
-
ComposioTool Enables use of Composio tools.
|
| 113 |
-
CSVSearchTool A RAG tool designed for searching within CSV files, tailored to handle structured data.
|
| 114 |
-
DALL-E Tool A tool for generating images using the DALL-E API.
|
| 115 |
-
DirectorySearchTool A RAG tool for searching within directories, useful for navigating through file systems.
|
| 116 |
-
DOCXSearchTool A RAG tool aimed at searching within DOCX documents, ideal for processing Word files.
|
| 117 |
-
DirectoryReadTool Facilitates reading and processing of directory structures and their contents.
|
| 118 |
-
ExaSearchTool Search the web with Exa, the fastest and most accurate web search API. Supports token-efficient highlights and full page content.
|
| 119 |
-
FileReadTool Enables reading and extracting data from files, supporting various file formats.
|
| 120 |
-
FirecrawlSearchTool A tool to search webpages using Firecrawl and return the results.
|
| 121 |
-
FirecrawlCrawlWebsiteTool A tool for crawling webpages using Firecrawl.
|
| 122 |
-
FirecrawlScrapeWebsiteTool A tool for scraping webpages URL using Firecrawl and returning its contents.
|
| 123 |
-
GithubSearchTool A RAG tool for searching within GitHub repositories, useful for code and documentation search.
|
| 124 |
-
SerperDevTool A specialized tool for development purposes, with specific functionalities under development.
|
| 125 |
-
TXTSearchTool A RAG tool focused on searching within text (.txt) files, suitable for unstructured data.
|
| 126 |
-
JSONSearchTool A RAG tool designed for searching within JSON files, catering to structured data handling.
|
| 127 |
-
LlamaIndexTool Enables the use of LlamaIndex tools.
|
| 128 |
-
MDXSearchTool A RAG tool tailored for searching within Markdown (MDX) files, useful for documentation.
|
| 129 |
-
PDFSearchTool A RAG tool aimed at searching within PDF documents, ideal for processing scanned documents.
|
| 130 |
-
PGSearchTool A RAG tool optimized for searching within PostgreSQL databases, suitable for database queries.
|
| 131 |
-
Vision Tool A tool for generating images using the DALL-E API.
|
| 132 |
-
RagTool A general-purpose RAG tool capable of handling various data sources and types.
|
| 133 |
-
ScrapeElementFromWebsiteTool Enables scraping specific elements from websites, useful for targeted data extraction.
|
| 134 |
-
ScrapeWebsiteTool Facilitates scraping entire websites, ideal for comprehensive data collection.
|
| 135 |
-
WebsiteSearchTool A RAG tool for searching website content, optimized for web data extraction.
|
| 136 |
-
XMLSearchTool A RAG tool designed for searching within XML files, suitable for structured data formats.
|
| 137 |
-
YoutubeChannelSearchTool A RAG tool for searching within YouTube channels, useful for video content analysis.
|
| 138 |
-
YoutubeVideoSearchTool A RAG tool aimed at searching within YouTube videos, ideal for video data extraction.
|
| 139 |
-
|
| 140 |
-
Creating your own Tools
|
| 141 |
-
Developers can craft custom tools tailored for their agent’s needs or utilize pre-built options.
|
| 142 |
-
There are two main ways for one to create a CrewAI tool:
|
| 143 |
-
|
| 144 |
-
Subclassing BaseTool
|
| 145 |
-
Code
|
| 146 |
-
|
| 147 |
-
from crewai.tools import BaseTool
|
| 148 |
-
from pydantic import BaseModel, Field
|
| 149 |
-
|
| 150 |
-
class MyToolInput(BaseModel):
|
| 151 |
-
"""Input schema for MyCustomTool."""
|
| 152 |
-
argument: str = Field(..., description="Description of the argument.")
|
| 153 |
-
|
| 154 |
-
class MyCustomTool(BaseTool):
|
| 155 |
-
name: str = "Name of my tool"
|
| 156 |
-
description: str = "What this tool does. It's vital for effective utilization."
|
| 157 |
-
args_schema: Type[BaseModel] = MyToolInput
|
| 158 |
-
|
| 159 |
-
def _run(self, argument: str) -> str:
|
| 160 |
-
# Your tool's logic here
|
| 161 |
-
return "Tool's result"
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
Asynchronous Tool Support
|
| 165 |
-
CrewAI supports asynchronous tools, allowing you to implement tools that perform non-blocking operations like network requests, file I/O, or other async operations without blocking the main execution thread.
|
| 166 |
-
|
| 167 |
-
Creating Async Tools
|
| 168 |
-
You can create async tools in two ways:
|
| 169 |
-
|
| 170 |
-
1. Using the tool Decorator with Async Functions
|
| 171 |
-
Code
|
| 172 |
-
|
| 173 |
-
from crewai.tools import tool
|
| 174 |
-
|
| 175 |
-
@tool("fetch_data_async")
|
| 176 |
-
async def fetch_data_async(query: str) -> str:
|
| 177 |
-
"""Asynchronously fetch data based on the query."""
|
| 178 |
-
# Simulate async operation
|
| 179 |
-
await asyncio.sleep(1)
|
| 180 |
-
return f"Data retrieved for {query}"
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
2. Implementing Async Methods in Custom Tool Classes
|
| 184 |
-
Code
|
| 185 |
-
|
| 186 |
-
from crewai.tools import BaseTool
|
| 187 |
-
|
| 188 |
-
class AsyncCustomTool(BaseTool):
|
| 189 |
-
name: str = "async_custom_tool"
|
| 190 |
-
description: str = "An asynchronous custom tool"
|
| 191 |
-
|
| 192 |
-
async def _run(self, query: str = "") -> str:
|
| 193 |
-
"""Asynchronously run the tool"""
|
| 194 |
-
# Your async implementation here
|
| 195 |
-
await asyncio.sleep(1)
|
| 196 |
-
return f"Processed {query} asynchronously"
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
Using Async Tools
|
| 200 |
-
Async tools work seamlessly in both standard Crew workflows and Flow-based workflows:
|
| 201 |
-
Code
|
| 202 |
-
|
| 203 |
-
# In standard Crew
|
| 204 |
-
agent = Agent(role="researcher", tools=[async_custom_tool])
|
| 205 |
-
|
| 206 |
-
# In Flow
|
| 207 |
-
class MyFlow(Flow):
|
| 208 |
-
@start()
|
| 209 |
-
async def begin(self):
|
| 210 |
-
crew = Crew(agents=[agent])
|
| 211 |
-
result = await crew.kickoff_async()
|
| 212 |
-
return result
|
| 213 |
-
|
| 214 |
-
The CrewAI framework automatically handles the execution of both synchronous and asynchronous tools, so you don’t need to worry about how to call them differently.
|
| 215 |
-
|
| 216 |
-
Utilizing the tool Decorator
|
| 217 |
-
Code
|
| 218 |
-
|
| 219 |
-
from crewai.tools import tool
|
| 220 |
-
@tool("Name of my tool")
|
| 221 |
-
def my_tool(question: str) -> str:
|
| 222 |
-
"""Clear description for what this tool is useful for, your agent will need this information to use it."""
|
| 223 |
-
# Function logic here
|
| 224 |
-
return "Result from your custom tool"
|
| 225 |
-
|
| 226 |
-
|
| 227 |
-
Custom Caching Mechanism
|
| 228 |
-
Tools can optionally implement a cache_function to fine-tune caching behavior. This function determines when to cache results based on specific conditions, offering granular control over caching logic.
|
| 229 |
-
Code
|
| 230 |
-
|
| 231 |
-
from crewai.tools import tool
|
| 232 |
-
|
| 233 |
-
@tool
|
| 234 |
-
def multiplication_tool(first_number: int, second_number: int) -> str:
|
| 235 |
-
"""Useful for when you need to multiply two numbers together."""
|
| 236 |
-
return first_number * second_number
|
| 237 |
-
|
| 238 |
-
def cache_func(args, result):
|
| 239 |
-
# In this case, we only cache the result if it's a multiple of 2
|
| 240 |
-
cache = result % 2 == 0
|
| 241 |
-
return cache
|
| 242 |
-
|
| 243 |
-
multiplication_tool.cache_function = cache_func
|
| 244 |
-
|
| 245 |
-
writer1 = Agent(
|
| 246 |
-
role="Writer",
|
| 247 |
-
goal="You write lessons of math for kids.",
|
| 248 |
-
backstory="You're an expert in writing and you love to teach kids but you know nothing of math.",
|
| 249 |
-
tools=[multiplication_tool],
|
| 250 |
-
allow_delegation=False,
|
| 251 |
-
)
|
| 252 |
-
#...
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
Conclusion
|
| 256 |
-
Tools are pivotal in extending the capabilities of CrewAI agents, enabling them to undertake a broad spectrum of tasks and collaborate effectively. When building solutions with CrewAI, leverage both custom and existing tools to empower your agents and enhance the AI ecosystem. Consider utilizing error handling, caching mechanisms, and the flexibility of tool arguments to optimize your agents’ performance and capabilities.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc/doc_crewai_training.md
DELETED
|
@@ -1,173 +0,0 @@
|
|
| 1 |
-
Core Concepts
|
| 2 |
-
Training
|
| 3 |
-
|
| 4 |
-
Learn how to train your CrewAI agents by giving them feedback early on and get consistent results.
|
| 5 |
-
|
| 6 |
-
Overview
|
| 7 |
-
The training feature in CrewAI allows you to train your AI agents using the command-line interface (CLI). By running the command crewai train -n <n_iterations>, you can specify the number of iterations for the training process. During training, CrewAI utilizes techniques to optimize the performance of your agents along with human feedback. This helps the agents improve their understanding, decision-making, and problem-solving abilities.
|
| 8 |
-
|
| 9 |
-
Training Your Crew Using the CLI
|
| 10 |
-
To use the training feature, follow these steps:
|
| 11 |
-
|
| 12 |
-
Open your terminal or command prompt.
|
| 13 |
-
Navigate to the directory where your CrewAI project is located.
|
| 14 |
-
Run the following command:
|
| 15 |
-
|
| 16 |
-
crewai train -n <n_iterations> -f <filename.pkl>
|
| 17 |
-
|
| 18 |
-
Replace <n_iterations> with the desired number of training iterations and <filename> with the appropriate filename ending with .pkl.
|
| 19 |
-
If you omit -f, the output defaults to trained_agents_data.pkl in the current working directory. You can pass an absolute path to control where the file is written.
|
| 20 |
-
|
| 21 |
-
Training your Crew programmatically
|
| 22 |
-
To train your crew programmatically, use the following steps:
|
| 23 |
-
|
| 24 |
-
Define the number of iterations for training.
|
| 25 |
-
Specify the input parameters for the training process.
|
| 26 |
-
Execute the training command within a try-except block to handle potential errors.
|
| 27 |
-
|
| 28 |
-
Code
|
| 29 |
-
|
| 30 |
-
n_iterations = 2
|
| 31 |
-
inputs = {"topic": "CrewAI Training"}
|
| 32 |
-
filename = "your_model.pkl"
|
| 33 |
-
|
| 34 |
-
try:
|
| 35 |
-
YourCrewName_Crew().crew().train(
|
| 36 |
-
n_iterations=n_iterations,
|
| 37 |
-
inputs=inputs,
|
| 38 |
-
filename=filename
|
| 39 |
-
)
|
| 40 |
-
|
| 41 |
-
except Exception as e:
|
| 42 |
-
raise Exception(f"An error occurred while training the crew: {e}")
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
How trained data is used by agents
|
| 46 |
-
CrewAI uses the training artifacts in two ways: during training to incorporate your human feedback, and after training to guide agents with consolidated suggestions.
|
| 47 |
-
|
| 48 |
-
Training data flow
|
| 49 |
-
|
| 50 |
-
Iterations
|
| 51 |
-
|
| 52 |
-
Yes
|
| 53 |
-
|
| 54 |
-
No
|
| 55 |
-
|
| 56 |
-
Start training
|
| 57 |
-
CLI: crewai train -n -f
|
| 58 |
-
or Python: crew.train(...)
|
| 59 |
-
|
| 60 |
-
Setup training mode
|
| 61 |
-
- task.human_input = true
|
| 62 |
-
- disable delegation
|
| 63 |
-
- init training_data.pkl + trained file
|
| 64 |
-
|
| 65 |
-
Iteration i
|
| 66 |
-
initial_output
|
| 67 |
-
|
| 68 |
-
User human_feedback
|
| 69 |
-
|
| 70 |
-
improved_output
|
| 71 |
-
|
| 72 |
-
Append to training_data.pkl
|
| 73 |
-
by agent_id and iteration
|
| 74 |
-
|
| 75 |
-
More iterations?
|
| 76 |
-
|
| 77 |
-
Evaluate per agent
|
| 78 |
-
aggregate iterations
|
| 79 |
-
|
| 80 |
-
Consolidate
|
| 81 |
-
suggestions[] + quality + final_summary
|
| 82 |
-
|
| 83 |
-
Save by agent role to trained file
|
| 84 |
-
(default: trained_agents_data.pkl)
|
| 85 |
-
|
| 86 |
-
Normal (non-training) runs
|
| 87 |
-
|
| 88 |
-
Auto-load suggestions
|
| 89 |
-
from trained_agents_data.pkl
|
| 90 |
-
|
| 91 |
-
Append to prompt
|
| 92 |
-
for consistent improvements
|
| 93 |
-
|
| 94 |
-
During training runs
|
| 95 |
-
|
| 96 |
-
On each iteration, the system records for every agent:
|
| 97 |
-
initial_output: the agent’s first answer
|
| 98 |
-
human_feedback: your inline feedback when prompted
|
| 99 |
-
improved_output: the agent’s follow-up answer after feedback
|
| 100 |
-
This data is stored in a working file named training_data.pkl keyed by the agent’s internal ID and iteration.
|
| 101 |
-
While training is active, the agent automatically appends your prior human feedback to its prompt to enforce those instructions on subsequent attempts within the training session. Training is interactive: tasks set human_input = true, so running in a non-interactive environment will block on user input.
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
After training completes
|
| 105 |
-
|
| 106 |
-
When train(...) finishes, CrewAI evaluates the collected training data per agent and produces a consolidated result containing:
|
| 107 |
-
suggestions: clear, actionable instructions distilled from your feedback and the difference between initial/improved outputs
|
| 108 |
-
quality: a 0–10 score capturing improvement
|
| 109 |
-
final_summary: a step-by-step set of action items for future tasks
|
| 110 |
-
These consolidated results are saved to the filename you pass to train(...) (default via CLI is trained_agents_data.pkl). Entries are keyed by the agent’s role so they can be applied across sessions.
|
| 111 |
-
During normal (non-training) execution, each agent automatically loads its consolidated suggestions and appends them to the task prompt as mandatory instructions. This gives you consistent improvements without changing your agent definitions.
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
File summary
|
| 115 |
-
|
| 116 |
-
training_data.pkl (ephemeral, per-session):
|
| 117 |
-
Structure: agent_id -> { iteration_number: { initial_output, human_feedback, improved_output } }
|
| 118 |
-
Purpose: capture raw data and human feedback during training
|
| 119 |
-
Location: saved in the current working directory (CWD)
|
| 120 |
-
trained_agents_data.pkl (or your custom filename):
|
| 121 |
-
Structure: agent_role -> { suggestions: string[], quality: number, final_summary: string }
|
| 122 |
-
Purpose: persist consolidated guidance for future runs
|
| 123 |
-
Location: written to the CWD by default; use -f to set a custom (including absolute) path
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
Small Language Model Considerations
|
| 127 |
-
When using smaller language models (≤7B parameters) for training data evaluation, be aware that they may face challenges with generating structured outputs and following complex instructions.
|
| 128 |
-
|
| 129 |
-
Limitations of Small Models in Training Evaluation
|
| 130 |
-
JSON Output Accuracy
|
| 131 |
-
Smaller models often struggle with producing valid JSON responses needed for structured training evaluations, leading to parsing errors and incomplete data.
|
| 132 |
-
Evaluation Quality
|
| 133 |
-
Models under 7B parameters may provide less nuanced evaluations with limited reasoning depth compared to larger models.
|
| 134 |
-
Instruction Following
|
| 135 |
-
Complex training evaluation criteria may not be fully followed or considered by smaller models.
|
| 136 |
-
Consistency
|
| 137 |
-
Evaluations across multiple training iterations may lack consistency with smaller models.
|
| 138 |
-
|
| 139 |
-
Recommendations for Training
|
| 140 |
-
|
| 141 |
-
Best Practice
|
| 142 |
-
Small Model Usage
|
| 143 |
-
|
| 144 |
-
For optimal training quality and reliable evaluations, we strongly recommend using models with at least 7B parameters or larger:
|
| 145 |
-
|
| 146 |
-
from crewai import Agent, Crew, Task, LLM
|
| 147 |
-
|
| 148 |
-
# Recommended minimum for training evaluation
|
| 149 |
-
llm = LLM(model="mistral/open-mistral-7b")
|
| 150 |
-
|
| 151 |
-
# Better options for reliable training evaluation
|
| 152 |
-
llm = LLM(model="anthropic/claude-3-sonnet-20240229-v1:0")
|
| 153 |
-
llm = LLM(model="gpt-4o")
|
| 154 |
-
|
| 155 |
-
# Use this LLM with your agents
|
| 156 |
-
agent = Agent(
|
| 157 |
-
role="Training Evaluator",
|
| 158 |
-
goal="Provide accurate training feedback",
|
| 159 |
-
llm=llm
|
| 160 |
-
)
|
| 161 |
-
|
| 162 |
-
More powerful models provide higher quality feedback with better reasoning, leading to more effective training iterations.
|
| 163 |
-
|
| 164 |
-
Key Points to Note
|
| 165 |
-
|
| 166 |
-
Positive Integer Requirement: Ensure that the number of iterations (n_iterations) is a positive integer. The code will raise a ValueError if this condition is not met.
|
| 167 |
-
Filename Requirement: Ensure that the filename ends with .pkl. The code will raise a ValueError if this condition is not met.
|
| 168 |
-
Error Handling: The code handles subprocess errors and unexpected exceptions, providing error messages to the user.
|
| 169 |
-
Trained guidance is applied at prompt time; it does not modify your Python/YAML agent configuration.
|
| 170 |
-
Agents automatically load trained suggestions from a file named trained_agents_data.pkl located in the current working directory. If you trained to a different filename, pass that path with Crew(trained_agents_file="my_custom_trained.pkl"), set CREWAI_TRAINED_AGENTS_FILE, or use crewai run -f my_custom_trained.pkl.
|
| 171 |
-
You can change the output filename when calling crewai train with -f/--filename. Absolute paths are supported if you want to save outside the CWD.
|
| 172 |
-
|
| 173 |
-
It is important to note that the training process may take some time, depending on the complexity of your agents and will also require your feedback on each iteration. Once the training is complete, your agents will be equipped with enhanced capabilities and knowledge, ready to tackle complex tasks and provide more consistent and valuable insights. Remember to regularly update and retrain your agents to ensure they stay up-to-date with the latest information and advancements in the field.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
doc/doc_phoenix_tracing.md
DELETED
|
@@ -1,93 +0,0 @@
|
|
| 1 |
-
CrewAI
|
| 2 |
-
CrewAI Tracing
|
| 3 |
-
|
| 4 |
-
Instrument multi-agent applications using CrewAI
|
| 5 |
-
https://storage.googleapis.com/arize-phoenix-assets/assets/images/phoenix-docs-images/gc.ico
|
| 6 |
-
Google Colab
|
| 7 |
-
colab.research.google.com
|
| 8 |
-
|
| 9 |
-
Install
|
| 10 |
-
|
| 11 |
-
pip install openinference-instrumentation-crewai crewai crewai-tools
|
| 12 |
-
|
| 13 |
-
CrewAI uses either Langchain or LiteLLM under the hood to call models, depending on the version. If you’re using CrewAI<0.63.0, we recommend installing our openinference-instrumentation-langchain library to get visibility of LLM calls. If you’re using CrewAI>= 0.63.0, we recommend instead adding our openinference-instrumentation-litellm library to get visibility of LLM calls.
|
| 14 |
-
|
| 15 |
-
Setup
|
| 16 |
-
Connect to your Phoenix instance using the register function.
|
| 17 |
-
|
| 18 |
-
from phoenix.otel import register
|
| 19 |
-
|
| 20 |
-
# configure the Phoenix tracer
|
| 21 |
-
tracer_provider = register(
|
| 22 |
-
project_name="my-llm-app", # Default is 'default'
|
| 23 |
-
auto_instrument=True # Auto-instrument your app based on installed OI dependencies
|
| 24 |
-
)
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
Run CrewAI
|
| 28 |
-
From here, you can run CrewAI as normal
|
| 29 |
-
|
| 30 |
-
import os
|
| 31 |
-
from crewai import Agent, Task, Crew, Process
|
| 32 |
-
from crewai_tools import SerperDevTool
|
| 33 |
-
|
| 34 |
-
os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_API_KEY"
|
| 35 |
-
os.environ["SERPER_API_KEY"] = "YOUR_SERPER_API_KEY"
|
| 36 |
-
search_tool = SerperDevTool()
|
| 37 |
-
|
| 38 |
-
# Define your agents with roles and goals
|
| 39 |
-
researcher = Agent(
|
| 40 |
-
role='Senior Research Analyst',
|
| 41 |
-
goal='Uncover cutting-edge developments in AI and data science',
|
| 42 |
-
backstory="""You work at a leading tech think tank.
|
| 43 |
-
Your expertise lies in identifying emerging trends.
|
| 44 |
-
You have a knack for dissecting complex data and presenting actionable insights.""",
|
| 45 |
-
verbose=True,
|
| 46 |
-
allow_delegation=False,
|
| 47 |
-
# You can pass an optional llm attribute specifying what model you wanna use.
|
| 48 |
-
# llm=ChatOpenAI(model_name="gpt-3.5", temperature=0.7),
|
| 49 |
-
tools=[search_tool]
|
| 50 |
-
)
|
| 51 |
-
writer = Agent(
|
| 52 |
-
role='Tech Content Strategist',
|
| 53 |
-
goal='Craft compelling content on tech advancements',
|
| 54 |
-
backstory="""You are a renowned Content Strategist, known for your insightful and engaging articles.
|
| 55 |
-
You transform complex concepts into compelling narratives.""",
|
| 56 |
-
verbose=True,
|
| 57 |
-
allow_delegation=True
|
| 58 |
-
)
|
| 59 |
-
|
| 60 |
-
# Create tasks for your agents
|
| 61 |
-
task1 = Task(
|
| 62 |
-
description="""Conduct a comprehensive analysis of the latest advancements in AI in 2024.
|
| 63 |
-
Identify key trends, breakthrough technologies, and potential industry impacts.""",
|
| 64 |
-
expected_output="Full analysis report in bullet points",
|
| 65 |
-
agent=researcher
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
task2 = Task(
|
| 69 |
-
description="""Using the insights provided, develop an engaging blog
|
| 70 |
-
post that highlights the most significant AI advancements.
|
| 71 |
-
Your post should be informative yet accessible, catering to a tech-savvy audience.
|
| 72 |
-
Make it sound cool, avoid complex words so it doesn't sound like AI.""",
|
| 73 |
-
expected_output="Full blog post of at least 4 paragraphs",
|
| 74 |
-
agent=writer
|
| 75 |
-
)
|
| 76 |
-
|
| 77 |
-
# Instantiate your crew with a sequential process
|
| 78 |
-
crew = Crew(
|
| 79 |
-
agents=[researcher, writer],
|
| 80 |
-
tasks=[task1, task2],
|
| 81 |
-
verbose=True, # Enable verbose logging
|
| 82 |
-
process = Process.sequential
|
| 83 |
-
)
|
| 84 |
-
|
| 85 |
-
# Get your crew to work!
|
| 86 |
-
result = crew.kickoff()
|
| 87 |
-
|
| 88 |
-
print("######################")
|
| 89 |
-
print(result)
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
Observe
|
| 93 |
-
Now that you have tracing setup, all calls to your Crew will be streamed to your running Phoenix for observability and evaluation.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
|
@@ -4,9 +4,8 @@ modal>=1.5.0
|
|
| 4 |
crewai-tools>=1.9.3
|
| 5 |
litellm>=1.88.1
|
| 6 |
httpx>=0.28.1
|
| 7 |
-
gradio>=6.17.3
|
| 8 |
opentelemetry-sdk>=1.34.0
|
| 9 |
opentelemetry-semantic-conventions>=0.55b0
|
| 10 |
arize-phoenix-otel
|
| 11 |
openinference-instrumentation-crewai>=1.1.9
|
| 12 |
-
openinference-instrumentation-
|
|
|
|
| 4 |
crewai-tools>=1.9.3
|
| 5 |
litellm>=1.88.1
|
| 6 |
httpx>=0.28.1
|
|
|
|
| 7 |
opentelemetry-sdk>=1.34.0
|
| 8 |
opentelemetry-semantic-conventions>=0.55b0
|
| 9 |
arize-phoenix-otel
|
| 10 |
openinference-instrumentation-crewai>=1.1.9
|
| 11 |
+
openinference-instrumentation-openai>=0.1.51
|
setup_env.sh
DELETED
|
@@ -1,78 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env bash
|
| 2 |
-
set -euo pipefail
|
| 3 |
-
|
| 4 |
-
ENV_NAME="${1:-crewai_test}"
|
| 5 |
-
ENV_DIR="/Applications/anaconda3/envs/$ENV_NAME"
|
| 6 |
-
ENV_PYTHON="$ENV_DIR/bin/python"
|
| 7 |
-
ENV_PIP="$ENV_PYTHON -m pip"
|
| 8 |
-
|
| 9 |
-
echo ">>> Creating conda env: $ENV_NAME"
|
| 10 |
-
conda create -n "$ENV_NAME" python=3.12 -y
|
| 11 |
-
|
| 12 |
-
# ------------------------------------------------------------------
|
| 13 |
-
# DEPENDENCY RESOLUTION STRATEGY
|
| 14 |
-
#
|
| 15 |
-
# pip's resolver refuses to install packages with conflicting
|
| 16 |
-
# declared metadata, even when they work fine at runtime. The known
|
| 17 |
-
# conflicts are:
|
| 18 |
-
#
|
| 19 |
-
# crewai 1.9.3 → openai~=1.83, aiosqlite~=0.21, opentelemetry-sdk~=1.34, pydantic~=2.11, tokenizers~=0.20
|
| 20 |
-
# litellm 1.88.1 → openai>=2.20, tokenizers>=0.21
|
| 21 |
-
# arize-phoenix 17.2.0 → aiosqlite>=0.22, opentelemetry-sdk (any), pydantic-ai-slim (any)
|
| 22 |
-
#
|
| 23 |
-
# At runtime all version pairs are compatible. The workaround is to
|
| 24 |
-
# install in layers with --no-deps for the conflicting packages.
|
| 25 |
-
#
|
| 26 |
-
# Step 1 — Install base deps (pip resolves these cleanly).
|
| 27 |
-
# Step 2 — Override openai + litellm (--no-deps bypasses the ~=1.83/>=2.20 clash).
|
| 28 |
-
# Step 3 — Override OTel to 1.42.x (phoenix needs semconv attrs missing from 0.55).
|
| 29 |
-
# Step 4 — Install phoenix's heavy transitive deps (scikit-learn, pydantic-ai-slim, …).
|
| 30 |
-
# Step 5 — Install arize-phoenix itself (--no-deps bypasses aiosqlite/OTel clash).
|
| 31 |
-
# ------------------------------------------------------------------
|
| 32 |
-
|
| 33 |
-
echo ">>> Step 1 — base packages"
|
| 34 |
-
$ENV_PIP install \
|
| 35 |
-
crewai==1.9.3 crewai-tools==1.9.3 \
|
| 36 |
-
python-dotenv==1.1.1 httpx==0.28.1 modal==1.5.0 gradio==6.17.3 \
|
| 37 |
-
openinference-instrumentation-crewai==1.1.9 \
|
| 38 |
-
openinference-instrumentation-openai==0.1.51 \
|
| 39 |
-
--only-binary :all:
|
| 40 |
-
|
| 41 |
-
echo ">>> Step 2 — upgrade openai + install litellm (--no-deps)"
|
| 42 |
-
$ENV_PIP install "openai>=2.20.0,<3.0.0" litellm==1.88.1 fastuuid --no-deps --force-reinstall
|
| 43 |
-
|
| 44 |
-
echo ">>> Step 3 — upgrade OTel packages for phoenix compat (--no-deps)"
|
| 45 |
-
$ENV_PIP install \
|
| 46 |
-
"opentelemetry-sdk>=1.42.0" \
|
| 47 |
-
"opentelemetry-semantic-conventions>=0.60b0" \
|
| 48 |
-
"opentelemetry-api>=1.42.0" \
|
| 49 |
-
"opentelemetry-exporter-otlp>=1.42.0" \
|
| 50 |
-
"opentelemetry-proto>=1.42.0" \
|
| 51 |
-
--no-deps --force-reinstall
|
| 52 |
-
|
| 53 |
-
echo ">>> Step 4 — phoenix transitive deps"
|
| 54 |
-
$ENV_PIP install \
|
| 55 |
-
aioitertools alembic email-validator authlib joserfc jsonpath-ng ldap3 \
|
| 56 |
-
grpc-interceptor prometheus-client psutil pystache sqlean-py strawberry-graphql \
|
| 57 |
-
jmespath "aiosqlite>=0.22.1" \
|
| 58 |
-
"pydantic-ai-slim>=1.95.0" anthropic "google-genai>=1.0.0" \
|
| 59 |
-
"mcp>=1.27.0" "tokenizers>=0.21.0" \
|
| 60 |
-
--only-binary :all:
|
| 61 |
-
|
| 62 |
-
echo ">>> Step 5 — scikit-learn metadata (phoenix reads its version at import)"
|
| 63 |
-
$ENV_PIP install scikit-learn --force-reinstall --only-binary :all:
|
| 64 |
-
|
| 65 |
-
echo ">>> Step 6 — arize-phoenix itself (--no-deps)"
|
| 66 |
-
$ENV_PIP install arize-phoenix==17.2.0 arize-phoenix-client arize-phoenix-evals arize-phoenix-otel --no-deps
|
| 67 |
-
|
| 68 |
-
echo ">>> Verifying import..."
|
| 69 |
-
$ENV_PYTHON -c "
|
| 70 |
-
from phoenix.otel import register
|
| 71 |
-
from openinference.instrumentation.openai import OpenAIInstrumentor
|
| 72 |
-
from crew2 import run_pipeline, generate_image, generate_voice, transcribe_audio
|
| 73 |
-
print('All imports OK')
|
| 74 |
-
" 2>&1 | grep -E '^All|Traceback|Error'
|
| 75 |
-
|
| 76 |
-
echo ""
|
| 77 |
-
echo "=== Setup complete: $ENV_NAME ==="
|
| 78 |
-
echo "Run: conda activate $ENV_NAME && python app.py"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
skills/gemma4-image-prompting.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: gemma4-image-prompting
|
| 3 |
-
description: Guidelines for translating user concepts into highly detailed, structured text prompts optimized for Gemma 4 25B's multimodal understanding.
|
| 4 |
-
metadata:
|
| 5 |
-
author: your-team
|
| 6 |
-
version: "1.0"
|
| 7 |
-
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
# Gemma 4 25B Image Prompting Guidelines
|
| 11 |
-
|
| 12 |
-
When engineering an image prompt for Gemma 4 25B, structure the output using the following checklist to maximize visual fidelity and adherence:
|
| 13 |
-
|
| 14 |
-
Core Subject & Action: Clearly define the main subject, their posture, expression, and what they are actively doing. Use concrete nouns instead of vague metaphors.
|
| 15 |
-
|
| 16 |
-
Environment & Lighting: Specify the setting (e.g., cyberpunk alley, serene alpine lake) and the lighting conditions (e.g., volumetric golden hour light, harsh neon rim lighting, diffused overcast daylight).
|
| 17 |
-
|
| 18 |
-
Style & Medium: Explicitly state the artistic medium (e.g., photorealistic 35mm photograph, cinematic film still, oil painting, 3D claymation) to guide the model's rendering style.
|
| 19 |
-
|
| 20 |
-
Composition & Camera: Define the framing and camera technicals (e.g., close-up macro shot, wide-angle bird's-eye view, shallow depth of field with a blurred background).
|
| 21 |
-
|
| 22 |
-
Negative Weights (Optional): List elements to avoid (e.g., text, motion blur, extra limbs) if the user specifies constraints.
|
| 23 |
-
|
| 24 |
-
Optimization Matrix
|
| 25 |
-
|
| 26 |
-
Photorealism: Include camera body/lens models (e.g., "shot on Sony A7R V, 85mm lens"), aperture ($f/1.4$), and specific texture details (e.g., "skin pores, fabric weave").
|
| 27 |
-
|
| 28 |
-
Stylized Art: Reference specific art movements, color palettes (e.g., "muted earth tones", "vibrant synthwave pastel"), or historical eras rather than generic words like "beautiful" or "stunning".
|
| 29 |
-
|
| 30 |
-
Gemma 4 Constraints: Keep the final prompt under 250 words. Avoid buzzwords like "photorealistic 8k HDR"—instead, describe the details that imply high quality.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
skills/semantic-clarification.md
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
---
|
| 2 |
-
name: semantic-clarification
|
| 3 |
-
description: Guidelines for analyzing user input, understanding its semantic intent, and systematically asking clarifying questions if the request is ambiguous or incomplete.
|
| 4 |
-
metadata:
|
| 5 |
-
author: your-team
|
| 6 |
-
version: "1.0"
|
| 7 |
-
|
| 8 |
-
---
|
| 9 |
-
|
| 10 |
-
# Semantic Clarification Guidelines
|
| 11 |
-
|
| 12 |
-
When processing a user prompt or request, follow this checklist to ensure complete understanding before execution:
|
| 13 |
-
|
| 14 |
-
Intent Extraction: Identify the core goal of the user, filtering out conversational noise and focusing on the underlying objective.
|
| 15 |
-
|
| 16 |
-
Ambiguity Detection: Look for vague terminology, missing parameters, or conflicting instructions that could lead to multiple interpretations.
|
| 17 |
-
|
| 18 |
-
Context Matching: Evaluate if the input provides enough context (e.g., target audience, technical constraints, or format preferences) to deliver a high-quality output.
|
| 19 |
-
|
| 20 |
-
Proactive Probing: If gaps are found, formulate a polite, precise, and structured question to gather the missing pieces without overwhelming the user.
|
| 21 |
-
|
| 22 |
-
Action Thresholds
|
| 23 |
-
|
| 24 |
-
Critical (Block & Ask): Missing the core objective or completely contradictory instructions → Stop execution and ask for immediate clarification.
|
| 25 |
-
|
| 26 |
-
Major (Proceed with Assumptions): Core objective is clear, but key preferences (like format or scope) are missing → State your assumptions clearly and ask the user to confirm or adjust.
|
| 27 |
-
|
| 28 |
-
Minor (Proceed Fully): The request is clear and actionable, with only trivial details left out → Execute immediately and provide the output.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/__init__.py
DELETED
|
File without changes
|
tests/test_app.py
DELETED
|
@@ -1,84 +0,0 @@
|
|
| 1 |
-
from unittest.mock import ANY, MagicMock, patch
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
from app import process
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
@pytest.fixture(autouse=True)
|
| 9 |
-
def reset_app_mocks():
|
| 10 |
-
"""Replace app module's crew2 imports with mocks before each test."""
|
| 11 |
-
import app as _app_mod
|
| 12 |
-
|
| 13 |
-
_app_mod.run_pipeline = MagicMock(
|
| 14 |
-
return_value={
|
| 15 |
-
"prompt": "A dragon",
|
| 16 |
-
"result_corroborate": "Evidence 1",
|
| 17 |
-
"result_opposite": "Counter 1",
|
| 18 |
-
}
|
| 19 |
-
)
|
| 20 |
-
_app_mod.generate_image = MagicMock(return_value=b"fake_png_bytes")
|
| 21 |
-
_app_mod.generate_caption = MagicMock(
|
| 22 |
-
return_value={"caption": "A beautiful narration.", "voice_style": "girl"}
|
| 23 |
-
)
|
| 24 |
-
_app_mod.generate_voice = MagicMock(return_value=b"fake_wav_bytes")
|
| 25 |
-
_app_mod.transcribe_audio = MagicMock(return_value="transcribed text")
|
| 26 |
-
|
| 27 |
-
# Also mock PIL.Image.open
|
| 28 |
-
patcher = patch("PIL.Image.open", return_value="fake_image_object")
|
| 29 |
-
patcher.start()
|
| 30 |
-
yield
|
| 31 |
-
patcher.stop()
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
class TestProcess:
|
| 35 |
-
def test_text_input_returns_all_outputs(self):
|
| 36 |
-
image, audio_path, caption, style = process("test statement", None)
|
| 37 |
-
|
| 38 |
-
import app as _app_mod
|
| 39 |
-
|
| 40 |
-
assert image == "fake_image_object"
|
| 41 |
-
assert isinstance(audio_path, str)
|
| 42 |
-
assert audio_path.endswith(".wav")
|
| 43 |
-
assert caption == "A beautiful narration."
|
| 44 |
-
assert style == "girl"
|
| 45 |
-
|
| 46 |
-
_app_mod.run_pipeline.assert_called_once_with("test statement", session_id=ANY)
|
| 47 |
-
_app_mod.transcribe_audio.assert_not_called()
|
| 48 |
-
|
| 49 |
-
def test_audio_input_transcribes_first(self):
|
| 50 |
-
process(None, "/path/to/audio.wav")
|
| 51 |
-
|
| 52 |
-
import app as _app_mod
|
| 53 |
-
|
| 54 |
-
_app_mod.transcribe_audio.assert_called_once_with("/path/to/audio.wav", session_id=ANY)
|
| 55 |
-
_app_mod.run_pipeline.assert_called_once_with("transcribed text", session_id=ANY)
|
| 56 |
-
|
| 57 |
-
def test_empty_text_raises_error(self):
|
| 58 |
-
with pytest.raises(Exception, match="provide text"):
|
| 59 |
-
process("", None)
|
| 60 |
-
|
| 61 |
-
def test_none_input_raises_error(self):
|
| 62 |
-
with pytest.raises(Exception, match="provide text"):
|
| 63 |
-
process(None, None)
|
| 64 |
-
|
| 65 |
-
def test_caption_not_found_fallback(self):
|
| 66 |
-
import app as _app_mod
|
| 67 |
-
|
| 68 |
-
_app_mod.generate_caption.return_value = {
|
| 69 |
-
"caption": None,
|
| 70 |
-
"voice_style": None,
|
| 71 |
-
}
|
| 72 |
-
|
| 73 |
-
image, audio_path, caption, style = process("test", None)
|
| 74 |
-
|
| 75 |
-
assert caption == "(not found)"
|
| 76 |
-
assert style == "(not found)"
|
| 77 |
-
|
| 78 |
-
def test_audio_has_priority_over_text(self):
|
| 79 |
-
import app as _app_mod
|
| 80 |
-
|
| 81 |
-
process("ignored text", "/path/to/audio.wav")
|
| 82 |
-
|
| 83 |
-
_app_mod.transcribe_audio.assert_called_once_with("/path/to/audio.wav", session_id=ANY)
|
| 84 |
-
_app_mod.run_pipeline.assert_called_once_with("transcribed text", session_id=ANY)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_crew2.py
DELETED
|
@@ -1,207 +0,0 @@
|
|
| 1 |
-
import time
|
| 2 |
-
from unittest.mock import MagicMock, patch
|
| 3 |
-
|
| 4 |
-
import pytest
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
# Patch crew-level imports BEFORE importing crew2
|
| 8 |
-
with (
|
| 9 |
-
patch("crew2.search_tool", MagicMock()),
|
| 10 |
-
patch("crew2.llm", MagicMock()),
|
| 11 |
-
patch("crew2._get_flux_url", return_value="http://test-flux"),
|
| 12 |
-
patch("crew2._get_vox_url", return_value="http://test-vox"),
|
| 13 |
-
patch("crew2._get_transcribe_url", return_value="http://test-transcribe"),
|
| 14 |
-
):
|
| 15 |
-
from crew2 import (
|
| 16 |
-
_run_with_timeout,
|
| 17 |
-
generate_caption,
|
| 18 |
-
generate_image,
|
| 19 |
-
generate_voice,
|
| 20 |
-
run_pipeline,
|
| 21 |
-
transcribe_audio,
|
| 22 |
-
)
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
class TestRunWithTimeout:
|
| 26 |
-
def test_returns_result_when_fn_completes(self):
|
| 27 |
-
def fast():
|
| 28 |
-
return 42
|
| 29 |
-
|
| 30 |
-
assert _run_with_timeout(fast) == 42
|
| 31 |
-
|
| 32 |
-
def test_returns_fallback_on_timeout(self):
|
| 33 |
-
def slow():
|
| 34 |
-
time.sleep(10)
|
| 35 |
-
return 99
|
| 36 |
-
|
| 37 |
-
result = _run_with_timeout(slow, timeout=0.1)
|
| 38 |
-
assert result == "The search timed out. Using general knowledge as fallback."
|
| 39 |
-
|
| 40 |
-
def test_returns_result_with_args_via_lambda(self):
|
| 41 |
-
assert _run_with_timeout(lambda: "hello") == "hello"
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
class TestRunPipeline:
|
| 45 |
-
@patch("crew2.Crew")
|
| 46 |
-
def test_returns_dict_with_expected_keys(self, MockCrew):
|
| 47 |
-
mock_crew_instance = MagicMock()
|
| 48 |
-
mock_crew_instance.kickoff.return_value = "IMAGE PROMPT: A beautiful scene"
|
| 49 |
-
MockCrew.return_value = mock_crew_instance
|
| 50 |
-
|
| 51 |
-
result = run_pipeline("test statement")
|
| 52 |
-
|
| 53 |
-
assert isinstance(result, dict)
|
| 54 |
-
assert "prompt" in result
|
| 55 |
-
assert "result_corroborate" in result
|
| 56 |
-
assert "result_opposite" in result
|
| 57 |
-
assert result["prompt"] == "A beautiful scene"
|
| 58 |
-
|
| 59 |
-
@patch("crew2.Crew")
|
| 60 |
-
def test_prompt_parsing_case_sensitive(self, MockCrew):
|
| 61 |
-
mock_crew_instance = MagicMock()
|
| 62 |
-
mock_crew_instance.kickoff.return_value = (
|
| 63 |
-
"Some text\nIMAGE PROMPT: Dragon in the sky\nmore"
|
| 64 |
-
)
|
| 65 |
-
MockCrew.return_value = mock_crew_instance
|
| 66 |
-
|
| 67 |
-
result = run_pipeline("dragons")
|
| 68 |
-
assert result["prompt"] == "Dragon in the sky"
|
| 69 |
-
|
| 70 |
-
@patch("crew2.Crew")
|
| 71 |
-
def test_prompt_none_when_missing(self, MockCrew):
|
| 72 |
-
mock_crew_instance = MagicMock()
|
| 73 |
-
mock_crew_instance.kickoff.return_value = "No image prompt here"
|
| 74 |
-
MockCrew.return_value = mock_crew_instance
|
| 75 |
-
|
| 76 |
-
result = run_pipeline("no image")
|
| 77 |
-
assert result["prompt"] is None
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
class TestGenerateImage:
|
| 81 |
-
@patch("httpx.post")
|
| 82 |
-
def test_returns_bytes_on_success(self, mock_post):
|
| 83 |
-
mock_response = MagicMock()
|
| 84 |
-
mock_response.content = b"fake_png_bytes"
|
| 85 |
-
mock_post.return_value = mock_response
|
| 86 |
-
|
| 87 |
-
result = generate_image("test prompt")
|
| 88 |
-
assert result == b"fake_png_bytes"
|
| 89 |
-
mock_post.assert_called_once()
|
| 90 |
-
assert mock_post.call_args[1]["json"]["prompt"] == "test prompt"
|
| 91 |
-
assert mock_post.call_args[1]["json"]["steps"] == 30
|
| 92 |
-
|
| 93 |
-
@patch("httpx.post")
|
| 94 |
-
def test_raises_on_http_error(self, mock_post):
|
| 95 |
-
mock_response = MagicMock()
|
| 96 |
-
mock_response.raise_for_status.side_effect = Exception("HTTP 500")
|
| 97 |
-
mock_post.return_value = mock_response
|
| 98 |
-
|
| 99 |
-
with pytest.raises(Exception, match="HTTP 500"):
|
| 100 |
-
generate_image("fail")
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
class TestGenerateCaption:
|
| 104 |
-
def _make_httpx_response(self, text):
|
| 105 |
-
mock_resp = MagicMock()
|
| 106 |
-
mock_resp.json.return_value = {
|
| 107 |
-
"choices": [{"message": {"content": text}}]
|
| 108 |
-
}
|
| 109 |
-
return mock_resp
|
| 110 |
-
|
| 111 |
-
@patch("httpx.post")
|
| 112 |
-
def test_parses_caption_and_style(self, mock_post):
|
| 113 |
-
mock_post.return_value = self._make_httpx_response(
|
| 114 |
-
"CAPTION: A beautiful narration about the subject.\nVOICE_STYLE: gentle narrator"
|
| 115 |
-
)
|
| 116 |
-
|
| 117 |
-
result = generate_caption("corr", "opp", "img prompt")
|
| 118 |
-
|
| 119 |
-
assert result["caption"] == "A beautiful narration about the subject."
|
| 120 |
-
assert result["voice_style"] == "gentle narrator"
|
| 121 |
-
|
| 122 |
-
@patch("httpx.post")
|
| 123 |
-
def test_missing_caption_returns_none(self, mock_post):
|
| 124 |
-
mock_post.return_value = self._make_httpx_response(
|
| 125 |
-
"VOICE_STYLE: deep voice"
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
result = generate_caption("c", "o", "p")
|
| 129 |
-
assert result["caption"] is None
|
| 130 |
-
assert result["voice_style"] == "deep voice"
|
| 131 |
-
|
| 132 |
-
@patch("httpx.post")
|
| 133 |
-
def test_missing_voice_style_returns_none(self, mock_post):
|
| 134 |
-
mock_post.return_value = self._make_httpx_response(
|
| 135 |
-
"CAPTION: Some narration here."
|
| 136 |
-
)
|
| 137 |
-
|
| 138 |
-
result = generate_caption("c", "o", "p")
|
| 139 |
-
assert result["caption"] == "Some narration here."
|
| 140 |
-
assert result["voice_style"] is None
|
| 141 |
-
|
| 142 |
-
@patch("httpx.post")
|
| 143 |
-
def test_voice_style_lowered(self, mock_post):
|
| 144 |
-
mock_post.return_value = self._make_httpx_response(
|
| 145 |
-
"CAPTION: Cap.\nVOICE_STYLE: Authoritative News Anchor"
|
| 146 |
-
)
|
| 147 |
-
|
| 148 |
-
result = generate_caption("c", "o", "p")
|
| 149 |
-
assert result["voice_style"] == "authoritative news anchor"
|
| 150 |
-
|
| 151 |
-
@patch("httpx.post")
|
| 152 |
-
def test_raises_on_http_error(self, mock_post):
|
| 153 |
-
mock_post.side_effect = Exception("Connection refused")
|
| 154 |
-
|
| 155 |
-
with pytest.raises(Exception, match="Connection refused"):
|
| 156 |
-
generate_caption("c", "o", "p")
|
| 157 |
-
|
| 158 |
-
|
| 159 |
-
class TestGenerateVoice:
|
| 160 |
-
@patch("httpx.post")
|
| 161 |
-
def test_returns_bytes_on_success(self, mock_post):
|
| 162 |
-
mock_response = MagicMock()
|
| 163 |
-
mock_response.content = b"fake_wav_bytes"
|
| 164 |
-
mock_post.return_value = mock_response
|
| 165 |
-
|
| 166 |
-
result = generate_voice("girl", "Hello world")
|
| 167 |
-
assert result == b"fake_wav_bytes"
|
| 168 |
-
call_kwargs = mock_post.call_args[1]
|
| 169 |
-
assert call_kwargs["json"]["text"] == "Hello world"
|
| 170 |
-
assert call_kwargs["json"]["voice_style"] == "girl"
|
| 171 |
-
|
| 172 |
-
@patch("httpx.post")
|
| 173 |
-
def test_raises_on_http_error(self, mock_post):
|
| 174 |
-
mock_response = MagicMock()
|
| 175 |
-
mock_response.raise_for_status.side_effect = Exception("HTTP 503")
|
| 176 |
-
mock_post.return_value = mock_response
|
| 177 |
-
|
| 178 |
-
with pytest.raises(Exception, match="HTTP 503"):
|
| 179 |
-
generate_voice("girl", "fail")
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
class TestTranscribeAudio:
|
| 183 |
-
@patch("httpx.post")
|
| 184 |
-
def test_returns_transcription(self, mock_post):
|
| 185 |
-
mock_response = MagicMock()
|
| 186 |
-
mock_response.json.return_value = {"transcription": "hello world"}
|
| 187 |
-
mock_post.return_value = mock_response
|
| 188 |
-
|
| 189 |
-
with patch("builtins.open") as mock_open:
|
| 190 |
-
mock_file = MagicMock()
|
| 191 |
-
mock_file.read.return_value = b"fake_audio"
|
| 192 |
-
mock_open.return_value.__enter__.return_value = mock_file
|
| 193 |
-
|
| 194 |
-
result = transcribe_audio("/path/to/audio.wav")
|
| 195 |
-
assert result == "hello world"
|
| 196 |
-
|
| 197 |
-
@patch("httpx.post")
|
| 198 |
-
def test_raises_on_http_error(self, mock_post):
|
| 199 |
-
mock_post.side_effect = Exception("HTTP 503")
|
| 200 |
-
|
| 201 |
-
with patch("builtins.open") as mock_open:
|
| 202 |
-
mock_file = MagicMock()
|
| 203 |
-
mock_file.read.return_value = b"fake_audio"
|
| 204 |
-
mock_open.return_value.__enter__.return_value = mock_file
|
| 205 |
-
|
| 206 |
-
with pytest.raises(Exception, match="HTTP 503"):
|
| 207 |
-
transcribe_audio("/path/to/audio.wav")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_flux_generator.py
DELETED
|
@@ -1,72 +0,0 @@
|
|
| 1 |
-
from unittest.mock import MagicMock, patch
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
@pytest.fixture(autouse=True)
|
| 7 |
-
def _mock_modal():
|
| 8 |
-
with (
|
| 9 |
-
patch("modal.App.cls", side_effect=lambda **kw: lambda cls: cls),
|
| 10 |
-
patch("modal.App.function", side_effect=lambda **kw: lambda fn: fn),
|
| 11 |
-
):
|
| 12 |
-
yield
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@pytest.fixture(autouse=True)
|
| 16 |
-
def _mock_heavy_imports():
|
| 17 |
-
with patch.dict(
|
| 18 |
-
"sys.modules",
|
| 19 |
-
{"torch": MagicMock(), "diffusers": MagicMock(), "huggingface_hub": MagicMock()},
|
| 20 |
-
):
|
| 21 |
-
yield
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
class TestFluxGenerator:
|
| 25 |
-
def test_generate_impl_returns_png_bytes(self):
|
| 26 |
-
from flux_generator import FluxGenerator
|
| 27 |
-
|
| 28 |
-
gen = FluxGenerator()
|
| 29 |
-
gen.pipe = MagicMock()
|
| 30 |
-
|
| 31 |
-
mock_image = MagicMock()
|
| 32 |
-
gen.pipe.return_value.images = [mock_image]
|
| 33 |
-
|
| 34 |
-
result = gen._generate_impl("test prompt", 30)
|
| 35 |
-
|
| 36 |
-
gen.pipe.assert_called_once_with(
|
| 37 |
-
prompt="test prompt", num_inference_steps=30, guidance_scale=3.5
|
| 38 |
-
)
|
| 39 |
-
mock_image.save.assert_called_once()
|
| 40 |
-
assert isinstance(result, bytes)
|
| 41 |
-
|
| 42 |
-
def test_generate_delegates_to_impl(self):
|
| 43 |
-
from flux_generator import FluxGenerator
|
| 44 |
-
|
| 45 |
-
gen = FluxGenerator()
|
| 46 |
-
gen._generate_impl = MagicMock(return_value=b"png_data")
|
| 47 |
-
|
| 48 |
-
result = gen._generate("test", 20)
|
| 49 |
-
|
| 50 |
-
assert result == b"png_data"
|
| 51 |
-
gen._generate_impl.assert_called_once_with("test", 20)
|
| 52 |
-
|
| 53 |
-
def test_generate_endpoint_default_prompt(self):
|
| 54 |
-
from flux_generator import FluxGenerator
|
| 55 |
-
|
| 56 |
-
gen = FluxGenerator()
|
| 57 |
-
gen._generate_impl = MagicMock(return_value=b"data")
|
| 58 |
-
|
| 59 |
-
result = gen.generate({"steps": 10})
|
| 60 |
-
|
| 61 |
-
assert result.media_type == "image/png"
|
| 62 |
-
gen._generate_impl.assert_called_once_with("A cat wearing a spacesuit", 10)
|
| 63 |
-
|
| 64 |
-
def test_generate_endpoint_custom_prompt(self):
|
| 65 |
-
from flux_generator import FluxGenerator
|
| 66 |
-
|
| 67 |
-
gen = FluxGenerator()
|
| 68 |
-
gen._generate_impl = MagicMock(return_value=b"data")
|
| 69 |
-
|
| 70 |
-
gen.generate({"prompt": "my prompt", "steps": 5})
|
| 71 |
-
|
| 72 |
-
gen._generate_impl.assert_called_once_with("my prompt", 5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_transcribe_generator.py
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 1 |
-
import base64
|
| 2 |
-
from unittest.mock import MagicMock, patch
|
| 3 |
-
|
| 4 |
-
import pytest
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
@pytest.fixture(autouse=True)
|
| 8 |
-
def _mock_modal():
|
| 9 |
-
with (
|
| 10 |
-
patch("modal.App.cls", side_effect=lambda **kw: lambda cls: cls),
|
| 11 |
-
patch("modal.App.function", side_effect=lambda **kw: lambda fn: fn),
|
| 12 |
-
):
|
| 13 |
-
yield
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
@pytest.fixture(autouse=True)
|
| 17 |
-
def _mock_heavy_imports():
|
| 18 |
-
with patch.dict(
|
| 19 |
-
"sys.modules",
|
| 20 |
-
{
|
| 21 |
-
"torch": MagicMock(),
|
| 22 |
-
"transformers": MagicMock(),
|
| 23 |
-
"librosa": MagicMock(),
|
| 24 |
-
"soundfile": MagicMock(),
|
| 25 |
-
"requests": MagicMock(),
|
| 26 |
-
},
|
| 27 |
-
):
|
| 28 |
-
yield
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
class TestCohereTranscriber:
|
| 32 |
-
def test_transcribe_with_base64_audio(self):
|
| 33 |
-
from transcribe_generator import CohereTranscriber
|
| 34 |
-
|
| 35 |
-
gen = CohereTranscriber()
|
| 36 |
-
gen.device = "cpu"
|
| 37 |
-
gen.processor = MagicMock()
|
| 38 |
-
gen.model = MagicMock()
|
| 39 |
-
|
| 40 |
-
audio_b64 = base64.b64encode(b"fake_wav_data").decode()
|
| 41 |
-
|
| 42 |
-
with (
|
| 43 |
-
patch("soundfile.read", return_value=(MagicMock(ndim=1), 16000)),
|
| 44 |
-
patch.object(
|
| 45 |
-
gen.processor, "batch_decode", return_value=["hello world"]
|
| 46 |
-
),
|
| 47 |
-
):
|
| 48 |
-
result = gen.transcribe({"audio": audio_b64})
|
| 49 |
-
assert "transcription" in result.body.decode()
|
| 50 |
-
|
| 51 |
-
def test_transcribe_with_audio_url(self):
|
| 52 |
-
from transcribe_generator import CohereTranscriber
|
| 53 |
-
|
| 54 |
-
gen = CohereTranscriber()
|
| 55 |
-
gen.device = "cpu"
|
| 56 |
-
gen.processor = MagicMock()
|
| 57 |
-
gen.model = MagicMock()
|
| 58 |
-
|
| 59 |
-
with (
|
| 60 |
-
patch("requests.get") as mock_requests_get,
|
| 61 |
-
patch("soundfile.read", return_value=(MagicMock(ndim=1), 16000)),
|
| 62 |
-
patch.object(
|
| 63 |
-
gen.processor, "batch_decode", return_value=["test"]
|
| 64 |
-
),
|
| 65 |
-
):
|
| 66 |
-
mock_resp = MagicMock()
|
| 67 |
-
mock_resp.content = b"remote_audio"
|
| 68 |
-
mock_requests_get.return_value = mock_resp
|
| 69 |
-
|
| 70 |
-
result = gen.transcribe({"audio_url": "https://example.com/audio.wav"})
|
| 71 |
-
assert "transcription" in result.body.decode()
|
| 72 |
-
|
| 73 |
-
def test_transcribe_no_audio_returns_400(self):
|
| 74 |
-
from transcribe_generator import CohereTranscriber
|
| 75 |
-
|
| 76 |
-
gen = CohereTranscriber()
|
| 77 |
-
gen.model = MagicMock()
|
| 78 |
-
gen.processor = MagicMock()
|
| 79 |
-
|
| 80 |
-
from starlette.responses import JSONResponse
|
| 81 |
-
|
| 82 |
-
result = gen.transcribe({})
|
| 83 |
-
assert isinstance(result, JSONResponse)
|
| 84 |
-
assert result.status_code == 400
|
| 85 |
-
|
| 86 |
-
def test_transcribe_resamples_if_needed(self):
|
| 87 |
-
from transcribe_generator import CohereTranscriber
|
| 88 |
-
|
| 89 |
-
gen = CohereTranscriber()
|
| 90 |
-
gen.device = "cpu"
|
| 91 |
-
gen.processor = MagicMock()
|
| 92 |
-
gen.model = MagicMock()
|
| 93 |
-
|
| 94 |
-
audio_b64 = base64.b64encode(b"test").decode()
|
| 95 |
-
|
| 96 |
-
with (
|
| 97 |
-
patch("soundfile.read", return_value=(MagicMock(ndim=1), 8000)),
|
| 98 |
-
patch("librosa.resample", return_value=MagicMock(ndim=1)) as mock_resample,
|
| 99 |
-
patch.object(
|
| 100 |
-
gen.processor, "batch_decode", return_value=["resampled"]
|
| 101 |
-
),
|
| 102 |
-
):
|
| 103 |
-
gen.transcribe({"audio": audio_b64})
|
| 104 |
-
mock_resample.assert_called_once()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_unsloth_finetune.py
DELETED
|
@@ -1,201 +0,0 @@
|
|
| 1 |
-
from unittest.mock import MagicMock, patch
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
HEAVY_DEPS = {
|
| 7 |
-
"unsloth": MagicMock(),
|
| 8 |
-
"datasets": MagicMock(),
|
| 9 |
-
"torch": MagicMock(),
|
| 10 |
-
"wandb": MagicMock(),
|
| 11 |
-
"transformers": MagicMock(),
|
| 12 |
-
"trl": MagicMock(),
|
| 13 |
-
"unsloth.chat_templates": MagicMock(),
|
| 14 |
-
"unsloth.FastLanguageModel": MagicMock(),
|
| 15 |
-
}
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
@pytest.fixture(autouse=True, scope="class")
|
| 19 |
-
def _mock_heavy_deps():
|
| 20 |
-
"""Keep heavy deps available for module-level imports + subsequent function calls."""
|
| 21 |
-
with (
|
| 22 |
-
patch.dict("sys.modules", HEAVY_DEPS),
|
| 23 |
-
patch("modal.Image.debian_slim"),
|
| 24 |
-
patch("modal.Image.imports"),
|
| 25 |
-
patch("modal.App.cls", side_effect=lambda **kw: lambda cls: cls),
|
| 26 |
-
patch("modal.App.function", side_effect=lambda **kw: lambda fn: fn),
|
| 27 |
-
patch("modal.Retries", return_value=MagicMock()),
|
| 28 |
-
patch("modal.Secret.from_name", return_value=MagicMock()),
|
| 29 |
-
):
|
| 30 |
-
import unsloth_finetune
|
| 31 |
-
|
| 32 |
-
unsloth_finetune.TrainingArguments = MagicMock()
|
| 33 |
-
unsloth_finetune.SFTTrainer = MagicMock()
|
| 34 |
-
yield
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
class TestTrainingConfig:
|
| 38 |
-
def test_default_experiment_name_generated(self):
|
| 39 |
-
import unsloth_finetune as mod
|
| 40 |
-
|
| 41 |
-
config = mod.TrainingConfig(
|
| 42 |
-
model_name="test/model",
|
| 43 |
-
dataset_name="test/data",
|
| 44 |
-
max_seq_length=2048,
|
| 45 |
-
load_in_4bit=True,
|
| 46 |
-
load_in_8bit=False,
|
| 47 |
-
lora_r=16,
|
| 48 |
-
lora_alpha=16,
|
| 49 |
-
lora_dropout=0.0,
|
| 50 |
-
lora_bias="none",
|
| 51 |
-
use_rslora=False,
|
| 52 |
-
optim="adamw_8bit",
|
| 53 |
-
batch_size=4,
|
| 54 |
-
gradient_accumulation_steps=1,
|
| 55 |
-
packing=False,
|
| 56 |
-
use_gradient_checkpointing="unsloth",
|
| 57 |
-
learning_rate=2e-4,
|
| 58 |
-
lr_scheduler_type="cosine",
|
| 59 |
-
warmup_ratio=0.06,
|
| 60 |
-
weight_decay=0.01,
|
| 61 |
-
max_steps=5,
|
| 62 |
-
save_steps=2,
|
| 63 |
-
eval_steps=2,
|
| 64 |
-
logging_steps=1,
|
| 65 |
-
seed=105,
|
| 66 |
-
)
|
| 67 |
-
|
| 68 |
-
assert config.experiment_name is not None
|
| 69 |
-
assert "model" in config.experiment_name
|
| 70 |
-
assert "r16" in config.experiment_name
|
| 71 |
-
|
| 72 |
-
def test_custom_experiment_name_used(self):
|
| 73 |
-
import unsloth_finetune as mod
|
| 74 |
-
|
| 75 |
-
config = mod.TrainingConfig(
|
| 76 |
-
model_name="test/model",
|
| 77 |
-
dataset_name="test/data",
|
| 78 |
-
max_seq_length=2048,
|
| 79 |
-
load_in_4bit=True,
|
| 80 |
-
load_in_8bit=False,
|
| 81 |
-
lora_r=16,
|
| 82 |
-
lora_alpha=16,
|
| 83 |
-
lora_dropout=0.0,
|
| 84 |
-
lora_bias="none",
|
| 85 |
-
use_rslora=False,
|
| 86 |
-
optim="adamw_8bit",
|
| 87 |
-
batch_size=4,
|
| 88 |
-
gradient_accumulation_steps=1,
|
| 89 |
-
packing=False,
|
| 90 |
-
use_gradient_checkpointing="unsloth",
|
| 91 |
-
learning_rate=2e-4,
|
| 92 |
-
lr_scheduler_type="cosine",
|
| 93 |
-
warmup_ratio=0.06,
|
| 94 |
-
weight_decay=0.01,
|
| 95 |
-
max_steps=5,
|
| 96 |
-
save_steps=2,
|
| 97 |
-
eval_steps=2,
|
| 98 |
-
logging_steps=1,
|
| 99 |
-
seed=105,
|
| 100 |
-
experiment_name="my-custom-exp",
|
| 101 |
-
)
|
| 102 |
-
|
| 103 |
-
assert config.experiment_name == "my-custom-exp"
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
class TestGetStructuredPaths:
|
| 107 |
-
def test_returns_dataset_and_checkpoint_keys(self):
|
| 108 |
-
import unsloth_finetune as mod
|
| 109 |
-
|
| 110 |
-
config = mod.TrainingConfig(
|
| 111 |
-
model_name="m", dataset_name="test/data", max_seq_length=1024,
|
| 112 |
-
load_in_4bit=True, load_in_8bit=False, lora_r=8, lora_alpha=8,
|
| 113 |
-
lora_dropout=0.0, lora_bias="none", use_rslora=False,
|
| 114 |
-
optim="adamw_8bit", batch_size=2, gradient_accumulation_steps=1,
|
| 115 |
-
packing=False, use_gradient_checkpointing="unsloth",
|
| 116 |
-
learning_rate=2e-4, lr_scheduler_type="cosine",
|
| 117 |
-
warmup_ratio=0.06, weight_decay=0.01, max_steps=1,
|
| 118 |
-
save_steps=1, eval_steps=1, logging_steps=1, seed=42,
|
| 119 |
-
)
|
| 120 |
-
|
| 121 |
-
paths = mod.get_structured_paths(config)
|
| 122 |
-
|
| 123 |
-
assert "dataset_cache" in paths
|
| 124 |
-
assert "checkpoints" in paths
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
class TestSetupModelForTraining:
|
| 128 |
-
def test_calls_get_peft_model(self):
|
| 129 |
-
import unsloth_finetune as mod
|
| 130 |
-
|
| 131 |
-
config = mod.TrainingConfig(
|
| 132 |
-
model_name="m", dataset_name="d", max_seq_length=1024,
|
| 133 |
-
load_in_4bit=True, load_in_8bit=False, lora_r=8, lora_alpha=8,
|
| 134 |
-
lora_dropout=0.0, lora_bias="none", use_rslora=False,
|
| 135 |
-
optim="adamw_8bit", batch_size=2, gradient_accumulation_steps=1,
|
| 136 |
-
packing=False, use_gradient_checkpointing="unsloth",
|
| 137 |
-
learning_rate=2e-4, lr_scheduler_type="cosine",
|
| 138 |
-
warmup_ratio=0.06, weight_decay=0.01, max_steps=1,
|
| 139 |
-
save_steps=1, eval_steps=1, logging_steps=1, seed=42,
|
| 140 |
-
)
|
| 141 |
-
|
| 142 |
-
with patch.object(mod.FastLanguageModel, "get_peft_model") as mock_get:
|
| 143 |
-
mock_model = MagicMock()
|
| 144 |
-
mod.setup_model_for_training(mock_model, config)
|
| 145 |
-
mock_get.assert_called_once()
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
class TestCreateTrainingArguments:
|
| 149 |
-
def test_returns_training_arguments(self):
|
| 150 |
-
import unsloth_finetune as mod
|
| 151 |
-
|
| 152 |
-
config = mod.TrainingConfig(
|
| 153 |
-
model_name="m", dataset_name="d", max_seq_length=1024,
|
| 154 |
-
load_in_4bit=True, load_in_8bit=False, lora_r=8, lora_alpha=8,
|
| 155 |
-
lora_dropout=0.0, lora_bias="none", use_rslora=False,
|
| 156 |
-
optim="adamw_8bit", batch_size=2, gradient_accumulation_steps=1,
|
| 157 |
-
packing=False, use_gradient_checkpointing="unsloth",
|
| 158 |
-
learning_rate=2e-4, lr_scheduler_type="cosine",
|
| 159 |
-
warmup_ratio=0.06, weight_decay=0.01, max_steps=1,
|
| 160 |
-
save_steps=1, eval_steps=1, logging_steps=1, seed=42,
|
| 161 |
-
)
|
| 162 |
-
|
| 163 |
-
with patch.object(mod, "TrainingArguments") as mock_ta:
|
| 164 |
-
mod.create_training_arguments(config, "/tmp/output")
|
| 165 |
-
mock_ta.assert_called_once()
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
class TestCheckForExistingCheckpoint:
|
| 169 |
-
def test_none_when_no_checkpoint_dir(self):
|
| 170 |
-
import unsloth_finetune as mod
|
| 171 |
-
|
| 172 |
-
with patch("pathlib.Path") as mock_path:
|
| 173 |
-
mock_path.return_value.exists.return_value = False
|
| 174 |
-
result = mod.check_for_existing_checkpoint({"checkpoints": mock_path.return_value})
|
| 175 |
-
assert result is None
|
| 176 |
-
|
| 177 |
-
def test_none_when_no_checkpoints(self):
|
| 178 |
-
import unsloth_finetune as mod
|
| 179 |
-
|
| 180 |
-
with patch("pathlib.Path") as mock_path:
|
| 181 |
-
mock_path.return_value.exists.return_value = True
|
| 182 |
-
mock_path.return_value.glob.return_value = []
|
| 183 |
-
result = mod.check_for_existing_checkpoint({"checkpoints": mock_path.return_value})
|
| 184 |
-
assert result is None
|
| 185 |
-
|
| 186 |
-
def test_returns_latest_checkpoint(self):
|
| 187 |
-
import unsloth_finetune as mod
|
| 188 |
-
|
| 189 |
-
with patch("pathlib.Path") as mock_path:
|
| 190 |
-
mock_path.return_value.exists.return_value = True
|
| 191 |
-
mock_cp1 = MagicMock()
|
| 192 |
-
mock_cp1.name = "checkpoint-100"
|
| 193 |
-
mock_cp1.__str__.return_value = "/checkpoint-100"
|
| 194 |
-
mock_cp2 = MagicMock()
|
| 195 |
-
mock_cp2.name = "checkpoint-200"
|
| 196 |
-
mock_cp2.__str__.return_value = "/checkpoint-200"
|
| 197 |
-
mock_path.return_value.glob.return_value = [mock_cp1, mock_cp2]
|
| 198 |
-
|
| 199 |
-
result = mod.check_for_existing_checkpoint({"checkpoints": mock_path.return_value})
|
| 200 |
-
assert result is not None
|
| 201 |
-
assert mock_cp2.name in result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_vllm_inference.py
DELETED
|
@@ -1,91 +0,0 @@
|
|
| 1 |
-
from unittest.mock import AsyncMock, MagicMock
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
class MockAsyncContextManager:
|
| 7 |
-
"""Mimics an async context manager for `async with`."""
|
| 8 |
-
def __init__(self, response):
|
| 9 |
-
self._response = response
|
| 10 |
-
|
| 11 |
-
async def __aenter__(self):
|
| 12 |
-
return self._response
|
| 13 |
-
|
| 14 |
-
async def __aexit__(self, *args):
|
| 15 |
-
pass
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
@pytest.mark.asyncio
|
| 19 |
-
async def test_send_request_streams_content():
|
| 20 |
-
from vllm_inference import _send_request
|
| 21 |
-
|
| 22 |
-
mock_response = AsyncMock()
|
| 23 |
-
mock_response.content = MagicMock()
|
| 24 |
-
|
| 25 |
-
chunks = [
|
| 26 |
-
b'data: {"choices":[{"delta":{"content":"Hello"}}],"object":"chat.completion.chunk"}\n',
|
| 27 |
-
b'data: {"choices":[{"delta":{"content":" world"}}],"object":"chat.completion.chunk"}\n',
|
| 28 |
-
b"data: [DONE]\n",
|
| 29 |
-
]
|
| 30 |
-
|
| 31 |
-
def sync_iter():
|
| 32 |
-
for c in chunks:
|
| 33 |
-
yield c
|
| 34 |
-
|
| 35 |
-
mock_response.content.__aiter__.return_value = sync_iter()
|
| 36 |
-
|
| 37 |
-
mock_session = MagicMock()
|
| 38 |
-
mock_session.post.return_value = MockAsyncContextManager(mock_response)
|
| 39 |
-
|
| 40 |
-
await _send_request(mock_session, "test-model", [{"role": "user", "content": "hi"}])
|
| 41 |
-
|
| 42 |
-
mock_session.post.assert_called_once()
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
@pytest.mark.asyncio
|
| 46 |
-
async def test_send_request_with_reasoning():
|
| 47 |
-
from vllm_inference import _send_request
|
| 48 |
-
|
| 49 |
-
mock_response = AsyncMock()
|
| 50 |
-
mock_response.content = MagicMock()
|
| 51 |
-
|
| 52 |
-
chunks = [
|
| 53 |
-
b'data: {"choices":[{"delta":{"reasoning":"thinking..."}}],"object":"chat.completion.chunk"}\n',
|
| 54 |
-
b'data: {"choices":[{"delta":{"content":"answer"}}],"object":"chat.completion.chunk"}\n',
|
| 55 |
-
b"data: [DONE]\n",
|
| 56 |
-
]
|
| 57 |
-
|
| 58 |
-
def sync_iter():
|
| 59 |
-
for c in chunks:
|
| 60 |
-
yield c
|
| 61 |
-
|
| 62 |
-
mock_response.content.__aiter__.return_value = sync_iter()
|
| 63 |
-
|
| 64 |
-
mock_session = MagicMock()
|
| 65 |
-
mock_session.post.return_value = MockAsyncContextManager(mock_response)
|
| 66 |
-
|
| 67 |
-
await _send_request(mock_session, "test-model", [{"role": "user", "content": "q"}])
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
@pytest.mark.asyncio
|
| 71 |
-
async def test_send_request_empty_delta():
|
| 72 |
-
from vllm_inference import _send_request
|
| 73 |
-
|
| 74 |
-
mock_response = AsyncMock()
|
| 75 |
-
mock_response.content = MagicMock()
|
| 76 |
-
|
| 77 |
-
chunks = [
|
| 78 |
-
b'data: {"choices":[{"delta":{}}],"object":"chat.completion.chunk"}\n',
|
| 79 |
-
b"data: [DONE]\n",
|
| 80 |
-
]
|
| 81 |
-
|
| 82 |
-
def sync_iter():
|
| 83 |
-
for c in chunks:
|
| 84 |
-
yield c
|
| 85 |
-
|
| 86 |
-
mock_response.content.__aiter__.return_value = sync_iter()
|
| 87 |
-
|
| 88 |
-
mock_session = MagicMock()
|
| 89 |
-
mock_session.post.return_value = MockAsyncContextManager(mock_response)
|
| 90 |
-
|
| 91 |
-
await _send_request(mock_session, "m", [])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/test_voxcpm_generator.py
DELETED
|
@@ -1,92 +0,0 @@
|
|
| 1 |
-
from unittest.mock import MagicMock, patch
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
@pytest.fixture(autouse=True, scope="class")
|
| 7 |
-
def _mock_modal():
|
| 8 |
-
with (
|
| 9 |
-
patch("modal.App.cls", side_effect=lambda **kw: lambda cls: cls),
|
| 10 |
-
patch("modal.App.function", side_effect=lambda **kw: lambda fn: fn),
|
| 11 |
-
):
|
| 12 |
-
yield
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@pytest.fixture(autouse=True, scope="class")
|
| 16 |
-
def _mock_heavy_deps():
|
| 17 |
-
"""Make heavy ML deps importable before voxcpm_generator module is loaded."""
|
| 18 |
-
with patch.dict(
|
| 19 |
-
"sys.modules",
|
| 20 |
-
{
|
| 21 |
-
"datasets": MagicMock(),
|
| 22 |
-
"voxcpm": MagicMock(),
|
| 23 |
-
"soundfile": MagicMock(),
|
| 24 |
-
},
|
| 25 |
-
):
|
| 26 |
-
yield
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def _make_gen():
|
| 30 |
-
"""Create a VoxCPMGenerator without running __init__."""
|
| 31 |
-
from voxcpm_generator import VoxCPMGenerator
|
| 32 |
-
|
| 33 |
-
gen = object.__new__(VoxCPMGenerator)
|
| 34 |
-
gen.model = MagicMock()
|
| 35 |
-
return gen
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
class TestVoxCPMGenerator:
|
| 39 |
-
def test_synthesize_impl_prepends_style(self):
|
| 40 |
-
gen = _make_gen()
|
| 41 |
-
gen.model.generate.return_value = [0.1, 0.2, 0.3]
|
| 42 |
-
gen.model.tts_model.sample_rate = 24000
|
| 43 |
-
|
| 44 |
-
with patch("soundfile.write") as mock_sf:
|
| 45 |
-
gen._synthesize_impl("hello world", "girl")
|
| 46 |
-
|
| 47 |
-
gen.model.generate.assert_called_once()
|
| 48 |
-
call_kwargs = gen.model.generate.call_args[1]
|
| 49 |
-
assert call_kwargs["text"] == "(girl)hello world"
|
| 50 |
-
assert call_kwargs["cfg_value"] == 2.0
|
| 51 |
-
assert call_kwargs["inference_timesteps"] == 10
|
| 52 |
-
mock_sf.assert_called_once()
|
| 53 |
-
|
| 54 |
-
def test_synthesize_impl_no_style(self):
|
| 55 |
-
gen = _make_gen()
|
| 56 |
-
gen.model.generate.return_value = [0.1, 0.2]
|
| 57 |
-
gen.model.tts_model.sample_rate = 24000
|
| 58 |
-
|
| 59 |
-
with patch("soundfile.write"):
|
| 60 |
-
gen._synthesize_impl("hello", "")
|
| 61 |
-
call_kwargs = gen.model.generate.call_args[1]
|
| 62 |
-
assert call_kwargs["text"] == "hello"
|
| 63 |
-
|
| 64 |
-
def test_synthesize_delegates(self):
|
| 65 |
-
gen = _make_gen()
|
| 66 |
-
gen._synthesize_impl = MagicMock(return_value=b"wav_data")
|
| 67 |
-
|
| 68 |
-
result = gen._synthesize("test", "girl", "/ref.wav")
|
| 69 |
-
|
| 70 |
-
gen._synthesize_impl.assert_called_once_with("test", "girl", "/ref.wav")
|
| 71 |
-
assert result == b"wav_data"
|
| 72 |
-
|
| 73 |
-
def test_synthesize_endpoint_parses_body(self):
|
| 74 |
-
gen = _make_gen()
|
| 75 |
-
gen._synthesize_impl = MagicMock(return_value=b"wav")
|
| 76 |
-
|
| 77 |
-
result = gen.synthesize({"text": "hello", "voice_style": "surfer"})
|
| 78 |
-
|
| 79 |
-
gen._synthesize_impl.assert_called_once_with(
|
| 80 |
-
"hello", "surfer", "/root/reference_audio.wav"
|
| 81 |
-
)
|
| 82 |
-
assert result.media_type == "audio/wav"
|
| 83 |
-
|
| 84 |
-
def test_synthesize_endpoint_default_ref(self):
|
| 85 |
-
gen = _make_gen()
|
| 86 |
-
gen._synthesize_impl = MagicMock(return_value=b"wav")
|
| 87 |
-
|
| 88 |
-
gen.synthesize({"text": "hello"})
|
| 89 |
-
|
| 90 |
-
gen._synthesize_impl.assert_called_once_with(
|
| 91 |
-
"hello", "", "/root/reference_audio.wav"
|
| 92 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
todo.md
DELETED
|
@@ -1,12 +0,0 @@
|
|
| 1 |
-
# Todo List for Phoenix Observability on Hugging Face Spaces with CrewAI
|
| 2 |
-
|
| 3 |
-
- [x] Read the tutorial: https://huggingface.co/learn/cookbook/phoenix_observability_on_hf_spaces
|
| 4 |
-
- [x] Understand the steps to set up Phoenix observability dashboard on HF Spaces
|
| 5 |
-
- [ ] Create a new Hugging Face Space (if not already existing) for the observability dashboard (space already exists: RCaz/phoenix-arize-observability)
|
| 6 |
-
- [x] Set up the Space with necessary dependencies (phoenix, crewai, etc.)
|
| 7 |
-
- [x] Write a basic CrewAI agent/task script
|
| 8 |
-
- [x] Configure CrewAI telemetry to export traces to the Phoenix endpoint (pointing at https://huggingface.co/spaces/RCaz/phoenix-arize-observability?logs=container)
|
| 9 |
-
- [ ] Test the setup by running the CrewAI script and verifying logs appear in the Phoenix UI
|
| 10 |
-
- [ ] Adjust any configuration issues (ports, environment variables, etc.)
|
| 11 |
-
- [ ] Document the process and update this todo list as progress is made
|
| 12 |
-
- [ ] Optional: Enhance the CrewAI workflow with more complex tasks and monitoring
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
transcribe_generator.py
DELETED
|
@@ -1,125 +0,0 @@
|
|
| 1 |
-
import base64
|
| 2 |
-
import io
|
| 3 |
-
import json
|
| 4 |
-
|
| 5 |
-
import modal
|
| 6 |
-
from starlette.responses import JSONResponse
|
| 7 |
-
|
| 8 |
-
app = modal.App("cohere-transcriber")
|
| 9 |
-
|
| 10 |
-
transcribe_image = modal.Image.debian_slim(python_version="3.12").pip_install(
|
| 11 |
-
"torch>=2.5.0",
|
| 12 |
-
"transformers>=5.4.0",
|
| 13 |
-
"huggingface_hub",
|
| 14 |
-
"soundfile",
|
| 15 |
-
"librosa",
|
| 16 |
-
"fastapi[standard]",
|
| 17 |
-
"requests",
|
| 18 |
-
"sentencepiece",
|
| 19 |
-
)
|
| 20 |
-
|
| 21 |
-
hf_cache_vol = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
|
| 22 |
-
|
| 23 |
-
MODEL_NAME = "CohereLabs/cohere-transcribe-03-2026"
|
| 24 |
-
SAMPLE_RATE = 16000
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
@app.cls(
|
| 28 |
-
image=transcribe_image,
|
| 29 |
-
gpu="T4",
|
| 30 |
-
timeout=300,
|
| 31 |
-
scaledown_window=300,
|
| 32 |
-
volumes={"/root/.cache/huggingface": hf_cache_vol},
|
| 33 |
-
secrets=[modal.Secret.from_name("huggingface")],
|
| 34 |
-
)
|
| 35 |
-
class CohereTranscriber:
|
| 36 |
-
def __init__(self):
|
| 37 |
-
import torch
|
| 38 |
-
from transformers import AutoProcessor
|
| 39 |
-
|
| 40 |
-
self.device = "cuda"
|
| 41 |
-
from transformers import AutoModelForSpeechSeq2Seq, AutoConfig
|
| 42 |
-
# Config: preprocessor.features=128, window_size=0.025s (400 @16kHz), window_stride=0.01s (160 @16kHz)
|
| 43 |
-
self.processor = AutoProcessor.from_pretrained(
|
| 44 |
-
MODEL_NAME,
|
| 45 |
-
trust_remote_code=True,
|
| 46 |
-
feature_size=128,
|
| 47 |
-
n_window_size=400,
|
| 48 |
-
n_window_stride=160,
|
| 49 |
-
n_fft=512,
|
| 50 |
-
)
|
| 51 |
-
# Load config, get model class from pattern matching, patch list→set for transformers 5.12 compat
|
| 52 |
-
config = AutoConfig.from_pretrained(MODEL_NAME, trust_remote_code=True)
|
| 53 |
-
# Try loading; if it fails due to list|set, patch and retry
|
| 54 |
-
import transformers.modeling_utils as _mu
|
| 55 |
-
orig_fn = _mu.PreTrainedModel._adjust_missing_and_unexpected_keys
|
| 56 |
-
def patched_fn(self, *a, **kw):
|
| 57 |
-
if hasattr(self, '_keys_to_ignore_on_load_unexpected') and isinstance(self._keys_to_ignore_on_load_unexpected, list):
|
| 58 |
-
self._keys_to_ignore_on_load_unexpected = set(self._keys_to_ignore_on_load_unexpected)
|
| 59 |
-
return orig_fn(self, *a, **kw)
|
| 60 |
-
_mu.PreTrainedModel._adjust_missing_and_unexpected_keys = patched_fn
|
| 61 |
-
try:
|
| 62 |
-
self.model = AutoModelForSpeechSeq2Seq.from_pretrained(
|
| 63 |
-
MODEL_NAME,
|
| 64 |
-
dtype=torch.bfloat16,
|
| 65 |
-
trust_remote_code=True,
|
| 66 |
-
).to(self.device)
|
| 67 |
-
finally:
|
| 68 |
-
_mu.PreTrainedModel._adjust_missing_and_unexpected_keys = orig_fn
|
| 69 |
-
|
| 70 |
-
@modal.fastapi_endpoint(method="POST")
|
| 71 |
-
def transcribe(self, body: dict) -> JSONResponse:
|
| 72 |
-
import librosa
|
| 73 |
-
import soundfile as sf
|
| 74 |
-
import requests as _requests
|
| 75 |
-
import torch
|
| 76 |
-
|
| 77 |
-
audio_bytes = None
|
| 78 |
-
|
| 79 |
-
if "audio" in body:
|
| 80 |
-
audio_bytes = base64.b64decode(body["audio"])
|
| 81 |
-
elif "audio_url" in body:
|
| 82 |
-
resp = _requests.get(body["audio_url"], timeout=60)
|
| 83 |
-
resp.raise_for_status()
|
| 84 |
-
audio_bytes = resp.content
|
| 85 |
-
else:
|
| 86 |
-
return JSONResponse(
|
| 87 |
-
{"error": "Provide either `audio` (base64 WAV) or `audio_url`"},
|
| 88 |
-
status_code=400,
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
audio, sr = sf.read(io.BytesIO(audio_bytes))
|
| 92 |
-
if sr != SAMPLE_RATE:
|
| 93 |
-
audio = librosa.resample(audio, orig_sr=sr, target_sr=SAMPLE_RATE)
|
| 94 |
-
if audio.ndim > 1:
|
| 95 |
-
audio = audio.mean(axis=1)
|
| 96 |
-
|
| 97 |
-
inputs = self.processor(
|
| 98 |
-
audio, sampling_rate=SAMPLE_RATE, return_tensors="pt"
|
| 99 |
-
)
|
| 100 |
-
input_features = inputs.input_features.to(self.device, dtype=torch.bfloat16)
|
| 101 |
-
|
| 102 |
-
with torch.no_grad():
|
| 103 |
-
generated_ids = self.model.generate(input_features)
|
| 104 |
-
|
| 105 |
-
transcription = self.processor.batch_decode(
|
| 106 |
-
generated_ids, skip_special_tokens=True
|
| 107 |
-
)[0]
|
| 108 |
-
|
| 109 |
-
return JSONResponse({"transcription": transcription})
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
@app.local_entrypoint()
|
| 113 |
-
def main(audio_path: str = None):
|
| 114 |
-
import httpx
|
| 115 |
-
|
| 116 |
-
if not audio_path:
|
| 117 |
-
audio_path = input("Path to audio file: ").strip()
|
| 118 |
-
|
| 119 |
-
with open(audio_path, "rb") as f:
|
| 120 |
-
audio_b64 = base64.b64encode(f.read()).decode()
|
| 121 |
-
|
| 122 |
-
transcriber = CohereTranscriber()
|
| 123 |
-
url = transcriber.transcribe.web_url
|
| 124 |
-
resp = httpx.post(url, json={"audio": audio_b64}, timeout=120)
|
| 125 |
-
print(json.dumps(resp.json(), indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
vllm_inference.py
DELETED
|
@@ -1,165 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from typing import Any
|
| 3 |
-
|
| 4 |
-
import aiohttp
|
| 5 |
-
import modal
|
| 6 |
-
|
| 7 |
-
vllm_image = (
|
| 8 |
-
modal.Image.from_registry("nvidia/cuda:12.9.0-devel-ubuntu22.04", add_python="3.12")
|
| 9 |
-
.entrypoint([])
|
| 10 |
-
.uv_pip_install("vllm==0.21.0")
|
| 11 |
-
.env(
|
| 12 |
-
{
|
| 13 |
-
"HF_XET_HIGH_PERFORMANCE": "1",
|
| 14 |
-
"VLLM_LOG_STATS_INTERVAL": "1",
|
| 15 |
-
}
|
| 16 |
-
)
|
| 17 |
-
)
|
| 18 |
-
|
| 19 |
-
MODEL_NAME = "google/gemma-4-26B-A4B-it"
|
| 20 |
-
MODEL_REVISION = "47b6801b24d15ff9bcd8c96dfaea0be9ed3a0301"
|
| 21 |
-
|
| 22 |
-
hf_cache_vol = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
|
| 23 |
-
vllm_cache_vol = modal.Volume.from_name("vllm-cache", create_if_missing=True)
|
| 24 |
-
|
| 25 |
-
FAST_BOOT = False
|
| 26 |
-
|
| 27 |
-
SPECULATIVE_MODEL_NAME = "google/gemma-4-26B-A4B-it-assistant"
|
| 28 |
-
SPECULATIVE_MODEL_REVISION = "f188f476dc11dd5bb3014dc861529d316bce49d3"
|
| 29 |
-
|
| 30 |
-
app = modal.App("example-vllm-inference")
|
| 31 |
-
|
| 32 |
-
N_GPU = 1
|
| 33 |
-
MINUTES = 60
|
| 34 |
-
VLLM_PORT = 8000
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
@app.function(
|
| 38 |
-
image=vllm_image,
|
| 39 |
-
gpu=f"H200:{N_GPU}",
|
| 40 |
-
scaledown_window=15 * MINUTES,
|
| 41 |
-
timeout=10 * MINUTES,
|
| 42 |
-
volumes={
|
| 43 |
-
"/root/.cache/huggingface": hf_cache_vol,
|
| 44 |
-
"/root/.cache/vllm": vllm_cache_vol,
|
| 45 |
-
},
|
| 46 |
-
)
|
| 47 |
-
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
|
| 48 |
-
def serve():
|
| 49 |
-
import subprocess
|
| 50 |
-
|
| 51 |
-
cmd = [
|
| 52 |
-
"vllm",
|
| 53 |
-
"serve",
|
| 54 |
-
MODEL_NAME,
|
| 55 |
-
"--revision",
|
| 56 |
-
MODEL_REVISION,
|
| 57 |
-
"--served-model-name",
|
| 58 |
-
MODEL_NAME,
|
| 59 |
-
"llm",
|
| 60 |
-
"--host",
|
| 61 |
-
"0.0.0.0",
|
| 62 |
-
"--port",
|
| 63 |
-
str(VLLM_PORT),
|
| 64 |
-
"--uvicorn-log-level",
|
| 65 |
-
"info",
|
| 66 |
-
"--async-scheduling",
|
| 67 |
-
]
|
| 68 |
-
|
| 69 |
-
if FAST_BOOT:
|
| 70 |
-
cmd.append("--enforce-eager")
|
| 71 |
-
else:
|
| 72 |
-
cmd.append("--no-enforce-eager")
|
| 73 |
-
|
| 74 |
-
cmd += ["--tensor-parallel-size", str(N_GPU)]
|
| 75 |
-
|
| 76 |
-
cmd += [
|
| 77 |
-
"--limit-mm-per-prompt",
|
| 78 |
-
json.dumps({"image": 0, "video": 0, "audio": 0}),
|
| 79 |
-
"--enable-auto-tool-choice",
|
| 80 |
-
"--reasoning-parser",
|
| 81 |
-
"gemma4",
|
| 82 |
-
"--tool-call-parser",
|
| 83 |
-
"gemma4",
|
| 84 |
-
]
|
| 85 |
-
|
| 86 |
-
cmd += [
|
| 87 |
-
"--speculative-config",
|
| 88 |
-
json.dumps(
|
| 89 |
-
{
|
| 90 |
-
"model": SPECULATIVE_MODEL_NAME,
|
| 91 |
-
"revision": SPECULATIVE_MODEL_REVISION,
|
| 92 |
-
"num_speculative_tokens": 4,
|
| 93 |
-
}
|
| 94 |
-
),
|
| 95 |
-
]
|
| 96 |
-
|
| 97 |
-
print("Starting vLLM:", " ".join(cmd), flush=True)
|
| 98 |
-
|
| 99 |
-
# Start vLLM as a subprocess. Modal's web_server monitors the port.
|
| 100 |
-
subprocess.Popen(cmd)
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
@app.local_entrypoint()
|
| 104 |
-
async def test(test_timeout=15 * MINUTES, content=None, twice=True):
|
| 105 |
-
url = await serve.get_web_url.aio()
|
| 106 |
-
|
| 107 |
-
system_prompt = {
|
| 108 |
-
"role": "system",
|
| 109 |
-
"content": "You are a pirate who can't help but drop sly reminders that he went to Harvard.",
|
| 110 |
-
}
|
| 111 |
-
if content is None:
|
| 112 |
-
content = "Explain the singular value decomposition."
|
| 113 |
-
|
| 114 |
-
messages = [
|
| 115 |
-
system_prompt,
|
| 116 |
-
{"role": "user", "content": content},
|
| 117 |
-
]
|
| 118 |
-
|
| 119 |
-
async with aiohttp.ClientSession(base_url=url) as session:
|
| 120 |
-
print(f"Running health check for server at {url}")
|
| 121 |
-
async with session.get("/health", timeout=test_timeout - 1 * MINUTES) as resp:
|
| 122 |
-
up = resp.status == 200
|
| 123 |
-
assert up, f"Failed health check for server at {url}"
|
| 124 |
-
print(f"Successful health check for server at {url}")
|
| 125 |
-
|
| 126 |
-
print(f"Sending messages to {url}:", *messages, sep="\n\t")
|
| 127 |
-
await _send_request(session, "llm", messages)
|
| 128 |
-
if twice:
|
| 129 |
-
messages[0]["content"] = "You are Jar Jar Binks."
|
| 130 |
-
print(f"Sending messages to {url}:", *messages, sep="\n\t")
|
| 131 |
-
await _send_request(session, "llm", messages)
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
async def _send_request(
|
| 135 |
-
session: aiohttp.ClientSession, model: str, messages: list
|
| 136 |
-
) -> None:
|
| 137 |
-
payload: dict[str, Any] = {"messages": messages, "model": model, "stream": True}
|
| 138 |
-
payload["chat_template_kwargs"] = {"enable_thinking": True}
|
| 139 |
-
|
| 140 |
-
headers = {"Content-Type": "application/json", "Accept": "text/event-stream"}
|
| 141 |
-
|
| 142 |
-
async with session.post(
|
| 143 |
-
"/v1/chat/completions", json=payload, headers=headers
|
| 144 |
-
) as resp:
|
| 145 |
-
async for raw in resp.content:
|
| 146 |
-
resp.raise_for_status()
|
| 147 |
-
line = raw.decode().strip()
|
| 148 |
-
if not line or line == "data: [DONE]":
|
| 149 |
-
continue
|
| 150 |
-
if line.startswith("data: "):
|
| 151 |
-
line = line[len("data: ") :]
|
| 152 |
-
|
| 153 |
-
chunk = json.loads(line)
|
| 154 |
-
assert chunk["object"] == "chat.completion.chunk"
|
| 155 |
-
delta = chunk["choices"][0]["delta"]
|
| 156 |
-
content = (
|
| 157 |
-
delta.get("content")
|
| 158 |
-
or delta.get("reasoning")
|
| 159 |
-
or delta.get("reasoning_content")
|
| 160 |
-
)
|
| 161 |
-
if content:
|
| 162 |
-
print(content, end="")
|
| 163 |
-
else:
|
| 164 |
-
print("\n", chunk)
|
| 165 |
-
print()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
voxcpm_generator.py
DELETED
|
@@ -1,108 +0,0 @@
|
|
| 1 |
-
import io
|
| 2 |
-
import os
|
| 3 |
-
|
| 4 |
-
import modal
|
| 5 |
-
from starlette.responses import Response
|
| 6 |
-
|
| 7 |
-
app = modal.App("voxcpm-generator")
|
| 8 |
-
|
| 9 |
-
voxcpm_image = (
|
| 10 |
-
modal.Image.debian_slim(python_version="3.12")
|
| 11 |
-
.run_commands("echo cache-bust-2026-06-12-voxcpm")
|
| 12 |
-
.pip_install(
|
| 13 |
-
"voxcpm",
|
| 14 |
-
"fastapi[standard]",
|
| 15 |
-
"soundfile",
|
| 16 |
-
"requests",
|
| 17 |
-
"huggingface_hub",
|
| 18 |
-
"datasets",
|
| 19 |
-
"librosa",
|
| 20 |
-
)
|
| 21 |
-
)
|
| 22 |
-
|
| 23 |
-
hf_cache_vol = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
|
| 24 |
-
|
| 25 |
-
REFERENCE_AUDIO_PATH = "/root/reference_audio.wav"
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
@app.cls(
|
| 29 |
-
image=voxcpm_image,
|
| 30 |
-
gpu="T4",
|
| 31 |
-
timeout=600,
|
| 32 |
-
scaledown_window=300,
|
| 33 |
-
volumes={"/root/.cache/huggingface": hf_cache_vol},
|
| 34 |
-
secrets=[modal.Secret.from_name("huggingface")],
|
| 35 |
-
)
|
| 36 |
-
class VoxCPMGenerator:
|
| 37 |
-
def __init__(self):
|
| 38 |
-
os.environ["TORCHDYNAMO_DISABLE"] = "1"
|
| 39 |
-
|
| 40 |
-
from datasets import load_dataset
|
| 41 |
-
import soundfile as sf
|
| 42 |
-
|
| 43 |
-
if not os.path.exists(REFERENCE_AUDIO_PATH):
|
| 44 |
-
ds = load_dataset(
|
| 45 |
-
"facebook/voxpopuli",
|
| 46 |
-
"en",
|
| 47 |
-
split="train",
|
| 48 |
-
streaming=True,
|
| 49 |
-
trust_remote_code=True,
|
| 50 |
-
)
|
| 51 |
-
sample = next(iter(ds))
|
| 52 |
-
audio_array = sample["audio"]["array"]
|
| 53 |
-
sr = sample["audio"]["sampling_rate"]
|
| 54 |
-
sf.write(REFERENCE_AUDIO_PATH, audio_array, sr)
|
| 55 |
-
|
| 56 |
-
from voxcpm import VoxCPM
|
| 57 |
-
|
| 58 |
-
self.model = VoxCPM.from_pretrained("openbmb/VoxCPM2")
|
| 59 |
-
|
| 60 |
-
def _synthesize_impl(
|
| 61 |
-
self, text: str, voice_style: str = "", reference_audio: str | None = None
|
| 62 |
-
) -> bytes:
|
| 63 |
-
if voice_style:
|
| 64 |
-
text = f"({voice_style}){text}"
|
| 65 |
-
ref = reference_audio or REFERENCE_AUDIO_PATH
|
| 66 |
-
print(f"Synthesizing: text=[{text[:80]}...] ref=[{ref}]", flush=True)
|
| 67 |
-
audio = self.model.generate(
|
| 68 |
-
text=text, reference_wav_path=ref, cfg_value=2.0, inference_timesteps=10
|
| 69 |
-
)
|
| 70 |
-
|
| 71 |
-
import soundfile as sf
|
| 72 |
-
|
| 73 |
-
buf = io.BytesIO()
|
| 74 |
-
sf.write(buf, audio, samplerate=self.model.tts_model.sample_rate, format="WAV")
|
| 75 |
-
buf.seek(0)
|
| 76 |
-
return buf.read()
|
| 77 |
-
|
| 78 |
-
@modal.method()
|
| 79 |
-
def _synthesize(
|
| 80 |
-
self, text: str, voice_style: str = "", reference_audio: str | None = None
|
| 81 |
-
) -> bytes:
|
| 82 |
-
return self._synthesize_impl(text, voice_style, reference_audio)
|
| 83 |
-
|
| 84 |
-
@modal.fastapi_endpoint(method="POST")
|
| 85 |
-
def synthesize(self, body: dict) -> Response:
|
| 86 |
-
text = body["text"]
|
| 87 |
-
voice_style = body.get("voice_style", "")
|
| 88 |
-
reference_audio = body.get("reference_audio") or REFERENCE_AUDIO_PATH
|
| 89 |
-
return Response(
|
| 90 |
-
content=self._synthesize_impl(text, voice_style, reference_audio),
|
| 91 |
-
media_type="audio/wav",
|
| 92 |
-
)
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
@app.local_entrypoint()
|
| 96 |
-
def main(
|
| 97 |
-
text: str = "This is a test of the VoxCPM voice synthesis system.",
|
| 98 |
-
voice_style: str = "girl",
|
| 99 |
-
):
|
| 100 |
-
gen = VoxCPMGenerator()
|
| 101 |
-
voice_style_str = (voice_style or "girl").lower().strip()
|
| 102 |
-
ref_map = {"girl": REFERENCE_AUDIO_PATH, "surfer": REFERENCE_AUDIO_PATH}
|
| 103 |
-
ref = ref_map.get(voice_style_str, REFERENCE_AUDIO_PATH)
|
| 104 |
-
data = gen._synthesize.remote(text, voice_style_str, ref)
|
| 105 |
-
filename = "voxcpm_output.wav"
|
| 106 |
-
with open(filename, "wb") as f:
|
| 107 |
-
f.write(data)
|
| 108 |
-
print(f"Audio saved to {filename}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|