Add scripts for modal deployments

#1
by RCaz - opened
.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: Dual Lens
3
  emoji: 👀
4
- colorFrom: purple
5
- colorTo: purple
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: 'A Multi-Agent Research Synthesizer'
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, nullcontext
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.litellm import LiteLLMInstrumentor
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
- tracer = None
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
- Propagates OTel context (session ID, etc.) to the worker thread.
126
- """
127
- ctx = contextvars.copy_context()
128
- fut = _POOL.submit(ctx.run, fn)
129
- try:
130
- return fut.result(timeout=timeout)
131
- except concurrent.futures.TimeoutError:
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 and merges 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.
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
- with (
294
- tracer.start_as_current_span("flux.generate_image") if tracer else nullcontext()
295
- ):
296
- url = _get_flux_url()
297
- resp = _httpx.post(
298
- url,
299
- json={"prompt": prompt, "steps": 30},
300
- timeout=600,
301
- follow_redirects=True,
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
- _span_cm = (
361
- tracer.start_as_current_span("llm.generate_caption")
362
- if tracer
363
- else nullcontext()
 
364
  )
365
- with _span_cm as _span:
366
- resp = _httpx.post(
367
- f"{_VLLM_BASE_URL}/chat/completions",
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
- with (
420
- tracer.start_as_current_span("voxcpm.generate_voice")
421
- if tracer
422
- else nullcontext()
423
- ):
424
- url = _get_vox_url()
425
- resp = _httpx.post(
426
- url,
427
- json={"text": voice_script, "voice_style": voice_style},
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
- tracer.start_as_current_span("cohere.transcribe_audio")
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
- resp = _httpx.post(url, json={"audio": audio_b64}, timeout=600)
457
- resp.raise_for_status()
458
- text = resp.json()["transcription"]
459
- print(f'Transcribed: "{text}"\n')
460
- return text
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-litellm>=0.1.34
 
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}")