mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-07 10:12:38 +00:00
Compare commits
7 Commits
1.14.4a1
...
devin/1777
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee8b3be8e5 | ||
|
|
c7f01048b7 | ||
|
|
14c3963d2c | ||
|
|
feb2e715a3 | ||
|
|
e0b86750c2 | ||
|
|
2a40316521 | ||
|
|
e2deac5575 |
@@ -380,6 +380,33 @@ class AnotherFlow(Flow[dict]):
|
||||
print("Method-level persisted runs:", self.state["runs"])
|
||||
```
|
||||
|
||||
### مفتاح استمرارية مخصص
|
||||
|
||||
افتراضيًا، يستخدم `@persist` الحقل `state.id` المُولّد تلقائيًا كمفتاح للاستمرارية. إذا كان لتدفقك معرّف خاص به — مثل `conversation_id` مشترك بين عدة جلسات — يمكنك تمرير الوسيط `key` ليستخدم `@persist` تلك السمة كـ UUID للتدفق:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
turn: int = 0
|
||||
|
||||
@persist(key="conversation_id") # استخدام حقل مخصص كمفتاح للاستمرارية
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def begin(self):
|
||||
self.state.turn += 1
|
||||
print(f"Conversation {self.state.conversation_id} turn {self.state.turn}")
|
||||
|
||||
# إعادة تشغيل المحادثة بنفس conversation_id يُعيد تحميل الحالة السابقة
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
يقرأ المزخرف القيمة من `state[key]` للحالات من نوع dict، ومن `getattr(state, key)` للحالات من نوع Pydantic / كائن. إذا كانت السمة المحددة غير موجودة أو قيمتها falsy عند الحفظ، يُطلق `@persist` خطأ `ValueError` مثل `Flow state is missing required persistence key 'conversation_id'`. عند حذف `key`، يظل السلوك الأصلي قائمًا ويُستخدم `state.id`.
|
||||
|
||||
### كيف تعمل
|
||||
|
||||
1. **تعريف الحالة الفريد**
|
||||
|
||||
@@ -146,6 +146,15 @@ class ProductionFlow(Flow[AppState]):
|
||||
# ...
|
||||
```
|
||||
|
||||
افتراضيًا، يستخدم `@persist` الحقل `state.id` المُولّد تلقائيًا كمفتاح للحالة المحفوظة. إذا كان تطبيقك يمتلك معرّفًا طبيعيًا بالفعل — مثل `conversation_id` يربط عدة تشغيلات بنفس جلسة المستخدم — مرّره كـ `key` ليستخدمه المزخرف كـ UUID للتدفق. يُطلق `ValueError` إذا كانت السمة المحددة غير موجودة أو قيمتها falsy عند الحفظ.
|
||||
|
||||
```python
|
||||
@persist(key="conversation_id")
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
# يجب أن يحتوي AppState على conversation_id؛ استئناف الجلسة يُعيد تحميل الحالة السابقة
|
||||
...
|
||||
```
|
||||
|
||||
## الخلاصة
|
||||
|
||||
- **ابدأ بتدفق.**
|
||||
|
||||
@@ -116,6 +116,33 @@ class PersistentCounterFlow(Flow[CounterState]):
|
||||
return self.state.value
|
||||
```
|
||||
|
||||
### استخدام مفتاح استمرارية مخصص
|
||||
|
||||
افتراضيًا، يستخدم `@persist()` الحقل `state.id` المُولّد تلقائيًا كمفتاح للحالة المحفوظة. عندما يكون لمجالك معرّف طبيعي بالفعل — مثل `conversation_id` يربط عدة تشغيلات للتدفق بنفس جلسة المستخدم — مرّره كوسيط `key` ليستخدمه `@persist` كـ UUID للتدفق بدلًا من `id`:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
history: list[str] = []
|
||||
|
||||
@persist(key="conversation_id")
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def greet(self):
|
||||
self.state.history.append("hello")
|
||||
return self.state.history
|
||||
|
||||
# تشغيل ثانٍ بنفس conversation_id يُعيد تحميل الحالة السابقة
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
بالنسبة للحالات من نوع dict يقرأ `@persist` القيمة من `state[key]`، ولحالات Pydantic / الكائنات يقرأها من `getattr(state, key)`. إذا كانت السمة المحددة غير موجودة أو قيمتها falsy عند حفظ الحالة، يُطلق `@persist` خطأ `ValueError` مثل `Flow state is missing required persistence key 'conversation_id'`، فيظهر الفشل فورًا بدلًا من فقد بيانات الاستمرارية بصمت. استدعاء `@persist()` بدون `key` يحافظ على السلوك الأصلي ويستخدم `state.id`.
|
||||
|
||||
## أنماط حالة متقدمة
|
||||
|
||||
### المنطق الشرطي المبني على الحالة
|
||||
|
||||
@@ -380,6 +380,33 @@ class AnotherFlow(Flow[dict]):
|
||||
print("Method-level persisted runs:", self.state["runs"])
|
||||
```
|
||||
|
||||
### Custom Persistence Key
|
||||
|
||||
By default, `@persist` uses the auto-generated `state.id` field as the persistence key. If your flow models its own identifier — for example a `conversation_id` shared across sessions — you can pass a `key` argument and `@persist` will use that attribute as the flow UUID instead:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
turn: int = 0
|
||||
|
||||
@persist(key="conversation_id") # Use a custom field as the persistence key
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def begin(self):
|
||||
self.state.turn += 1
|
||||
print(f"Conversation {self.state.conversation_id} turn {self.state.turn}")
|
||||
|
||||
# Resuming the same conversation reloads its prior state by conversation_id
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
The decorator reads the value at `state[key]` for dict states, or `getattr(state, key)` for Pydantic / object states. If the named attribute is missing or falsy at save time, `@persist` raises a `ValueError` such as `Flow state is missing required persistence key 'conversation_id'`. When `key` is omitted, the existing behavior is preserved and `state.id` is used.
|
||||
|
||||
### How It Works
|
||||
|
||||
1. **Unique State Identification**
|
||||
|
||||
@@ -146,6 +146,15 @@ class ProductionFlow(Flow[AppState]):
|
||||
# ...
|
||||
```
|
||||
|
||||
By default `@persist` keys saved state by the auto-generated `state.id`. If your application already has a natural identifier — for example a `conversation_id` that ties multiple runs to the same user session — pass it as `key` and the decorator will use that attribute as the flow UUID. A `ValueError` is raised if the named attribute is missing or falsy at save time.
|
||||
|
||||
```python
|
||||
@persist(key="conversation_id")
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
# AppState must expose conversation_id; resuming a session reloads its prior state
|
||||
...
|
||||
```
|
||||
|
||||
## Summary
|
||||
|
||||
- **Start with a Flow.**
|
||||
|
||||
@@ -346,6 +346,33 @@ class SelectivePersistFlow(Flow):
|
||||
return f"Complete with count {self.state['count']}"
|
||||
```
|
||||
|
||||
#### Using a Custom Persistence Key
|
||||
|
||||
By default, `@persist()` keys persisted state by the flow's auto-generated `state.id`. When your domain already has a natural identifier — for example a `conversation_id` that ties multiple flow runs to the same user session — pass it as the `key` argument and `@persist` will use that attribute as the flow UUID instead of `id`:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
history: list[str] = []
|
||||
|
||||
@persist(key="conversation_id")
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def greet(self):
|
||||
self.state.history.append("hello")
|
||||
return self.state.history
|
||||
|
||||
# A second run with the same conversation_id reloads the prior state
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
For dict-based states `@persist` reads `state[key]`, and for Pydantic / object states it reads `getattr(state, key)`. If the named attribute is missing or falsy when state is being saved, `@persist` raises a `ValueError` like `Flow state is missing required persistence key 'conversation_id'`, so the failure surfaces immediately rather than silently dropping persisted data. Calling `@persist()` without `key` keeps the original behavior of using `state.id`.
|
||||
|
||||
|
||||
## Advanced State Patterns
|
||||
|
||||
|
||||
@@ -373,6 +373,33 @@ class AnotherFlow(Flow[dict]):
|
||||
print("Method-level persisted runs:", self.state["runs"])
|
||||
```
|
||||
|
||||
### 사용자 지정 영속성 키
|
||||
|
||||
기본적으로 `@persist`는 자동 생성된 `state.id` 필드를 영속성 키로 사용합니다. 여러 세션에 걸쳐 공유되는 `conversation_id`처럼 플로우에 자체 식별자가 있는 경우, `key` 인자를 전달하면 `@persist`가 해당 속성을 플로우 UUID로 사용합니다:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
turn: int = 0
|
||||
|
||||
@persist(key="conversation_id") # 사용자 지정 필드를 영속성 키로 사용
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def begin(self):
|
||||
self.state.turn += 1
|
||||
print(f"Conversation {self.state.conversation_id} turn {self.state.turn}")
|
||||
|
||||
# 동일한 conversation_id로 다시 실행하면 이전 상태가 다시 로드됩니다
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
이 데코레이터는 dict 상태의 경우 `state[key]`에서, Pydantic / 객체 상태의 경우 `getattr(state, key)`에서 값을 읽습니다. 저장 시점에 지정된 속성이 없거나 falsy 값이면, `@persist`는 `Flow state is missing required persistence key 'conversation_id'`와 같은 `ValueError`를 발생시킵니다. `key`를 생략하면 기존 동작이 유지되어 `state.id`가 사용됩니다.
|
||||
|
||||
### 작동 방식
|
||||
|
||||
1. **고유 상태 식별**
|
||||
|
||||
@@ -146,6 +146,15 @@ class ProductionFlow(Flow[AppState]):
|
||||
# ...
|
||||
```
|
||||
|
||||
기본적으로 `@persist`는 자동 생성된 `state.id`를 저장된 상태의 키로 사용합니다. 애플리케이션에 이미 자연스러운 식별자가 있는 경우 — 예를 들어 같은 사용자 세션에 속한 여러 실행을 묶는 `conversation_id` — `key`로 전달하면 데코레이터가 해당 속성을 플로우 UUID로 사용합니다. 저장 시점에 지정된 속성이 없거나 falsy 값이면 `ValueError`가 발생합니다.
|
||||
|
||||
```python
|
||||
@persist(key="conversation_id")
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
# AppState는 conversation_id를 노출해야 합니다; 세션을 재개하면 이전 상태가 다시 로드됩니다
|
||||
...
|
||||
```
|
||||
|
||||
## 요약
|
||||
|
||||
- **Flow로 시작하세요.**
|
||||
|
||||
@@ -346,6 +346,33 @@ class SelectivePersistFlow(Flow):
|
||||
return f"Complete with count {self.state['count']}"
|
||||
```
|
||||
|
||||
#### 사용자 지정 영속성 키 사용하기
|
||||
|
||||
기본적으로 `@persist()`는 자동 생성된 `state.id`를 영속 상태의 키로 사용합니다. 도메인에 이미 자연스러운 식별자가 있는 경우 — 예를 들어 같은 사용자 세션에 속한 여러 플로우 실행을 묶는 `conversation_id` — `key` 인자로 전달하면 `@persist`는 `id` 대신 해당 속성을 플로우 UUID로 사용합니다:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
history: list[str] = []
|
||||
|
||||
@persist(key="conversation_id")
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def greet(self):
|
||||
self.state.history.append("hello")
|
||||
return self.state.history
|
||||
|
||||
# 동일한 conversation_id로 두 번째 실행 시 이전 상태가 다시 로드됩니다
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
dict 기반 상태의 경우 `@persist`는 `state[key]`를 읽고, Pydantic / 객체 상태의 경우 `getattr(state, key)`를 읽습니다. 상태가 저장될 때 지정된 속성이 없거나 falsy 값이면 `@persist`는 `Flow state is missing required persistence key 'conversation_id'`와 같은 `ValueError`를 발생시켜, 영속 데이터가 조용히 손실되는 대신 즉시 실패가 드러나도록 합니다. `key` 없이 `@persist()`를 호출하면 기존 동작대로 `state.id`가 사용됩니다.
|
||||
|
||||
## 고급 상태 패턴
|
||||
|
||||
### 상태 기반 조건부 로직
|
||||
|
||||
@@ -193,6 +193,33 @@ Para um controle mais granular, você pode aplicar @persist em métodos específ
|
||||
# (O código não é traduzido)
|
||||
```
|
||||
|
||||
### Chave de Persistência Personalizada
|
||||
|
||||
Por padrão, `@persist` usa o campo `state.id` gerado automaticamente como chave de persistência. Se o seu flow já possui um identificador natural — por exemplo um `conversation_id` compartilhado entre sessões — você pode passar o argumento `key` e `@persist` usará esse atributo como UUID do flow:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
turn: int = 0
|
||||
|
||||
@persist(key="conversation_id") # Usa um campo personalizado como chave de persistência
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def begin(self):
|
||||
self.state.turn += 1
|
||||
print(f"Conversa {self.state.conversation_id} turno {self.state.turn}")
|
||||
|
||||
# Retomar a mesma conversa recarrega o estado anterior pelo conversation_id
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
O decorador lê o valor em `state[key]` para estados do tipo dicionário ou `getattr(state, key)` para estados Pydantic / objetos. Se o atributo informado estiver ausente ou for *falsy* no momento de salvar, `@persist` lança um `ValueError` como `Flow state is missing required persistence key 'conversation_id'`. Quando `key` é omitido, o comportamento original é preservado e `state.id` continua sendo usado.
|
||||
|
||||
### Como Funciona
|
||||
|
||||
1. **Identificação Única do Estado**
|
||||
|
||||
@@ -146,6 +146,15 @@ class ProductionFlow(Flow[AppState]):
|
||||
# ...
|
||||
```
|
||||
|
||||
Por padrão, `@persist` usa o `state.id` gerado automaticamente como chave do estado salvo. Se a sua aplicação já tem um identificador natural — por exemplo um `conversation_id` que liga várias execuções à mesma sessão de usuário — passe-o como `key` e o decorador usará esse atributo como UUID do flow. Um `ValueError` é lançado se o atributo informado estiver ausente ou for *falsy* no momento de salvar.
|
||||
|
||||
```python
|
||||
@persist(key="conversation_id")
|
||||
class ProductionFlow(Flow[AppState]):
|
||||
# AppState precisa expor conversation_id; retomar a sessão recarrega o estado anterior
|
||||
...
|
||||
```
|
||||
|
||||
## Resumo
|
||||
|
||||
- **Comece com um Flow.**
|
||||
|
||||
@@ -167,6 +167,33 @@ Para mais controle, você pode aplicar `@persist()` em métodos específicos:
|
||||
# código não traduzido
|
||||
```
|
||||
|
||||
#### Usando uma Chave de Persistência Personalizada
|
||||
|
||||
Por padrão, `@persist()` usa o `state.id` gerado automaticamente como chave do estado persistido. Quando seu domínio já possui um identificador natural — por exemplo um `conversation_id` que liga várias execuções do flow à mesma sessão de usuário — passe-o como argumento `key` e `@persist` usará esse atributo como UUID do flow em vez de `id`:
|
||||
|
||||
```python
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ConversationState(BaseModel):
|
||||
conversation_id: str
|
||||
history: list[str] = []
|
||||
|
||||
@persist(key="conversation_id")
|
||||
class ConversationFlow(Flow[ConversationState]):
|
||||
@start()
|
||||
def greet(self):
|
||||
self.state.history.append("hello")
|
||||
return self.state.history
|
||||
|
||||
# Uma segunda execução com o mesmo conversation_id recarrega o estado anterior
|
||||
flow = ConversationFlow(conversation_id="user-42")
|
||||
flow.kickoff()
|
||||
```
|
||||
|
||||
Para estados baseados em dicionário `@persist` lê `state[key]`, e para estados Pydantic / objetos lê `getattr(state, key)`. Se o atributo informado estiver ausente ou for *falsy* no momento em que o estado for salvo, `@persist` lança um `ValueError` como `Flow state is missing required persistence key 'conversation_id'`, fazendo com que a falha apareça imediatamente em vez de descartar silenciosamente os dados persistidos. Chamar `@persist()` sem `key` mantém o comportamento original de usar `state.id`.
|
||||
|
||||
## Padrões Avançados de Estado
|
||||
|
||||
### Lógica Condicional Baseada no Estado
|
||||
|
||||
@@ -2272,17 +2272,13 @@ class Crew(FlowTrackable, BaseModel):
|
||||
if should_suppress_tracing_messages():
|
||||
return
|
||||
|
||||
# Don't nag users who have explicitly declined tracing
|
||||
if has_user_declined_tracing():
|
||||
return
|
||||
|
||||
console = Console()
|
||||
|
||||
if has_user_declined_tracing():
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Crew code
|
||||
• Set CREWAI_TRACING_ENABLED=true in your project's .env file
|
||||
• Run: crewai traces enable"""
|
||||
else:
|
||||
message = """Info: Tracing is disabled.
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Crew code
|
||||
|
||||
@@ -868,17 +868,13 @@ class TraceCollectionListener(BaseEventListener):
|
||||
if should_suppress_tracing_messages():
|
||||
return
|
||||
|
||||
# Don't nag users who have explicitly declined tracing
|
||||
if has_user_declined_tracing():
|
||||
return
|
||||
|
||||
console = Console()
|
||||
|
||||
if has_user_declined_tracing():
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Crew/Flow code
|
||||
• Set CREWAI_TRACING_ENABLED=true in your project's .env file
|
||||
• Run: crewai traces enable"""
|
||||
else:
|
||||
message = """Info: Tracing is disabled.
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Crew/Flow code
|
||||
|
||||
@@ -53,10 +53,19 @@ def set_suppress_tracing_messages(suppress: bool) -> object:
|
||||
def should_suppress_tracing_messages() -> bool:
|
||||
"""Check if tracing messages should be suppressed.
|
||||
|
||||
Checks the context variable first, then falls back to the
|
||||
CREWAI_SUPPRESS_TRACING_MESSAGES environment variable.
|
||||
|
||||
Returns:
|
||||
True if messages should be suppressed, False otherwise.
|
||||
"""
|
||||
return _suppress_tracing_messages.get()
|
||||
if _suppress_tracing_messages.get():
|
||||
return True
|
||||
return os.getenv("CREWAI_SUPPRESS_TRACING_MESSAGES", "false").lower() in (
|
||||
"true",
|
||||
"1",
|
||||
"yes",
|
||||
)
|
||||
|
||||
|
||||
def should_enable_tracing(*, override: bool | None = None) -> bool:
|
||||
|
||||
@@ -145,16 +145,12 @@ To update, run: uv sync --upgrade-package crewai"""
|
||||
if listener and listener.first_time_handler.is_first_time:
|
||||
return
|
||||
|
||||
if not is_tracing_enabled_in_context():
|
||||
if has_user_declined_tracing():
|
||||
message = """Info: Tracing is disabled.
|
||||
# Don't nag users who have explicitly declined tracing
|
||||
if has_user_declined_tracing():
|
||||
return
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Crew/Flow code
|
||||
• Set CREWAI_TRACING_ENABLED=true in your project's .env file
|
||||
• Run: crewai traces enable"""
|
||||
else:
|
||||
message = """Info: Tracing is disabled.
|
||||
if not is_tracing_enabled_in_context():
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Crew/Flow code
|
||||
|
||||
@@ -3546,17 +3546,13 @@ class Flow(BaseModel, Generic[T], metaclass=FlowMeta):
|
||||
if should_suppress_tracing_messages():
|
||||
return
|
||||
|
||||
# Don't nag users who have explicitly declined tracing
|
||||
if has_user_declined_tracing():
|
||||
return
|
||||
|
||||
console = Console()
|
||||
|
||||
if has_user_declined_tracing():
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Flow code
|
||||
• Set CREWAI_TRACING_ENABLED=true in your project's .env file
|
||||
• Run: crewai traces enable"""
|
||||
else:
|
||||
message = """Info: Tracing is disabled.
|
||||
message = """Info: Tracing is disabled.
|
||||
|
||||
To enable tracing, do any one of these:
|
||||
• Set tracing=True in your Flow code
|
||||
|
||||
@@ -50,6 +50,7 @@ LOG_MESSAGES: Final[dict[str, str]] = {
|
||||
"save_error": "Failed to persist state for method {}: {}",
|
||||
"state_missing": "Flow instance has no state",
|
||||
"id_missing": "Flow state must have an 'id' field for persistence",
|
||||
"key_missing": "Flow state is missing required persistence key '{}'",
|
||||
}
|
||||
|
||||
|
||||
@@ -63,6 +64,7 @@ class PersistenceDecorator:
|
||||
method_name: str,
|
||||
persistence_instance: FlowPersistence,
|
||||
verbose: bool = False,
|
||||
key: str | None = None,
|
||||
) -> None:
|
||||
"""Persist flow state with proper error handling and logging.
|
||||
|
||||
@@ -74,9 +76,12 @@ class PersistenceDecorator:
|
||||
method_name: Name of the method that triggered persistence
|
||||
persistence_instance: The persistence backend to use
|
||||
verbose: Whether to log persistence operations
|
||||
key: Optional state attribute/key to use as the persistence key.
|
||||
When None, falls back to ``state.id``.
|
||||
|
||||
Raises:
|
||||
ValueError: If flow has no state or state lacks an ID
|
||||
ValueError: If flow has no state, state lacks an ID, or the
|
||||
requested ``key`` is missing or falsy on state.
|
||||
RuntimeError: If state persistence fails
|
||||
AttributeError: If flow instance lacks required state attributes
|
||||
"""
|
||||
@@ -85,19 +90,22 @@ class PersistenceDecorator:
|
||||
if state is None:
|
||||
raise ValueError("Flow instance has no state")
|
||||
|
||||
lookup_key = key if key is not None else "id"
|
||||
flow_uuid: str | None = None
|
||||
if isinstance(state, dict):
|
||||
flow_uuid = state.get("id")
|
||||
flow_uuid = state.get(lookup_key)
|
||||
elif hasattr(state, "_unwrap"):
|
||||
unwrapped = state._unwrap()
|
||||
if isinstance(unwrapped, dict):
|
||||
flow_uuid = unwrapped.get("id")
|
||||
flow_uuid = unwrapped.get(lookup_key)
|
||||
else:
|
||||
flow_uuid = getattr(unwrapped, "id", None)
|
||||
elif isinstance(state, BaseModel) or hasattr(state, "id"):
|
||||
flow_uuid = getattr(state, "id", None)
|
||||
flow_uuid = getattr(unwrapped, lookup_key, None)
|
||||
elif isinstance(state, BaseModel) or hasattr(state, lookup_key):
|
||||
flow_uuid = getattr(state, lookup_key, None)
|
||||
|
||||
if not flow_uuid:
|
||||
if key is not None:
|
||||
raise ValueError(LOG_MESSAGES["key_missing"].format(key))
|
||||
raise ValueError("Flow state must have an 'id' field for persistence")
|
||||
|
||||
# Log state saving only if verbose is True
|
||||
@@ -127,7 +135,7 @@ class PersistenceDecorator:
|
||||
logger.error(error_msg)
|
||||
raise ValueError(error_msg) from e
|
||||
except (TypeError, ValueError) as e:
|
||||
error_msg = LOG_MESSAGES["id_missing"]
|
||||
error_msg = str(e) or LOG_MESSAGES["id_missing"]
|
||||
if verbose:
|
||||
PRINTER.print(error_msg, color="red")
|
||||
logger.error(error_msg)
|
||||
@@ -135,7 +143,9 @@ class PersistenceDecorator:
|
||||
|
||||
|
||||
def persist(
|
||||
persistence: FlowPersistence | None = None, verbose: bool = False
|
||||
persistence: FlowPersistence | None = None,
|
||||
verbose: bool = False,
|
||||
key: str | None = None,
|
||||
) -> Callable[[type | Callable[..., T]], type | Callable[..., T]]:
|
||||
"""Decorator to persist flow state.
|
||||
|
||||
@@ -148,12 +158,16 @@ def persist(
|
||||
persistence: Optional FlowPersistence implementation to use.
|
||||
If not provided, uses SQLiteFlowPersistence.
|
||||
verbose: Whether to log persistence operations. Defaults to False.
|
||||
key: Optional name of the state attribute (for Pydantic/object states)
|
||||
or dict key (for dict states) to use as the persistence key. When
|
||||
``None`` (default) the decorator falls back to ``state.id``.
|
||||
|
||||
Returns:
|
||||
A decorator that can be applied to either a class or method
|
||||
|
||||
Raises:
|
||||
ValueError: If the flow state doesn't have an 'id' field
|
||||
ValueError: If the flow state doesn't have an 'id' field, or the
|
||||
specified ``key`` is missing or falsy on state.
|
||||
RuntimeError: If state persistence fails
|
||||
|
||||
Example:
|
||||
@@ -162,6 +176,10 @@ def persist(
|
||||
@start()
|
||||
def begin(self):
|
||||
pass
|
||||
|
||||
@persist(key="conversation_id") # Custom persistence key
|
||||
class MyFlow(Flow[MyState]):
|
||||
...
|
||||
"""
|
||||
|
||||
def decorator(target: type | Callable[..., T]) -> type | Callable[..., T]:
|
||||
@@ -207,7 +225,7 @@ def persist(
|
||||
) -> Any:
|
||||
result = await original_method(self, *args, **kwargs)
|
||||
PersistenceDecorator.persist_state(
|
||||
self, method_name, actual_persistence, verbose
|
||||
self, method_name, actual_persistence, verbose, key
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -237,7 +255,7 @@ def persist(
|
||||
def method_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any:
|
||||
result = original_method(self, *args, **kwargs)
|
||||
PersistenceDecorator.persist_state(
|
||||
self, method_name, actual_persistence, verbose
|
||||
self, method_name, actual_persistence, verbose, key
|
||||
)
|
||||
return result
|
||||
|
||||
@@ -276,7 +294,7 @@ def persist(
|
||||
else:
|
||||
result = method_coro
|
||||
PersistenceDecorator.persist_state(
|
||||
flow_instance, method.__name__, actual_persistence, verbose
|
||||
flow_instance, method.__name__, actual_persistence, verbose, key
|
||||
)
|
||||
return cast(T, result)
|
||||
|
||||
@@ -295,7 +313,7 @@ def persist(
|
||||
def method_sync_wrapper(flow_instance: Any, *args: Any, **kwargs: Any) -> T:
|
||||
result = method(flow_instance, *args, **kwargs)
|
||||
PersistenceDecorator.persist_state(
|
||||
flow_instance, method.__name__, actual_persistence, verbose
|
||||
flow_instance, method.__name__, actual_persistence, verbose, key
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
@@ -1160,7 +1160,7 @@ class LLM(BaseLLM):
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
messages=messages,
|
||||
usage=None,
|
||||
)
|
||||
return structured_response
|
||||
@@ -1316,7 +1316,7 @@ class LLM(BaseLLM):
|
||||
call_type=LLMCallType.LLM_CALL,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
messages=params["messages"],
|
||||
messages=messages,
|
||||
usage=None,
|
||||
)
|
||||
return structured_response
|
||||
|
||||
@@ -88,9 +88,24 @@ class AzureCompletion(BaseLLM):
|
||||
response_format: type[BaseModel] | None = None
|
||||
is_openai_model: bool = False
|
||||
is_azure_openai_endpoint: bool = False
|
||||
credential_scopes: list[str] | None = None
|
||||
|
||||
# Responses API settings
|
||||
api: Literal["completions", "responses"] = "completions"
|
||||
reasoning_effort: str | None = None
|
||||
instructions: str | None = None
|
||||
store: bool | None = None
|
||||
previous_response_id: str | None = None
|
||||
include: list[str] | None = None
|
||||
builtin_tools: list[str] | None = None
|
||||
parse_tool_outputs: bool = False
|
||||
auto_chain: bool = False
|
||||
auto_chain_reasoning: bool = False
|
||||
max_completion_tokens: int | None = None
|
||||
|
||||
_client: Any = PrivateAttr(default=None)
|
||||
_async_client: Any = PrivateAttr(default=None)
|
||||
_responses_delegate: Any = PrivateAttr(default=None)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -115,6 +130,10 @@ class AzureCompletion(BaseLLM):
|
||||
data["api_version"] = (
|
||||
data.get("api_version") or os.getenv("AZURE_API_VERSION") or "2024-06-01"
|
||||
)
|
||||
data["credential_scopes"] = (
|
||||
data.get("credential_scopes")
|
||||
or AzureCompletion._credential_scopes_from_env()
|
||||
)
|
||||
|
||||
# Credentials and endpoint are validated lazily in `_init_clients`
|
||||
# so the LLM can be constructed before deployment env vars are set.
|
||||
@@ -140,6 +159,15 @@ class AzureCompletion(BaseLLM):
|
||||
hostname == "openai.azure.com" or hostname.endswith(".openai.azure.com")
|
||||
) and "/openai/deployments/" in endpoint
|
||||
|
||||
@staticmethod
|
||||
def _credential_scopes_from_env() -> list[str] | None:
|
||||
"""Read ``AZURE_CREDENTIAL_SCOPES`` (comma-separated) into a list."""
|
||||
raw = os.getenv("AZURE_CREDENTIAL_SCOPES")
|
||||
if not raw:
|
||||
return None
|
||||
scopes = [s.strip() for s in raw.split(",") if s.strip()]
|
||||
return scopes or None
|
||||
|
||||
@model_validator(mode="after")
|
||||
def _init_clients(self) -> AzureCompletion:
|
||||
"""Eagerly build clients when credentials are available, otherwise
|
||||
@@ -147,12 +175,89 @@ class AzureCompletion(BaseLLM):
|
||||
import time even before deployment env vars are set.
|
||||
"""
|
||||
try:
|
||||
self._client = self._build_sync_client()
|
||||
self._async_client = self._build_async_client()
|
||||
if self.api == "responses":
|
||||
self._init_responses_delegate()
|
||||
else:
|
||||
self._client = self._build_sync_client()
|
||||
self._async_client = self._build_async_client()
|
||||
except ValueError:
|
||||
pass
|
||||
return self
|
||||
|
||||
def _init_responses_delegate(self) -> None:
|
||||
"""Create an OpenAICompletion delegate for the Azure OpenAI Responses API.
|
||||
|
||||
The Azure OpenAI Responses API uses the standard OpenAI Python SDK
|
||||
with a base_url pointing to the Azure resource's /openai/v1/ endpoint.
|
||||
"""
|
||||
from crewai.llms.providers.openai.completion import OpenAICompletion
|
||||
|
||||
base_url = self._get_responses_base_url()
|
||||
|
||||
delegate_kwargs: dict[str, Any] = {
|
||||
"model": self.model,
|
||||
"api_key": self.api_key,
|
||||
"base_url": base_url,
|
||||
"api": "responses",
|
||||
"provider": "openai",
|
||||
"stream": self.stream,
|
||||
}
|
||||
|
||||
if self.temperature is not None:
|
||||
delegate_kwargs["temperature"] = self.temperature
|
||||
if self.top_p is not None:
|
||||
delegate_kwargs["top_p"] = self.top_p
|
||||
if self.max_tokens is not None:
|
||||
delegate_kwargs["max_tokens"] = self.max_tokens
|
||||
if self.max_completion_tokens is not None:
|
||||
delegate_kwargs["max_completion_tokens"] = self.max_completion_tokens
|
||||
if self.stop:
|
||||
delegate_kwargs["stop"] = self.stop
|
||||
if self.timeout is not None:
|
||||
delegate_kwargs["timeout"] = self.timeout
|
||||
if self.max_retries != 2:
|
||||
delegate_kwargs["max_retries"] = self.max_retries
|
||||
if self.reasoning_effort is not None:
|
||||
delegate_kwargs["reasoning_effort"] = self.reasoning_effort
|
||||
if self.instructions is not None:
|
||||
delegate_kwargs["instructions"] = self.instructions
|
||||
if self.store is not None:
|
||||
delegate_kwargs["store"] = self.store
|
||||
if self.previous_response_id is not None:
|
||||
delegate_kwargs["previous_response_id"] = self.previous_response_id
|
||||
if self.include is not None:
|
||||
delegate_kwargs["include"] = self.include
|
||||
if self.builtin_tools is not None:
|
||||
delegate_kwargs["builtin_tools"] = self.builtin_tools
|
||||
if self.parse_tool_outputs:
|
||||
delegate_kwargs["parse_tool_outputs"] = self.parse_tool_outputs
|
||||
if self.auto_chain:
|
||||
delegate_kwargs["auto_chain"] = self.auto_chain
|
||||
if self.auto_chain_reasoning:
|
||||
delegate_kwargs["auto_chain_reasoning"] = self.auto_chain_reasoning
|
||||
if self.response_format is not None:
|
||||
delegate_kwargs["response_format"] = self.response_format
|
||||
if self.additional_params:
|
||||
delegate_kwargs["additional_params"] = self.additional_params
|
||||
|
||||
self._responses_delegate = OpenAICompletion(**delegate_kwargs)
|
||||
|
||||
def _get_responses_base_url(self) -> str:
|
||||
"""Construct the base URL for the Azure OpenAI Responses API.
|
||||
|
||||
Extracts the scheme and host from the configured endpoint and appends
|
||||
the ``/openai/v1/`` path required by the Azure OpenAI Responses API.
|
||||
|
||||
Returns:
|
||||
The Responses API base URL, e.g.
|
||||
``https://myresource.openai.azure.com/openai/v1/``
|
||||
"""
|
||||
if not self.endpoint:
|
||||
raise ValueError("Azure endpoint is required for Responses API")
|
||||
parsed = urlparse(self.endpoint)
|
||||
base = f"{parsed.scheme}://{parsed.netloc}"
|
||||
return f"{base}/openai/v1/"
|
||||
|
||||
def _build_sync_client(self) -> Any:
|
||||
return ChatCompletionsClient(**self._make_client_kwargs())
|
||||
|
||||
@@ -188,12 +293,17 @@ class AzureCompletion(BaseLLM):
|
||||
"Azure endpoint is required. Set AZURE_ENDPOINT environment "
|
||||
"variable or pass endpoint parameter."
|
||||
)
|
||||
if self.credential_scopes is None:
|
||||
self.credential_scopes = AzureCompletion._credential_scopes_from_env()
|
||||
|
||||
client_kwargs: dict[str, Any] = {
|
||||
"endpoint": self.endpoint,
|
||||
"credential": self._resolve_credential(),
|
||||
}
|
||||
if self.api_version:
|
||||
client_kwargs["api_version"] = self.api_version
|
||||
if self.credential_scopes:
|
||||
client_kwargs["credential_scopes"] = self.credential_scopes
|
||||
return client_kwargs
|
||||
|
||||
def _resolve_credential(self) -> Any:
|
||||
@@ -252,6 +362,18 @@ class AzureCompletion(BaseLLM):
|
||||
config["presence_penalty"] = self.presence_penalty
|
||||
if self.max_tokens is not None:
|
||||
config["max_tokens"] = self.max_tokens
|
||||
if self.api != "completions":
|
||||
config["api"] = self.api
|
||||
if self.reasoning_effort is not None:
|
||||
config["reasoning_effort"] = self.reasoning_effort
|
||||
if self.instructions is not None:
|
||||
config["instructions"] = self.instructions
|
||||
if self.store is not None:
|
||||
config["store"] = self.store
|
||||
if self.max_completion_tokens is not None:
|
||||
config["max_completion_tokens"] = self.max_completion_tokens
|
||||
if self.credential_scopes:
|
||||
config["credential_scopes"] = self.credential_scopes
|
||||
return config
|
||||
|
||||
@staticmethod
|
||||
@@ -357,10 +479,10 @@ class AzureCompletion(BaseLLM):
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Call Azure AI Inference chat completions API.
|
||||
"""Call Azure AI Inference API.
|
||||
|
||||
Args:
|
||||
messages: Input messages for the chat completion
|
||||
messages: Input messages
|
||||
tools: List of tool/function definitions
|
||||
callbacks: Callback functions (not used in native implementation)
|
||||
available_functions: Available functions for tool calling
|
||||
@@ -369,8 +491,19 @@ class AzureCompletion(BaseLLM):
|
||||
response_model: Response model
|
||||
|
||||
Returns:
|
||||
Chat completion response or tool call result
|
||||
Completion response or tool call result
|
||||
"""
|
||||
if self.api == "responses":
|
||||
return self._responses_delegate.call(
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
callbacks=callbacks,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
)
|
||||
|
||||
with llm_call_context():
|
||||
try:
|
||||
# Emit call started event
|
||||
@@ -429,10 +562,10 @@ class AzureCompletion(BaseLLM):
|
||||
from_agent: Any | None = None,
|
||||
response_model: type[BaseModel] | None = None,
|
||||
) -> str | Any:
|
||||
"""Call Azure AI Inference chat completions API asynchronously.
|
||||
"""Call Azure AI Inference API asynchronously.
|
||||
|
||||
Args:
|
||||
messages: Input messages for the chat completion
|
||||
messages: Input messages
|
||||
tools: List of tool/function definitions
|
||||
callbacks: Callback functions (not used in native implementation)
|
||||
available_functions: Available functions for tool calling
|
||||
@@ -441,8 +574,19 @@ class AzureCompletion(BaseLLM):
|
||||
response_model: Pydantic model for structured output
|
||||
|
||||
Returns:
|
||||
Chat completion response or tool call result
|
||||
Completion response or tool call result
|
||||
"""
|
||||
if self.api == "responses":
|
||||
return await self._responses_delegate.acall(
|
||||
messages=messages,
|
||||
tools=tools,
|
||||
callbacks=callbacks,
|
||||
available_functions=available_functions,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_model=response_model,
|
||||
)
|
||||
|
||||
with llm_call_context():
|
||||
try:
|
||||
self._emit_call_started_event(
|
||||
@@ -1178,6 +1322,32 @@ class AzureCompletion(BaseLLM):
|
||||
return result
|
||||
return {"total_tokens": 0}
|
||||
|
||||
@property
|
||||
def last_response_id(self) -> str | None:
|
||||
"""Get the last response ID from Responses API auto-chaining."""
|
||||
if self._responses_delegate is not None:
|
||||
result: str | None = self._responses_delegate.last_response_id
|
||||
return result
|
||||
return None
|
||||
|
||||
@property
|
||||
def last_reasoning_items(self) -> list[Any] | None:
|
||||
"""Get the last reasoning items from Responses API auto-chain reasoning."""
|
||||
if self._responses_delegate is not None:
|
||||
result: list[Any] | None = self._responses_delegate.last_reasoning_items
|
||||
return result
|
||||
return None
|
||||
|
||||
def reset_chain(self) -> None:
|
||||
"""Reset the Responses API auto-chain state."""
|
||||
if self._responses_delegate is not None:
|
||||
self._responses_delegate.reset_chain()
|
||||
|
||||
def reset_reasoning_chain(self) -> None:
|
||||
"""Reset the Responses API reasoning chain state."""
|
||||
if self._responses_delegate is not None:
|
||||
self._responses_delegate.reset_reasoning_chain()
|
||||
|
||||
async def aclose(self) -> None:
|
||||
"""Close the async client and clean up resources.
|
||||
|
||||
|
||||
@@ -374,6 +374,7 @@ class MCPToolResolver:
|
||||
"MCP connection failed due to event loop cleanup issues. "
|
||||
"This may be due to authentication errors or server unavailability."
|
||||
) from e
|
||||
raise
|
||||
except asyncio.CancelledError as e:
|
||||
raise ConnectionError(
|
||||
"MCP connection was cancelled. This may indicate an authentication "
|
||||
@@ -401,6 +402,13 @@ class MCPToolResolver:
|
||||
filtered_tools.append(tool)
|
||||
tools_list = filtered_tools
|
||||
|
||||
if not tools_list:
|
||||
self._logger.log(
|
||||
"warning",
|
||||
f"No tools discovered from MCP server: {server_name}",
|
||||
)
|
||||
return cast(list[BaseTool], []), []
|
||||
|
||||
def _client_factory() -> MCPClient:
|
||||
transport, _ = self._create_transport(mcp_config)
|
||||
return MCPClient(
|
||||
|
||||
@@ -98,7 +98,14 @@ class InternalInstructor(Generic[T]):
|
||||
else:
|
||||
provider = "openai" # Default fallback
|
||||
|
||||
return instructor.from_provider(f"{provider}/{model_string}")
|
||||
extra_kwargs: dict[str, Any] = {}
|
||||
if self.llm is not None and not isinstance(self.llm, str):
|
||||
for attr in ("base_url", "api_key"):
|
||||
value = getattr(self.llm, attr, None)
|
||||
if value is not None:
|
||||
extra_kwargs[attr] = value
|
||||
|
||||
return instructor.from_provider(f"{provider}/{model_string}", **extra_kwargs)
|
||||
|
||||
def _extract_provider(self) -> str:
|
||||
"""Extract provider from LLM model name.
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"input":[{"role":"user","content":"Say hello in one sentence."}],"model":"gpt-5.2-chat"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '89'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- kkarmakar-ai-eus2.openai.azure.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 2.32.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.12
|
||||
method: POST
|
||||
uri: https://fake-azure-endpoint.openai.azure.com/openai/v1/responses
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"resp_0473c8c2b1c49f8c0069f23d0910e081958ebce72a734935c7\",\n
|
||||
\ \"object\": \"response\",\n \"created_at\": 1777483017,\n \"status\":
|
||||
\"completed\",\n \"background\": false,\n \"completed_at\": 1777483018,\n
|
||||
\ \"content_filters\": [\n {\n \"blocked\": false,\n \"source_type\":
|
||||
\"prompt\",\n \"content_filter_raw\": [],\n \"content_filter_results\":
|
||||
{\n \"jailbreak\": {\n \"detected\": false,\n \"filtered\":
|
||||
false\n },\n \"hate\": {\n \"filtered\": false,\n \"severity\":
|
||||
\"safe\"\n },\n \"sexual\": {\n \"filtered\": false,\n
|
||||
\ \"severity\": \"safe\"\n },\n \"violence\": {\n \"filtered\":
|
||||
false,\n \"severity\": \"safe\"\n },\n \"self_harm\":
|
||||
{\n \"filtered\": false,\n \"severity\": \"safe\"\n }\n
|
||||
\ },\n \"content_filter_offsets\": {\n \"start_offset\": 0,\n
|
||||
\ \"end_offset\": 368,\n \"check_offset\": 0\n }\n },\n
|
||||
\ {\n \"blocked\": false,\n \"source_type\": \"completion\",\n
|
||||
\ \"content_filter_raw\": [],\n \"content_filter_results\": {\n \"protected_material_code\":
|
||||
{\n \"detected\": false,\n \"filtered\": false\n },\n
|
||||
\ \"protected_material_text\": {\n \"detected\": false,\n \"filtered\":
|
||||
false\n },\n \"hate\": {\n \"filtered\": false,\n \"severity\":
|
||||
\"safe\"\n },\n \"sexual\": {\n \"filtered\": false,\n
|
||||
\ \"severity\": \"safe\"\n },\n \"violence\": {\n \"filtered\":
|
||||
false,\n \"severity\": \"safe\"\n },\n \"self_harm\":
|
||||
{\n \"filtered\": false,\n \"severity\": \"safe\"\n }\n
|
||||
\ },\n \"content_filter_offsets\": {\n \"start_offset\": 0,\n
|
||||
\ \"end_offset\": 53,\n \"check_offset\": 0\n }\n }\n
|
||||
\ ],\n \"error\": null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\":
|
||||
null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\":
|
||||
null,\n \"model\": \"gpt-5.2-chat\",\n \"output\": [\n {\n \"id\":
|
||||
\"rs_0473c8c2b1c49f8c0069f23d09f24481959bcf9fd847a9a475\",\n \"type\":
|
||||
\"reasoning\",\n \"summary\": []\n },\n {\n \"id\": \"msg_0473c8c2b1c49f8c0069f23d0a8ccc81958f776ad6016d7edd\",\n
|
||||
\ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\":
|
||||
[\n {\n \"type\": \"output_text\",\n \"annotations\":
|
||||
[],\n \"logprobs\": [],\n \"text\": \"Hello! \\ud83d\\ude0a\"\n
|
||||
\ }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\":
|
||||
true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\":
|
||||
null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\":
|
||||
\"medium\",\n \"summary\": null\n },\n \"safety_identifier\": null,\n
|
||||
\ \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n
|
||||
\ \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\":
|
||||
\"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\":
|
||||
0,\n \"top_p\": 0.85,\n \"truncation\": \"disabled\",\n \"usage\": {\n
|
||||
\ \"input_tokens\": 12,\n \"input_tokens_details\": {\n \"cached_tokens\":
|
||||
0\n },\n \"output_tokens\": 22,\n \"output_tokens_details\": {\n
|
||||
\ \"reasoning_tokens\": 0\n },\n \"total_tokens\": 34\n },\n \"user\":
|
||||
null,\n \"metadata\": {}\n}"
|
||||
headers:
|
||||
Content-Length:
|
||||
- '3203'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 29 Apr 2026 17:16:59 GMT
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
apim-request-id:
|
||||
- APIM-REQUEST-ID-XXX
|
||||
skip-error-remapping:
|
||||
- 'true'
|
||||
x-content-type-options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
x-ms-client-request-id:
|
||||
- X-MS-CLIENT-REQUEST-ID-XXX
|
||||
x-ms-is-spilled-over:
|
||||
- 'false'
|
||||
x-ms-region:
|
||||
- X-MS-REGION-XXX
|
||||
x-ratelimit-abusepenalty-active:
|
||||
- 'False'
|
||||
x-ratelimit-key:
|
||||
- gpt-5.2-chat
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-renewalperiod-requests:
|
||||
- '60'
|
||||
x-ratelimit-renewalperiod-tokens:
|
||||
- '60'
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,137 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"input":[{"role":"user","content":"What is 2 + 2? Be brief."}],"model":"gpt-5.2-chat","tools":[{"type":"web_search_preview"}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '127'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- kkarmakar-ai-eus2.openai.azure.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 2.32.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.12
|
||||
method: POST
|
||||
uri: https://fake-azure-endpoint.openai.azure.com/openai/v1/responses
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"resp_0d80ad9adad65fca0069f23d0c904c8194862acae4bd866cf5\",\n
|
||||
\ \"object\": \"response\",\n \"created_at\": 1777483020,\n \"status\":
|
||||
\"completed\",\n \"background\": false,\n \"completed_at\": 1777483022,\n
|
||||
\ \"content_filters\": [\n {\n \"blocked\": false,\n \"source_type\":
|
||||
\"prompt\",\n \"content_filter_raw\": [],\n \"content_filter_results\":
|
||||
{\n \"jailbreak\": {\n \"detected\": false,\n \"filtered\":
|
||||
false\n },\n \"hate\": {\n \"filtered\": false,\n \"severity\":
|
||||
\"safe\"\n },\n \"sexual\": {\n \"filtered\": false,\n
|
||||
\ \"severity\": \"safe\"\n },\n \"violence\": {\n \"filtered\":
|
||||
false,\n \"severity\": \"safe\"\n },\n \"self_harm\":
|
||||
{\n \"filtered\": false,\n \"severity\": \"safe\"\n }\n
|
||||
\ },\n \"content_filter_offsets\": {\n \"start_offset\": 0,\n
|
||||
\ \"end_offset\": 19017,\n \"check_offset\": 0\n }\n },\n
|
||||
\ {\n \"blocked\": false,\n \"source_type\": \"completion\",\n
|
||||
\ \"content_filter_raw\": [],\n \"content_filter_results\": {\n \"hate\":
|
||||
{\n \"filtered\": false,\n \"severity\": \"safe\"\n },\n
|
||||
\ \"sexual\": {\n \"filtered\": false,\n \"severity\":
|
||||
\"safe\"\n },\n \"violence\": {\n \"filtered\": false,\n
|
||||
\ \"severity\": \"safe\"\n },\n \"self_harm\": {\n \"filtered\":
|
||||
false,\n \"severity\": \"safe\"\n },\n \"protected_material_code\":
|
||||
{\n \"detected\": false,\n \"filtered\": false\n },\n
|
||||
\ \"protected_material_text\": {\n \"detected\": false,\n \"filtered\":
|
||||
false\n }\n },\n \"content_filter_offsets\": {\n \"start_offset\":
|
||||
0,\n \"end_offset\": 889,\n \"check_offset\": 0\n }\n }\n
|
||||
\ ],\n \"error\": null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\":
|
||||
null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\":
|
||||
null,\n \"model\": \"gpt-5.2-chat\",\n \"output\": [\n {\n \"id\":
|
||||
\"rs_0d80ad9adad65fca0069f23d0d8b8c8194b1a9ab61ddc3420d\",\n \"type\":
|
||||
\"reasoning\",\n \"summary\": []\n },\n {\n \"id\": \"msg_0d80ad9adad65fca0069f23d0e262081949c36d6cc1958eeed\",\n
|
||||
\ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\":
|
||||
[\n {\n \"type\": \"output_text\",\n \"annotations\":
|
||||
[],\n \"logprobs\": [],\n \"text\": \"2 + 2 = 4.\"\n }\n
|
||||
\ ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\":
|
||||
true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\":
|
||||
null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\":
|
||||
\"medium\",\n \"summary\": null\n },\n \"safety_identifier\": null,\n
|
||||
\ \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n
|
||||
\ \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\":
|
||||
\"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tools\": [\n {\n \"type\":
|
||||
\"web_search_preview\",\n \"search_content_types\": [\n \"text\"\n
|
||||
\ ],\n \"search_context_size\": \"medium\",\n \"user_location\":
|
||||
{\n \"type\": \"approximate\",\n \"city\": null,\n \"country\":
|
||||
\"US\",\n \"region\": null,\n \"timezone\": null\n }\n
|
||||
\ }\n ],\n \"top_logprobs\": 0,\n \"top_p\": 0.85,\n \"truncation\":
|
||||
\"disabled\",\n \"usage\": {\n \"input_tokens\": 4312,\n \"input_tokens_details\":
|
||||
{\n \"cached_tokens\": 0\n },\n \"output_tokens\": 28,\n \"output_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0\n },\n \"total_tokens\": 4340\n },\n
|
||||
\ \"user\": null,\n \"metadata\": {}\n}"
|
||||
headers:
|
||||
Content-Length:
|
||||
- '3507'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 29 Apr 2026 17:17:03 GMT
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
apim-request-id:
|
||||
- APIM-REQUEST-ID-XXX
|
||||
skip-error-remapping:
|
||||
- 'true'
|
||||
x-content-type-options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
x-ms-client-request-id:
|
||||
- X-MS-CLIENT-REQUEST-ID-XXX
|
||||
x-ms-is-spilled-over:
|
||||
- 'false'
|
||||
x-ms-region:
|
||||
- X-MS-REGION-XXX
|
||||
x-ratelimit-abusepenalty-active:
|
||||
- 'False'
|
||||
x-ratelimit-key:
|
||||
- gpt-5.2-chat
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-renewalperiod-requests:
|
||||
- '60'
|
||||
x-ratelimit-renewalperiod-tokens:
|
||||
- '60'
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,84 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages": [{"role": "user", "content": "Say hello in one sentence."}],
|
||||
"stream": false}'
|
||||
headers:
|
||||
Accept:
|
||||
- application/json
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '90'
|
||||
Content-Type:
|
||||
- application/json
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
api-key:
|
||||
- X-API-KEY-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
x-ms-client-request-id:
|
||||
- X-MS-CLIENT-REQUEST-ID-XXX
|
||||
method: POST
|
||||
uri: https://fake-azure-endpoint.openai.azure.com/openai/deployments/gpt-5.2-chat/chat/completions?api-version=2024-02-15-preview
|
||||
response:
|
||||
body:
|
||||
string: "{\"choices\":[{\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"protected_material_code\":{\"detected\":false,\"filtered\":false},\"protected_material_text\":{\"detected\":false,\"filtered\":false},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}},\"finish_reason\":\"stop\",\"index\":0,\"logprobs\":null,\"message\":{\"annotations\":[],\"content\":\"Hello!
|
||||
\U0001F60A\",\"refusal\":null,\"role\":\"assistant\"}}],\"created\":1777483024,\"id\":\"chatcmpl-Da2oyIDHFopG5fmCKbhDiEYG5ciBN\",\"model\":\"gpt-5.2-chat-latest\",\"object\":\"chat.completion\",\"prompt_filter_results\":[{\"prompt_index\":0,\"content_filter_results\":{\"hate\":{\"filtered\":false,\"severity\":\"safe\"},\"jailbreak\":{\"detected\":false,\"filtered\":false},\"self_harm\":{\"filtered\":false,\"severity\":\"safe\"},\"sexual\":{\"filtered\":false,\"severity\":\"safe\"},\"violence\":{\"filtered\":false,\"severity\":\"safe\"}}}],\"service_tier\":\"default\",\"system_fingerprint\":null,\"usage\":{\"completion_tokens\":13,\"completion_tokens_details\":{\"accepted_prediction_tokens\":0,\"audio_tokens\":0,\"reasoning_tokens\":0,\"rejected_prediction_tokens\":0},\"prompt_tokens\":12,\"prompt_tokens_details\":{\"audio_tokens\":0,\"cached_tokens\":0},\"total_tokens\":25}}\n"
|
||||
headers:
|
||||
Content-Length:
|
||||
- '1233'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 29 Apr 2026 17:17:05 GMT
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
apim-request-id:
|
||||
- APIM-REQUEST-ID-XXX
|
||||
azureml-model-session:
|
||||
- AZUREML-MODEL-SESSION-XXX
|
||||
skip-error-remapping:
|
||||
- 'true'
|
||||
x-accel-buffering:
|
||||
- 'no'
|
||||
x-content-type-options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
x-ms-client-request-id:
|
||||
- X-MS-CLIENT-REQUEST-ID-XXX
|
||||
x-ms-deployment-name:
|
||||
- gpt-5.2-chat
|
||||
x-ms-is-spilled-over:
|
||||
- 'false'
|
||||
x-ms-rai-invoked:
|
||||
- 'true'
|
||||
x-ms-region:
|
||||
- X-MS-REGION-XXX
|
||||
x-ratelimit-abusepenalty-active:
|
||||
- 'False'
|
||||
x-ratelimit-key:
|
||||
- gpt-5.2-chat
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-renewalperiod-requests:
|
||||
- '60'
|
||||
x-ratelimit-renewalperiod-tokens:
|
||||
- '60'
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -0,0 +1,128 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"input":[{"role":"user","content":"Say hello in one sentence."}],"model":"gpt-5.2-chat"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '89'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- kkarmakar-ai-eus2.openai.azure.com
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
x-stainless-async:
|
||||
- async:asyncio
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
x-stainless-package-version:
|
||||
- 2.32.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.12
|
||||
method: POST
|
||||
uri: https://fake-azure-endpoint.openai.azure.com/openai/v1/responses
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"resp_02861ec017218a520069f23d21dbf88193aa91a73d63d91302\",\n
|
||||
\ \"object\": \"response\",\n \"created_at\": 1777483041,\n \"status\":
|
||||
\"completed\",\n \"background\": false,\n \"completed_at\": 1777483043,\n
|
||||
\ \"content_filters\": [\n {\n \"blocked\": false,\n \"source_type\":
|
||||
\"prompt\",\n \"content_filter_raw\": [],\n \"content_filter_results\":
|
||||
{\n \"jailbreak\": {\n \"detected\": false,\n \"filtered\":
|
||||
false\n }\n },\n \"content_filter_offsets\": {\n \"start_offset\":
|
||||
0,\n \"end_offset\": 368,\n \"check_offset\": 0\n }\n },\n
|
||||
\ {\n \"blocked\": false,\n \"source_type\": \"completion\",\n
|
||||
\ \"content_filter_raw\": [],\n \"content_filter_results\": {\n \"protected_material_text\":
|
||||
{\n \"detected\": false,\n \"filtered\": false\n },\n
|
||||
\ \"protected_material_code\": {\n \"detected\": false,\n \"filtered\":
|
||||
false\n },\n \"hate\": {\n \"filtered\": false,\n \"severity\":
|
||||
\"safe\"\n },\n \"sexual\": {\n \"filtered\": false,\n
|
||||
\ \"severity\": \"safe\"\n },\n \"violence\": {\n \"filtered\":
|
||||
false,\n \"severity\": \"safe\"\n },\n \"self_harm\":
|
||||
{\n \"filtered\": false,\n \"severity\": \"safe\"\n }\n
|
||||
\ },\n \"content_filter_offsets\": {\n \"start_offset\": 0,\n
|
||||
\ \"end_offset\": 44,\n \"check_offset\": 0\n }\n }\n
|
||||
\ ],\n \"error\": null,\n \"frequency_penalty\": 0.0,\n \"incomplete_details\":
|
||||
null,\n \"instructions\": null,\n \"max_output_tokens\": null,\n \"max_tool_calls\":
|
||||
null,\n \"model\": \"gpt-5.2-chat\",\n \"output\": [\n {\n \"id\":
|
||||
\"rs_02861ec017218a520069f23d2287ac819399dd23b8dd56028e\",\n \"type\":
|
||||
\"reasoning\",\n \"summary\": []\n },\n {\n \"id\": \"msg_02861ec017218a520069f23d23082c81939838ab2eebf4e89c\",\n
|
||||
\ \"type\": \"message\",\n \"status\": \"completed\",\n \"content\":
|
||||
[\n {\n \"type\": \"output_text\",\n \"annotations\":
|
||||
[],\n \"logprobs\": [],\n \"text\": \"Hello! \\ud83d\\udc4b\"\n
|
||||
\ }\n ],\n \"role\": \"assistant\"\n }\n ],\n \"parallel_tool_calls\":
|
||||
true,\n \"presence_penalty\": 0.0,\n \"previous_response_id\": null,\n \"prompt_cache_key\":
|
||||
null,\n \"prompt_cache_retention\": null,\n \"reasoning\": {\n \"effort\":
|
||||
\"medium\",\n \"summary\": null\n },\n \"safety_identifier\": null,\n
|
||||
\ \"service_tier\": \"default\",\n \"store\": true,\n \"temperature\": 1.0,\n
|
||||
\ \"text\": {\n \"format\": {\n \"type\": \"text\"\n },\n \"verbosity\":
|
||||
\"medium\"\n },\n \"tool_choice\": \"auto\",\n \"tools\": [],\n \"top_logprobs\":
|
||||
0,\n \"top_p\": 0.85,\n \"truncation\": \"disabled\",\n \"usage\": {\n
|
||||
\ \"input_tokens\": 12,\n \"input_tokens_details\": {\n \"cached_tokens\":
|
||||
0\n },\n \"output_tokens\": 21,\n \"output_tokens_details\": {\n
|
||||
\ \"reasoning_tokens\": 0\n },\n \"total_tokens\": 33\n },\n \"user\":
|
||||
null,\n \"metadata\": {}\n}"
|
||||
headers:
|
||||
Content-Length:
|
||||
- '2844'
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Wed, 29 Apr 2026 17:17:25 GMT
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
apim-request-id:
|
||||
- APIM-REQUEST-ID-XXX
|
||||
skip-error-remapping:
|
||||
- 'true'
|
||||
x-content-type-options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
x-ms-client-request-id:
|
||||
- X-MS-CLIENT-REQUEST-ID-XXX
|
||||
x-ms-is-spilled-over:
|
||||
- 'false'
|
||||
x-ms-region:
|
||||
- X-MS-REGION-XXX
|
||||
x-ratelimit-abusepenalty-active:
|
||||
- 'False'
|
||||
x-ratelimit-key:
|
||||
- gpt-5.2-chat
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
x-ratelimit-renewalperiod-requests:
|
||||
- '60'
|
||||
x-ratelimit-renewalperiod-tokens:
|
||||
- '60'
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1518,3 +1518,120 @@ def test_azure_no_detail_fields():
|
||||
assert usage["completion_tokens"] == 30
|
||||
assert usage["cached_prompt_tokens"] == 0
|
||||
assert usage["reasoning_tokens"] == 0
|
||||
|
||||
|
||||
def test_azure_credential_scopes_passed_to_client():
|
||||
"""`credential_scopes` constructor arg flows through `_make_client_kwargs`
|
||||
so the underlying ChatCompletionsClient requests tokens for the requested
|
||||
audience (e.g. ``cognitiveservices.azure.com/.default``)."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
scopes = ["https://cognitiveservices.azure.com/.default"]
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
llm = AzureCompletion(
|
||||
model="gpt-4",
|
||||
api_key="test-key",
|
||||
endpoint="https://test.openai.azure.com",
|
||||
credential_scopes=scopes,
|
||||
)
|
||||
kwargs = llm._make_client_kwargs()
|
||||
assert kwargs["credential_scopes"] == scopes
|
||||
|
||||
|
||||
def test_azure_credential_scopes_omitted_by_default():
|
||||
"""Without explicit scopes or env var, the kwarg must not be set so the
|
||||
Azure SDK chooses its own default audience."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
llm = AzureCompletion(
|
||||
model="gpt-4",
|
||||
api_key="test-key",
|
||||
endpoint="https://test.openai.azure.com",
|
||||
)
|
||||
kwargs = llm._make_client_kwargs()
|
||||
assert "credential_scopes" not in kwargs
|
||||
|
||||
|
||||
def test_azure_credential_scopes_from_env_comma_separated():
|
||||
"""``AZURE_CREDENTIAL_SCOPES`` accepts a comma-separated list. Whitespace
|
||||
around entries is stripped; empty entries are dropped."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"AZURE_API_KEY": "test-key",
|
||||
"AZURE_ENDPOINT": "https://test.openai.azure.com",
|
||||
"AZURE_CREDENTIAL_SCOPES": " https://cognitiveservices.azure.com/.default , https://other/.default ",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
llm = AzureCompletion(model="gpt-4")
|
||||
assert llm.credential_scopes == [
|
||||
"https://cognitiveservices.azure.com/.default",
|
||||
"https://other/.default",
|
||||
]
|
||||
kwargs = llm._make_client_kwargs()
|
||||
assert kwargs["credential_scopes"] == llm.credential_scopes
|
||||
|
||||
|
||||
def test_azure_credential_scopes_constructor_overrides_env():
|
||||
"""A constructor-provided ``credential_scopes`` must win over the env var,
|
||||
matching how endpoint/api_key precedence works elsewhere in this provider."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
explicit = ["https://explicit/.default"]
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"AZURE_API_KEY": "test-key",
|
||||
"AZURE_ENDPOINT": "https://test.openai.azure.com",
|
||||
"AZURE_CREDENTIAL_SCOPES": "https://env/.default",
|
||||
},
|
||||
clear=True,
|
||||
):
|
||||
llm = AzureCompletion(model="gpt-4", credential_scopes=explicit)
|
||||
assert llm.credential_scopes == explicit
|
||||
|
||||
|
||||
def test_azure_credential_scopes_lazy_env_read():
|
||||
"""When the LLM is built before ``AZURE_CREDENTIAL_SCOPES`` is exported
|
||||
(e.g. constructed at module import), the lazy client builder must still
|
||||
pick up the env value — same pattern as the existing api_key/endpoint
|
||||
lazy reads."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
llm = AzureCompletion(
|
||||
model="gpt-4",
|
||||
api_key="test-key",
|
||||
endpoint="https://test.openai.azure.com",
|
||||
)
|
||||
assert llm.credential_scopes is None
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"AZURE_CREDENTIAL_SCOPES": "https://late/.default"},
|
||||
clear=True,
|
||||
):
|
||||
kwargs = llm._make_client_kwargs()
|
||||
assert kwargs["credential_scopes"] == ["https://late/.default"]
|
||||
assert llm.credential_scopes == ["https://late/.default"]
|
||||
|
||||
|
||||
def test_azure_credential_scopes_in_to_config_dict():
|
||||
"""Config round-trips the scopes so an LLM rebuilt from `to_config_dict`
|
||||
keeps the same audience."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
scopes = ["https://cognitiveservices.azure.com/.default"]
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
llm = AzureCompletion(
|
||||
model="gpt-4",
|
||||
api_key="test-key",
|
||||
endpoint="https://test.openai.azure.com",
|
||||
credential_scopes=scopes,
|
||||
)
|
||||
config = llm.to_config_dict()
|
||||
assert config["credential_scopes"] == scopes
|
||||
|
||||
395
lib/crewai/tests/llms/azure/test_azure_responses.py
Normal file
395
lib/crewai/tests/llms/azure/test_azure_responses.py
Normal file
@@ -0,0 +1,395 @@
|
||||
"""Tests for Azure OpenAI Responses API support.
|
||||
|
||||
Verifies that AzureCompletion with api='responses' correctly delegates
|
||||
to OpenAICompletion configured with the Azure OpenAI /openai/v1/ base URL.
|
||||
"""
|
||||
|
||||
import os
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def azure_env():
|
||||
"""Set Azure environment variables for tests."""
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{
|
||||
"AZURE_API_KEY": "test-azure-key",
|
||||
"AZURE_ENDPOINT": "https://myresource.openai.azure.com",
|
||||
},
|
||||
):
|
||||
yield
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_openai_completion():
|
||||
"""Mock OpenAICompletion to avoid real client creation.
|
||||
|
||||
Patches at the source module so that the dynamic import inside
|
||||
_init_responses_delegate picks up the mock.
|
||||
"""
|
||||
instance = MagicMock()
|
||||
instance.call = MagicMock(return_value="responses-result")
|
||||
instance.acall = AsyncMock(return_value="async-responses-result")
|
||||
instance.last_response_id = "resp_abc123"
|
||||
instance.last_reasoning_items = [{"type": "reasoning"}]
|
||||
instance.reset_chain = MagicMock()
|
||||
instance.reset_reasoning_chain = MagicMock()
|
||||
mock_cls = MagicMock(return_value=instance)
|
||||
|
||||
with patch(
|
||||
"crewai.llms.providers.openai.completion.OpenAICompletion",
|
||||
mock_cls,
|
||||
):
|
||||
yield mock_cls, instance
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helper to build AzureCompletion with api="responses" while mocking imports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _create_azure_responses(**overrides):
|
||||
"""Create an AzureCompletion(api='responses').
|
||||
|
||||
Must be called inside a context where OpenAICompletion is already mocked
|
||||
(i.e. via the ``mock_openai_completion`` fixture).
|
||||
"""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
defaults = {
|
||||
"model": "gpt-4o",
|
||||
"api_key": "test-azure-key",
|
||||
"endpoint": "https://myresource.openai.azure.com",
|
||||
"api": "responses",
|
||||
}
|
||||
defaults.update(overrides)
|
||||
return AzureCompletion(**defaults)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Initialization tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAzureResponsesInit:
|
||||
"""Test initialization with api='responses'."""
|
||||
|
||||
def test_default_api_is_completions(self):
|
||||
"""Default api should be 'completions' (existing behaviour)."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
comp = AzureCompletion(
|
||||
model="gpt-4o",
|
||||
api_key="key",
|
||||
endpoint="https://res.openai.azure.com",
|
||||
)
|
||||
assert comp.api == "completions"
|
||||
assert comp._responses_delegate is None
|
||||
|
||||
def test_responses_api_creates_delegate(self, mock_openai_completion):
|
||||
mock_cls, instance = mock_openai_completion
|
||||
comp = _create_azure_responses()
|
||||
|
||||
assert comp.api == "responses"
|
||||
assert comp._responses_delegate is instance
|
||||
mock_cls.assert_called_once()
|
||||
|
||||
def test_completions_clients_not_created_in_responses_mode(
|
||||
self, mock_openai_completion
|
||||
):
|
||||
"""When api='responses', azure-ai-inference clients should not be created."""
|
||||
_mock_cls, _ = mock_openai_completion
|
||||
comp = _create_azure_responses()
|
||||
|
||||
assert comp._client is None
|
||||
assert comp._async_client is None
|
||||
|
||||
def test_responses_base_url_from_base_endpoint(self, mock_openai_completion):
|
||||
mock_cls, _ = mock_openai_completion
|
||||
_create_azure_responses(
|
||||
endpoint="https://myresource.openai.azure.com",
|
||||
)
|
||||
call_kwargs = mock_cls.call_args[1]
|
||||
assert (
|
||||
call_kwargs["base_url"] == "https://myresource.openai.azure.com/openai/v1/"
|
||||
)
|
||||
|
||||
def test_responses_base_url_strips_deployment_path(self, mock_openai_completion):
|
||||
"""Endpoint with /openai/deployments/... should still produce correct base_url."""
|
||||
mock_cls, _ = mock_openai_completion
|
||||
_create_azure_responses(
|
||||
endpoint="https://myresource.openai.azure.com/openai/deployments/gpt-4o",
|
||||
)
|
||||
call_kwargs = mock_cls.call_args[1]
|
||||
assert (
|
||||
call_kwargs["base_url"] == "https://myresource.openai.azure.com/openai/v1/"
|
||||
)
|
||||
|
||||
def test_responses_base_url_preserves_port(self, mock_openai_completion):
|
||||
mock_cls, _ = mock_openai_completion
|
||||
_create_azure_responses(
|
||||
endpoint="https://myresource.openai.azure.com:8443/openai/deployments/gpt-4o",
|
||||
)
|
||||
call_kwargs = mock_cls.call_args[1]
|
||||
assert (
|
||||
call_kwargs["base_url"]
|
||||
== "https://myresource.openai.azure.com:8443/openai/v1/"
|
||||
)
|
||||
|
||||
def test_delegate_receives_model_and_api_key(self, mock_openai_completion):
|
||||
mock_cls, _ = mock_openai_completion
|
||||
_create_azure_responses(
|
||||
model="gpt-4o",
|
||||
api_key="my-key",
|
||||
)
|
||||
call_kwargs = mock_cls.call_args[1]
|
||||
assert call_kwargs["model"] == "gpt-4o"
|
||||
assert call_kwargs["api_key"] == "my-key"
|
||||
assert call_kwargs["api"] == "responses"
|
||||
assert call_kwargs["provider"] == "openai"
|
||||
|
||||
def test_delegate_receives_optional_params(self, mock_openai_completion):
|
||||
mock_cls, _ = mock_openai_completion
|
||||
_create_azure_responses(
|
||||
temperature=0.5,
|
||||
top_p=0.9,
|
||||
max_tokens=1000,
|
||||
max_completion_tokens=800,
|
||||
reasoning_effort="medium",
|
||||
instructions="Be helpful",
|
||||
store=True,
|
||||
previous_response_id="resp_prev",
|
||||
include=["reasoning.encrypted_content"],
|
||||
builtin_tools=["web_search"],
|
||||
parse_tool_outputs=True,
|
||||
auto_chain=True,
|
||||
auto_chain_reasoning=True,
|
||||
stream=True,
|
||||
)
|
||||
call_kwargs = mock_cls.call_args[1]
|
||||
assert call_kwargs["temperature"] == 0.5
|
||||
assert call_kwargs["top_p"] == 0.9
|
||||
assert call_kwargs["max_tokens"] == 1000
|
||||
assert call_kwargs["max_completion_tokens"] == 800
|
||||
assert call_kwargs["reasoning_effort"] == "medium"
|
||||
assert call_kwargs["instructions"] == "Be helpful"
|
||||
assert call_kwargs["store"] is True
|
||||
assert call_kwargs["previous_response_id"] == "resp_prev"
|
||||
assert call_kwargs["include"] == ["reasoning.encrypted_content"]
|
||||
assert call_kwargs["builtin_tools"] == ["web_search"]
|
||||
assert call_kwargs["parse_tool_outputs"] is True
|
||||
assert call_kwargs["auto_chain"] is True
|
||||
assert call_kwargs["auto_chain_reasoning"] is True
|
||||
assert call_kwargs["stream"] is True
|
||||
|
||||
def test_delegate_omits_unset_optional_params(self, mock_openai_completion):
|
||||
"""Params left at defaults should not be passed to the delegate."""
|
||||
mock_cls, _ = mock_openai_completion
|
||||
_create_azure_responses()
|
||||
call_kwargs = mock_cls.call_args[1]
|
||||
# These should NOT be in kwargs because they were not set
|
||||
assert "temperature" not in call_kwargs
|
||||
assert "reasoning_effort" not in call_kwargs
|
||||
assert "instructions" not in call_kwargs
|
||||
assert "store" not in call_kwargs
|
||||
assert "max_completion_tokens" not in call_kwargs
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Call delegation tests (VCR cassette-based)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAzureResponsesCall:
|
||||
"""Test call / acall delegation to the Responses API using VCR cassettes."""
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_call_delegates_to_responses(self):
|
||||
from crewai.llm import LLM
|
||||
|
||||
llm = LLM(model="azure/gpt-5.2-chat", api="responses")
|
||||
result = llm.call("Say hello in one sentence.")
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_call_with_tools_delegates(self):
|
||||
from crewai.llm import LLM
|
||||
|
||||
llm = LLM(
|
||||
model="azure/gpt-5.2-chat",
|
||||
api="responses",
|
||||
builtin_tools=["web_search"],
|
||||
)
|
||||
result = llm.call("What is 2 + 2? Be brief.")
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_completions_call_unchanged(self):
|
||||
"""Default api='completions' should not use the responses delegate."""
|
||||
from crewai.llm import LLM
|
||||
|
||||
llm = LLM(model="azure/gpt-5.2-chat")
|
||||
result = llm.call("Say hello in one sentence.")
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Delegated property & method tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAzureResponsesProperties:
|
||||
"""Test properties and methods delegated to the responses delegate."""
|
||||
|
||||
def test_last_response_id(self, mock_openai_completion):
|
||||
_mock_cls, _ = mock_openai_completion
|
||||
comp = _create_azure_responses()
|
||||
assert comp.last_response_id == "resp_abc123"
|
||||
|
||||
def test_last_response_id_none_for_completions(self):
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
comp = AzureCompletion(
|
||||
model="gpt-4o",
|
||||
api_key="key",
|
||||
endpoint="https://res.openai.azure.com",
|
||||
)
|
||||
assert comp.last_response_id is None
|
||||
|
||||
def test_last_reasoning_items(self, mock_openai_completion):
|
||||
_mock_cls, _ = mock_openai_completion
|
||||
comp = _create_azure_responses()
|
||||
assert comp.last_reasoning_items == [{"type": "reasoning"}]
|
||||
|
||||
def test_reset_chain(self, mock_openai_completion):
|
||||
_mock_cls, instance = mock_openai_completion
|
||||
comp = _create_azure_responses()
|
||||
comp.reset_chain()
|
||||
instance.reset_chain.assert_called_once()
|
||||
|
||||
def test_reset_reasoning_chain(self, mock_openai_completion):
|
||||
_mock_cls, instance = mock_openai_completion
|
||||
comp = _create_azure_responses()
|
||||
comp.reset_reasoning_chain()
|
||||
instance.reset_reasoning_chain.assert_called_once()
|
||||
|
||||
def test_reset_chain_noop_for_completions(self):
|
||||
"""reset_chain should not raise when delegate is None."""
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
comp = AzureCompletion(
|
||||
model="gpt-4o",
|
||||
api_key="key",
|
||||
endpoint="https://res.openai.azure.com",
|
||||
)
|
||||
comp.reset_chain() # should not raise
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Feature-support method tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAzureResponsesFeatures:
|
||||
"""Test supports_* and config methods."""
|
||||
|
||||
def test_supports_function_calling_responses(self, mock_openai_completion):
|
||||
_mock_cls, _ = mock_openai_completion
|
||||
comp = _create_azure_responses()
|
||||
assert comp.supports_function_calling() is True
|
||||
|
||||
def test_supports_function_calling_completions_openai_model(self):
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
comp = AzureCompletion(
|
||||
model="gpt-4o",
|
||||
api_key="key",
|
||||
endpoint="https://res.openai.azure.com",
|
||||
)
|
||||
assert comp.supports_function_calling() is True
|
||||
|
||||
def test_supports_stop_words_false_for_responses(self, mock_openai_completion):
|
||||
_mock_cls, _ = mock_openai_completion
|
||||
comp = _create_azure_responses(model="o4-mini")
|
||||
assert comp.supports_stop_words() is False
|
||||
|
||||
def test_supports_stop_words_true_for_completions_gpt4(self):
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
comp = AzureCompletion(
|
||||
model="gpt-4o",
|
||||
api_key="key",
|
||||
endpoint="https://res.openai.azure.com",
|
||||
)
|
||||
assert comp.supports_stop_words() is True
|
||||
|
||||
def test_to_config_dict_includes_responses_fields(self, mock_openai_completion):
|
||||
_mock_cls, _ = mock_openai_completion
|
||||
comp = _create_azure_responses(
|
||||
reasoning_effort="high",
|
||||
instructions="Be concise",
|
||||
store=True,
|
||||
max_completion_tokens=500,
|
||||
)
|
||||
config = comp.to_config_dict()
|
||||
assert config["api"] == "responses"
|
||||
assert config["reasoning_effort"] == "high"
|
||||
assert config["instructions"] == "Be concise"
|
||||
assert config["store"] is True
|
||||
assert config["max_completion_tokens"] == 500
|
||||
|
||||
def test_to_config_dict_omits_api_for_completions(self):
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
comp = AzureCompletion(
|
||||
model="gpt-4o",
|
||||
api_key="key",
|
||||
endpoint="https://res.openai.azure.com",
|
||||
)
|
||||
config = comp.to_config_dict()
|
||||
assert "api" not in config
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# LLM factory integration test
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestAzureResponsesViaLLMFactory:
|
||||
"""Test that the LLM factory passes api='responses' through to AzureCompletion."""
|
||||
|
||||
@pytest.mark.usefixtures("azure_env")
|
||||
def test_llm_factory_passes_api_kwarg(self):
|
||||
"""LLM(model='azure/gpt-4o', api='responses') should create AzureCompletion
|
||||
with api='responses' and a delegate."""
|
||||
with (
|
||||
patch(
|
||||
"crewai.llms.providers.openai.completion.OpenAI",
|
||||
),
|
||||
patch(
|
||||
"crewai.llms.providers.openai.completion.AsyncOpenAI",
|
||||
),
|
||||
):
|
||||
from crewai.llm import LLM
|
||||
|
||||
llm = LLM(model="azure/gpt-4o", api="responses")
|
||||
|
||||
from crewai.llms.providers.azure.completion import AzureCompletion
|
||||
|
||||
assert isinstance(llm, AzureCompletion)
|
||||
assert llm.api == "responses"
|
||||
assert llm._responses_delegate is not None
|
||||
15
lib/crewai/tests/llms/azure/test_azure_responses_async.py
Normal file
15
lib/crewai/tests/llms/azure/test_azure_responses_async.py
Normal file
@@ -0,0 +1,15 @@
|
||||
"""Async tests for Azure OpenAI Responses API support."""
|
||||
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
@pytest.mark.asyncio
|
||||
async def test_acall_delegates_to_responses():
|
||||
from crewai.llm import LLM
|
||||
|
||||
llm = LLM(model="azure/gpt-5.2-chat", api="responses")
|
||||
result = await llm.acall("Say hello in one sentence.")
|
||||
|
||||
assert isinstance(result, str)
|
||||
assert len(result) > 0
|
||||
99
lib/crewai/tests/mcp/test_tool_resolver_native.py
Normal file
99
lib/crewai/tests/mcp/test_tool_resolver_native.py
Normal file
@@ -0,0 +1,99 @@
|
||||
"""Tests for MCPToolResolver native (non-AMP) resolution paths."""
|
||||
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from crewai.agent.core import Agent
|
||||
from crewai.mcp.config import MCPServerHTTP
|
||||
from crewai.mcp.tool_resolver import MCPToolResolver
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def agent():
|
||||
return Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def resolver(agent):
|
||||
return MCPToolResolver(agent=agent, logger=agent._logger)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def http_config():
|
||||
return MCPServerHTTP(url="https://mcp.example.com/api")
|
||||
|
||||
|
||||
class TestResolveNativeEmptyTools:
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
def test_logs_warning_and_returns_empty_when_server_has_no_tools(
|
||||
self, mock_client_class, resolver, http_config
|
||||
):
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=[])
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
mock_log = MagicMock()
|
||||
resolver._logger = MagicMock(log=mock_log)
|
||||
|
||||
tools, clients = resolver._resolve_native(http_config)
|
||||
|
||||
assert tools == []
|
||||
assert clients == []
|
||||
warning_calls = [
|
||||
call for call in mock_log.call_args_list if call.args[0] == "warning"
|
||||
]
|
||||
assert any(
|
||||
"No tools discovered from MCP server" in call.args[1]
|
||||
for call in warning_calls
|
||||
)
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
def test_logs_warning_when_tool_filter_removes_all_tools(
|
||||
self, mock_client_class, resolver
|
||||
):
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(
|
||||
return_value=[{"name": "search", "description": "Search"}]
|
||||
)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
config = MCPServerHTTP(
|
||||
url="https://mcp.example.com/api",
|
||||
tool_filter=lambda _tool: False,
|
||||
)
|
||||
|
||||
mock_log = MagicMock()
|
||||
resolver._logger = MagicMock(log=mock_log)
|
||||
|
||||
tools, clients = resolver._resolve_native(config)
|
||||
|
||||
assert tools == []
|
||||
assert clients == []
|
||||
warning_calls = [
|
||||
call for call in mock_log.call_args_list if call.args[0] == "warning"
|
||||
]
|
||||
assert any(
|
||||
"No tools discovered from MCP server" in call.args[1]
|
||||
for call in warning_calls
|
||||
)
|
||||
|
||||
|
||||
class TestResolveNativeRuntimeError:
|
||||
@patch("crewai.mcp.tool_resolver.asyncio.run")
|
||||
def test_unmatched_runtime_error_is_wrapped_not_swallowed(
|
||||
self, mock_asyncio_run, resolver, http_config
|
||||
):
|
||||
mock_asyncio_run.side_effect = RuntimeError("some other failure")
|
||||
|
||||
with pytest.raises(RuntimeError, match="Failed to get native MCP tools"):
|
||||
resolver._resolve_native(http_config)
|
||||
@@ -3,6 +3,7 @@
|
||||
import os
|
||||
from typing import Dict, List
|
||||
|
||||
import pytest
|
||||
from crewai.flow.flow import Flow, FlowState, listen, start
|
||||
from crewai.flow.persistence import persist
|
||||
from crewai.flow.persistence.sqlite import SQLiteFlowPersistence
|
||||
@@ -248,3 +249,69 @@ def test_persistence_with_base_model(tmp_path):
|
||||
assert message.type == "text"
|
||||
assert message.content == "Hello, World!"
|
||||
assert isinstance(flow.state._unwrap(), State)
|
||||
|
||||
|
||||
def test_persist_custom_key_with_pydantic_state(tmp_path):
|
||||
"""`@persist(key=...)` uses the named attribute on a Pydantic state."""
|
||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||
persistence = SQLiteFlowPersistence(db_path)
|
||||
|
||||
class KeyedState(FlowState):
|
||||
conversation_id: str = "conv-42"
|
||||
message: str = ""
|
||||
|
||||
class KeyedFlow(Flow[KeyedState]):
|
||||
@start()
|
||||
@persist(persistence, key="conversation_id")
|
||||
def init_step(self):
|
||||
self.state.message = "hello"
|
||||
|
||||
flow = KeyedFlow(persistence=persistence)
|
||||
flow.kickoff()
|
||||
|
||||
saved_state = persistence.load_state("conv-42")
|
||||
assert saved_state is not None
|
||||
assert saved_state["message"] == "hello"
|
||||
# The default `state.id` lookup must NOT have been used as the key.
|
||||
assert persistence.load_state(flow.state.id) is None
|
||||
|
||||
|
||||
def test_persist_custom_key_with_dict_state(tmp_path):
|
||||
"""`@persist(key=...)` uses the named key on a dict state."""
|
||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||
persistence = SQLiteFlowPersistence(db_path)
|
||||
|
||||
class DictKeyedFlow(Flow[Dict[str, str]]):
|
||||
initial_state = dict()
|
||||
|
||||
@start()
|
||||
@persist(persistence, key="conversation_id")
|
||||
def init_step(self):
|
||||
self.state["conversation_id"] = "conv-dict-7"
|
||||
self.state["message"] = "hi from dict"
|
||||
|
||||
flow = DictKeyedFlow(persistence=persistence)
|
||||
flow.kickoff()
|
||||
|
||||
saved_state = persistence.load_state("conv-dict-7")
|
||||
assert saved_state is not None
|
||||
assert saved_state["message"] == "hi from dict"
|
||||
|
||||
|
||||
def test_persist_custom_key_missing_raises(tmp_path):
|
||||
"""A missing/falsy custom key must raise a clear ValueError."""
|
||||
db_path = os.path.join(tmp_path, "test_flows.db")
|
||||
persistence = SQLiteFlowPersistence(db_path)
|
||||
|
||||
class MissingKeyFlow(Flow[Dict[str, str]]):
|
||||
initial_state = dict()
|
||||
|
||||
@start()
|
||||
@persist(persistence, key="conversation_id")
|
||||
def init_step(self):
|
||||
# Intentionally do NOT set "conversation_id" on state.
|
||||
self.state["message"] = "no key here"
|
||||
|
||||
flow = MissingKeyFlow(persistence=persistence)
|
||||
with pytest.raises(ValueError, match="conversation_id"):
|
||||
flow.kickoff()
|
||||
|
||||
259
lib/crewai/tests/tracing/test_tracing_message_suppression.py
Normal file
259
lib/crewai/tests/tracing/test_tracing_message_suppression.py
Normal file
@@ -0,0 +1,259 @@
|
||||
"""Tests for tracing disabled message suppression (issue #5665).
|
||||
|
||||
Verifies that:
|
||||
- Users who explicitly declined tracing are NOT nagged with the message.
|
||||
- The CREWAI_SUPPRESS_TRACING_MESSAGES env var suppresses the message.
|
||||
- The message is shown only when tracing is disabled and user hasn't declined.
|
||||
"""
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
set_suppress_tracing_messages,
|
||||
should_suppress_tracing_messages,
|
||||
)
|
||||
|
||||
|
||||
class TestShouldSuppressTracingMessages:
|
||||
"""Tests for the should_suppress_tracing_messages utility function."""
|
||||
|
||||
def test_suppress_false_by_default(self):
|
||||
"""By default, messages should NOT be suppressed."""
|
||||
token = set_suppress_tracing_messages(False)
|
||||
try:
|
||||
assert should_suppress_tracing_messages() is False
|
||||
finally:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
_suppress_tracing_messages,
|
||||
)
|
||||
_suppress_tracing_messages.reset(token)
|
||||
|
||||
def test_suppress_via_context_var(self):
|
||||
"""Setting the context var should suppress messages."""
|
||||
token = set_suppress_tracing_messages(True)
|
||||
try:
|
||||
assert should_suppress_tracing_messages() is True
|
||||
finally:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
_suppress_tracing_messages,
|
||||
)
|
||||
_suppress_tracing_messages.reset(token)
|
||||
|
||||
@pytest.mark.parametrize("env_value", ["true", "True", "TRUE", "1", "yes", "YES"])
|
||||
def test_suppress_via_env_var(self, env_value, monkeypatch):
|
||||
"""CREWAI_SUPPRESS_TRACING_MESSAGES env var should suppress messages."""
|
||||
token = set_suppress_tracing_messages(False)
|
||||
try:
|
||||
monkeypatch.setenv("CREWAI_SUPPRESS_TRACING_MESSAGES", env_value)
|
||||
assert should_suppress_tracing_messages() is True
|
||||
finally:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
_suppress_tracing_messages,
|
||||
)
|
||||
_suppress_tracing_messages.reset(token)
|
||||
|
||||
@pytest.mark.parametrize("env_value", ["false", "False", "0", "no", ""])
|
||||
def test_no_suppress_with_falsy_env_var(self, env_value, monkeypatch):
|
||||
"""Falsy values for the env var should NOT suppress messages."""
|
||||
token = set_suppress_tracing_messages(False)
|
||||
try:
|
||||
monkeypatch.setenv("CREWAI_SUPPRESS_TRACING_MESSAGES", env_value)
|
||||
assert should_suppress_tracing_messages() is False
|
||||
finally:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
_suppress_tracing_messages,
|
||||
)
|
||||
_suppress_tracing_messages.reset(token)
|
||||
|
||||
def test_context_var_takes_precedence_over_env(self, monkeypatch):
|
||||
"""Context var set to True should suppress even if env var is false."""
|
||||
token = set_suppress_tracing_messages(True)
|
||||
try:
|
||||
monkeypatch.setenv("CREWAI_SUPPRESS_TRACING_MESSAGES", "false")
|
||||
assert should_suppress_tracing_messages() is True
|
||||
finally:
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
_suppress_tracing_messages,
|
||||
)
|
||||
_suppress_tracing_messages.reset(token)
|
||||
|
||||
|
||||
class TestShowTracingDisabledMessage:
|
||||
"""Tests that _show_tracing_disabled_message does not nag declined users."""
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={"first_execution_done": True, "trace_consent": False},
|
||||
)
|
||||
def test_crew_no_message_when_user_declined(self, mock_load):
|
||||
"""Crew._show_tracing_disabled_message should not print when user declined."""
|
||||
from crewai.crew import Crew
|
||||
|
||||
with patch("crewai.crew.Console") as MockConsole:
|
||||
Crew._show_tracing_disabled_message()
|
||||
MockConsole.return_value.print.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={"first_execution_done": True, "trace_consent": False},
|
||||
)
|
||||
def test_flow_no_message_when_user_declined(self, mock_load):
|
||||
"""Flow._show_tracing_disabled_message should not print when user declined."""
|
||||
from crewai.flow.flow import Flow
|
||||
|
||||
with patch("crewai.flow.flow.Console") as MockConsole:
|
||||
Flow._show_tracing_disabled_message()
|
||||
MockConsole.return_value.print.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={"first_execution_done": True, "trace_consent": False},
|
||||
)
|
||||
def test_trace_listener_no_message_when_user_declined(self, mock_load):
|
||||
"""TraceCollectionListener._show_tracing_disabled_message should not print when user declined."""
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
listener = TraceCollectionListener.__new__(TraceCollectionListener)
|
||||
with patch("rich.console.Console") as MockConsole:
|
||||
listener._show_tracing_disabled_message()
|
||||
MockConsole.return_value.print.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={},
|
||||
)
|
||||
def test_crew_shows_message_when_user_has_not_decided(self, mock_load):
|
||||
"""Crew._show_tracing_disabled_message should print when user hasn't decided yet."""
|
||||
from crewai.crew import Crew
|
||||
|
||||
with patch("crewai.crew.Console") as MockConsole:
|
||||
mock_console_instance = MockConsole.return_value
|
||||
Crew._show_tracing_disabled_message()
|
||||
mock_console_instance.print.assert_called_once()
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={},
|
||||
)
|
||||
def test_crew_no_message_when_suppress_env_set(self, mock_load, monkeypatch):
|
||||
"""Crew._show_tracing_disabled_message should not print when env var suppresses."""
|
||||
from crewai.crew import Crew
|
||||
|
||||
monkeypatch.setenv("CREWAI_SUPPRESS_TRACING_MESSAGES", "true")
|
||||
with patch("crewai.crew.Console") as MockConsole:
|
||||
Crew._show_tracing_disabled_message()
|
||||
MockConsole.return_value.print.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={},
|
||||
)
|
||||
def test_flow_no_message_when_suppress_env_set(self, mock_load, monkeypatch):
|
||||
"""Flow._show_tracing_disabled_message should not print when env var suppresses."""
|
||||
from crewai.flow.flow import Flow
|
||||
|
||||
monkeypatch.setenv("CREWAI_SUPPRESS_TRACING_MESSAGES", "true")
|
||||
with patch("crewai.flow.flow.Console") as MockConsole:
|
||||
Flow._show_tracing_disabled_message()
|
||||
MockConsole.return_value.print.assert_not_called()
|
||||
|
||||
|
||||
class TestConsoleFormatterTracingMessage:
|
||||
"""Tests for console_formatter._show_tracing_disabled_message_if_needed."""
|
||||
|
||||
def _make_formatter(self):
|
||||
from crewai.events.utils.console_formatter import ConsoleFormatter
|
||||
|
||||
formatter = ConsoleFormatter.__new__(ConsoleFormatter)
|
||||
formatter.console = MagicMock()
|
||||
formatter.verbose = True
|
||||
return formatter
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={"first_execution_done": True, "trace_consent": False},
|
||||
)
|
||||
def test_no_message_when_user_declined(self, mock_load):
|
||||
"""Console formatter should not show the message when user declined tracing."""
|
||||
formatter = self._make_formatter()
|
||||
|
||||
with patch(
|
||||
"crewai.events.listeners.tracing.trace_listener.TraceCollectionListener"
|
||||
) as mock_listener_cls:
|
||||
mock_listener_cls._instance = None
|
||||
formatter._show_tracing_disabled_message_if_needed()
|
||||
|
||||
formatter.console.print.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={},
|
||||
)
|
||||
def test_no_message_when_suppress_env_set(self, mock_load, monkeypatch):
|
||||
"""Console formatter should not show the message when env var is set."""
|
||||
monkeypatch.setenv("CREWAI_SUPPRESS_TRACING_MESSAGES", "true")
|
||||
formatter = self._make_formatter()
|
||||
|
||||
formatter._show_tracing_disabled_message_if_needed()
|
||||
|
||||
formatter.console.print.assert_not_called()
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={},
|
||||
)
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils.is_tracing_enabled_in_context",
|
||||
return_value=False,
|
||||
)
|
||||
def test_message_shown_when_tracing_disabled_and_not_declined(
|
||||
self, mock_tracing_ctx, mock_load
|
||||
):
|
||||
"""Console formatter should show the message when tracing disabled and user hasn't declined."""
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
formatter = self._make_formatter()
|
||||
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.first_time_handler.is_first_time = False
|
||||
original_instance = TraceCollectionListener._instance
|
||||
|
||||
try:
|
||||
TraceCollectionListener._instance = mock_instance # type: ignore[misc]
|
||||
formatter._show_tracing_disabled_message_if_needed()
|
||||
formatter.console.print.assert_called_once()
|
||||
finally:
|
||||
TraceCollectionListener._instance = original_instance # type: ignore[misc]
|
||||
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils._load_user_data",
|
||||
return_value={},
|
||||
)
|
||||
@patch(
|
||||
"crewai.events.listeners.tracing.utils.is_tracing_enabled_in_context",
|
||||
return_value=True,
|
||||
)
|
||||
def test_no_message_when_tracing_enabled(self, mock_tracing_ctx, mock_load):
|
||||
"""Console formatter should not show the message when tracing is enabled."""
|
||||
from crewai.events.listeners.tracing.trace_listener import (
|
||||
TraceCollectionListener,
|
||||
)
|
||||
|
||||
formatter = self._make_formatter()
|
||||
|
||||
mock_instance = MagicMock()
|
||||
mock_instance.first_time_handler.is_first_time = False
|
||||
original_instance = TraceCollectionListener._instance
|
||||
|
||||
try:
|
||||
TraceCollectionListener._instance = mock_instance # type: ignore[misc]
|
||||
formatter._show_tracing_disabled_message_if_needed()
|
||||
formatter.console.print.assert_not_called()
|
||||
finally:
|
||||
TraceCollectionListener._instance = original_instance # type: ignore[misc]
|
||||
@@ -940,6 +940,8 @@ def test_internal_instructor_real_unsupported_provider() -> None:
|
||||
mock_llm.is_litellm = False
|
||||
mock_llm.model = "unsupported-model"
|
||||
mock_llm.provider = "unsupported"
|
||||
mock_llm.base_url = None
|
||||
mock_llm.api_key = None
|
||||
|
||||
# This should raise a ConfigurationError from the real instructor library
|
||||
with pytest.raises(Exception) as exc_info:
|
||||
@@ -952,3 +954,45 @@ def test_internal_instructor_real_unsupported_provider() -> None:
|
||||
|
||||
# Verify it's a configuration error about unsupported provider
|
||||
assert "Unsupported provider" in str(exc_info.value) or "unsupported" in str(exc_info.value).lower()
|
||||
|
||||
|
||||
def test_internal_instructor_forwards_base_url_and_api_key() -> None:
|
||||
"""base_url and api_key on the LLM must flow into instructor.from_provider."""
|
||||
from crewai.utilities.internal_instructor import InternalInstructor
|
||||
|
||||
mock_llm = Mock()
|
||||
mock_llm.is_litellm = False
|
||||
mock_llm.model = "gpt-4o"
|
||||
mock_llm.provider = "openai"
|
||||
mock_llm.base_url = "https://custom.example.com/v1"
|
||||
mock_llm.api_key = "sk-custom"
|
||||
|
||||
with patch("instructor.from_provider") as mock_from_provider:
|
||||
mock_from_provider.return_value = Mock()
|
||||
|
||||
InternalInstructor(content="x", model=SimpleModel, llm=mock_llm)
|
||||
|
||||
mock_from_provider.assert_called_once_with(
|
||||
"openai/gpt-4o",
|
||||
base_url="https://custom.example.com/v1",
|
||||
api_key="sk-custom",
|
||||
)
|
||||
|
||||
|
||||
def test_internal_instructor_omits_unset_base_url_and_api_key() -> None:
|
||||
"""When base_url/api_key are None, they must not be passed to from_provider."""
|
||||
from crewai.utilities.internal_instructor import InternalInstructor
|
||||
|
||||
mock_llm = Mock()
|
||||
mock_llm.is_litellm = False
|
||||
mock_llm.model = "gpt-4o"
|
||||
mock_llm.provider = "openai"
|
||||
mock_llm.base_url = None
|
||||
mock_llm.api_key = None
|
||||
|
||||
with patch("instructor.from_provider") as mock_from_provider:
|
||||
mock_from_provider.return_value = Mock()
|
||||
|
||||
InternalInstructor(content="x", model=SimpleModel, llm=mock_llm)
|
||||
|
||||
mock_from_provider.assert_called_once_with("openai/gpt-4o")
|
||||
|
||||
Reference in New Issue
Block a user