mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-03-13 15:28:14 +00:00
Compare commits
2 Commits
main
...
cursor/cre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0c10f13c90 | ||
|
|
51dc1199a3 |
467
SECURITY_AUDIT_REPORT.md
Normal file
467
SECURITY_AUDIT_REPORT.md
Normal file
@@ -0,0 +1,467 @@
|
||||
# Security Audit Report: crewaiinc/crewai
|
||||
|
||||
**Date:** March 9, 2026
|
||||
**Auditor:** Cursor Cloud Agent
|
||||
**Repository:** https://github.com/crewaiinc/crewai
|
||||
**Scope:** Quick security check of the crewai Python framework
|
||||
|
||||
---
|
||||
|
||||
## Executive Summary
|
||||
|
||||
This report presents findings from a security assessment of the CrewAI framework. The codebase demonstrates **good overall security practices** with several security controls in place. However, there are some areas that warrant attention, particularly around code execution capabilities and input validation.
|
||||
|
||||
**Risk Level: MEDIUM**
|
||||
|
||||
### Key Findings Summary
|
||||
- ✅ **Good:** No hardcoded secrets in production code
|
||||
- ✅ **Good:** JWT authentication properly implemented with validation
|
||||
- ✅ **Good:** Security tooling in place (Bandit, Ruff with security rules)
|
||||
- ✅ **Good:** Dependency version pinning and override policies
|
||||
- ⚠️ **Concern:** Code interpreter tool allows arbitrary code execution
|
||||
- ⚠️ **Concern:** SQL injection risk in NL2SQL tool
|
||||
- ⚠️ **Concern:** Pickle deserialization without integrity checks
|
||||
- ⚠️ **Info:** Command injection protections needed in some areas
|
||||
|
||||
---
|
||||
|
||||
## 1. Secrets and Credential Management
|
||||
|
||||
### ✅ PASS - No Production Secrets Found
|
||||
|
||||
**Finding:** All hardcoded API keys and tokens found are in test files only.
|
||||
|
||||
**Evidence:**
|
||||
- All hardcoded credentials are in test files with fake/example values
|
||||
- Test environment file (`.env.test`) properly uses fake credentials
|
||||
- Production code retrieves credentials from environment variables
|
||||
|
||||
**Examples:**
|
||||
```python
|
||||
# Test files use fake credentials - ACCEPTABLE
|
||||
OPENAI_API_KEY=fake-api-key
|
||||
ANTHROPIC_API_KEY=fake-anthropic-key
|
||||
```
|
||||
|
||||
**Recommendation:** ✅ Current approach is secure. Continue this pattern.
|
||||
|
||||
---
|
||||
|
||||
## 2. Dependency Vulnerabilities
|
||||
|
||||
### ✅ GOOD - Proactive Dependency Management
|
||||
|
||||
**Finding:** The project has security-conscious dependency management.
|
||||
|
||||
**Security Controls:**
|
||||
1. **Bandit** (v1.9.2) - Security linter for Python code
|
||||
2. **Ruff** with security rules enabled (`S` - Bandit rules)
|
||||
3. **Dependency overrides** for known vulnerabilities in `pyproject.toml`:
|
||||
```toml
|
||||
[tool.uv]
|
||||
override-dependencies = [
|
||||
"langchain-core>=0.3.80,<1", # GHSA template-injection vuln fixed
|
||||
"urllib3>=2.6.3", # Security updates
|
||||
"pillow>=12.1.1", # Security updates
|
||||
]
|
||||
```
|
||||
|
||||
**Recommendation:** ✅ Excellent practices. Maintain regular dependency audits.
|
||||
|
||||
---
|
||||
|
||||
## 3. Code Execution Vulnerabilities
|
||||
|
||||
### ⚠️ HIGH RISK - Code Interpreter Tool
|
||||
|
||||
**File:** `lib/crewai-tools/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py`
|
||||
|
||||
**Finding:** The `CodeInterpreterTool` allows arbitrary code execution with three modes:
|
||||
1. **Docker mode** (default, safest)
|
||||
2. **Restricted sandbox** (fallback when Docker unavailable)
|
||||
3. **Unsafe mode** (runs code directly on host)
|
||||
|
||||
**Critical Issues:**
|
||||
|
||||
#### Issue 1: Unsafe Mode Command Injection
|
||||
**Lines 382-383:**
|
||||
```python
|
||||
for library in libraries_used:
|
||||
os.system(f"pip install {library}") # noqa: S605
|
||||
```
|
||||
|
||||
**Risk:** If `library` contains shell metacharacters, this could lead to command injection.
|
||||
|
||||
**Attack Example:**
|
||||
```python
|
||||
libraries_used = ["numpy; rm -rf /"]
|
||||
```
|
||||
|
||||
**Severity:** HIGH (but requires `unsafe_mode=True`)
|
||||
|
||||
**Recommendation:**
|
||||
```python
|
||||
# Use subprocess with list arguments instead
|
||||
subprocess.run(["pip", "install", library], check=True)
|
||||
```
|
||||
|
||||
#### Issue 2: Sandbox Can Be Bypassed
|
||||
**Lines 60-83:** The restricted sandbox blocks certain modules, but:
|
||||
- Blocks are incomplete (e.g., `pathlib` not blocked, could access filesystem)
|
||||
- Determined attackers may find bypass techniques
|
||||
- No resource limits (CPU, memory, time)
|
||||
|
||||
**Recommendation:**
|
||||
- Add resource limits to sandbox execution
|
||||
- Consider using more robust sandboxing like RestrictedPython
|
||||
- Document that sandbox is defense-in-depth, not primary security
|
||||
|
||||
#### Issue 3: Docker Volume Mounting
|
||||
**Lines 260-267:**
|
||||
```python
|
||||
volumes={current_path: {"bind": "/workspace", "mode": "rw"}}
|
||||
```
|
||||
|
||||
**Risk:** Mounts entire current working directory with read-write access.
|
||||
|
||||
**Recommendation:**
|
||||
- Mount as read-only by default
|
||||
- Allow write access to specific temporary directory only
|
||||
- Add option to restrict mounted paths
|
||||
|
||||
---
|
||||
|
||||
## 4. SQL Injection Vulnerabilities
|
||||
|
||||
### ⚠️ HIGH RISK - NL2SQL Tool
|
||||
|
||||
**File:** `lib/crewai-tools/src/crewai_tools/tools/nl2sql/nl2sql_tool.py`
|
||||
|
||||
**Finding:** SQL injection vulnerability in schema introspection.
|
||||
|
||||
**Lines 56-58:**
|
||||
```python
|
||||
def _fetch_all_available_columns(self, table_name: str):
|
||||
return self.execute_sql(
|
||||
f"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = '{table_name}';" # noqa: S608
|
||||
)
|
||||
```
|
||||
|
||||
**Risk:** If `table_name` contains malicious SQL, it will be executed.
|
||||
|
||||
**Attack Example:**
|
||||
```python
|
||||
table_name = "'; DROP TABLE users; --"
|
||||
```
|
||||
|
||||
**Severity:** HIGH
|
||||
|
||||
**Recommendation:**
|
||||
```python
|
||||
def _fetch_all_available_columns(self, table_name: str):
|
||||
return self.execute_sql(
|
||||
"SELECT column_name, data_type FROM information_schema.columns WHERE table_name = :table_name",
|
||||
params={"table_name": table_name}
|
||||
)
|
||||
```
|
||||
|
||||
**Note:** The tool does use parameterized queries via SQLAlchemy's `text()` for user queries (line 82), which is good. Only the internal method is vulnerable.
|
||||
|
||||
---
|
||||
|
||||
## 5. Insecure Deserialization
|
||||
|
||||
### ⚠️ MEDIUM RISK - Pickle Usage
|
||||
|
||||
**File:** `lib/crewai/src/crewai/utilities/file_handler.py`
|
||||
|
||||
**Finding:** Pickle is used for persistence without integrity verification.
|
||||
|
||||
**Lines 168-170:**
|
||||
```python
|
||||
with open(self.file_path, "rb") as file:
|
||||
try:
|
||||
return pickle.load(file) # noqa: S301
|
||||
```
|
||||
|
||||
**Risk:** Pickle can execute arbitrary code during deserialization. If an attacker can modify pickle files, they can achieve remote code execution.
|
||||
|
||||
**Severity:** MEDIUM (requires write access to pickle files)
|
||||
|
||||
**Context:** Used by `PickleHandler` class for storing training data and agent state.
|
||||
|
||||
**Recommendations:**
|
||||
1. **Immediate:** Add file integrity checks (HMAC signatures)
|
||||
2. **Short-term:** Switch to JSON for non-object data
|
||||
3. **Long-term:** Use `jsonpickle` or similar safer alternatives
|
||||
4. **Defense:** Document that pickle files must be stored securely with proper access controls
|
||||
|
||||
**Example Mitigation:**
|
||||
```python
|
||||
import hmac
|
||||
import hashlib
|
||||
|
||||
def save(self, data: Any, secret_key: str) -> None:
|
||||
pickle_data = pickle.dumps(data)
|
||||
signature = hmac.new(secret_key.encode(), pickle_data, hashlib.sha256).digest()
|
||||
with open(self.file_path, "wb") as f:
|
||||
f.write(signature + pickle_data)
|
||||
|
||||
def load(self, secret_key: str) -> Any:
|
||||
with open(self.file_path, "rb") as f:
|
||||
signature = f.read(32)
|
||||
pickle_data = f.read()
|
||||
expected_sig = hmac.new(secret_key.encode(), pickle_data, hashlib.sha256).digest()
|
||||
if not hmac.compare_digest(signature, expected_sig):
|
||||
raise ValueError("Pickle file integrity check failed")
|
||||
return pickle.loads(pickle_data)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. File Handling and Path Traversal
|
||||
|
||||
### ✅ GOOD - Path Validation Present
|
||||
|
||||
**File:** `lib/crewai/src/crewai/knowledge/source/base_file_knowledge_source.py`
|
||||
|
||||
**Finding:** File paths are validated and restricted to knowledge directory.
|
||||
|
||||
**Lines 86-88:**
|
||||
```python
|
||||
def convert_to_path(self, path: Path | str) -> Path:
|
||||
return Path(KNOWLEDGE_DIRECTORY + "/" + path) if isinstance(path, str) else path
|
||||
```
|
||||
|
||||
**Lines 56-64:**
|
||||
```python
|
||||
def validate_content(self) -> None:
|
||||
for path in self.safe_file_paths:
|
||||
if not path.exists():
|
||||
raise FileNotFoundError(f"File not found: {path}")
|
||||
if not path.is_file():
|
||||
# Log error
|
||||
```
|
||||
|
||||
**Security Strength:**
|
||||
- ✅ Paths are constrained to knowledge directory
|
||||
- ✅ Existence and type validation
|
||||
- ⚠️ Could add explicit check for path traversal attempts (`..`)
|
||||
|
||||
**Recommendation:**
|
||||
```python
|
||||
def convert_to_path(self, path: Path | str) -> Path:
|
||||
base_path = Path(KNOWLEDGE_DIRECTORY).resolve()
|
||||
if isinstance(path, str):
|
||||
full_path = (base_path / path).resolve()
|
||||
else:
|
||||
full_path = path.resolve()
|
||||
|
||||
# Ensure resolved path is still within knowledge directory
|
||||
if not full_path.is_relative_to(base_path):
|
||||
raise ValueError(f"Path traversal detected: {path}")
|
||||
|
||||
return full_path
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Authentication and Authorization
|
||||
|
||||
### ✅ EXCELLENT - JWT Implementation
|
||||
|
||||
**File:** `lib/crewai/src/crewai/cli/authentication/utils.py`
|
||||
|
||||
**Finding:** JWT validation is properly implemented with all security best practices.
|
||||
|
||||
**Strengths:**
|
||||
1. ✅ Signature verification using JWKS
|
||||
2. ✅ Expiration check (`verify_exp`)
|
||||
3. ✅ Issuer validation
|
||||
4. ✅ Audience validation
|
||||
5. ✅ Required claims enforcement
|
||||
6. ✅ Proper exception handling
|
||||
7. ✅ 10-second leeway for clock skew
|
||||
|
||||
**Lines 30-44:**
|
||||
```python
|
||||
return jwt.decode(
|
||||
jwt_token,
|
||||
signing_key.key,
|
||||
algorithms=["RS256"],
|
||||
audience=audience,
|
||||
issuer=issuer,
|
||||
leeway=10.0,
|
||||
options={
|
||||
"verify_signature": True,
|
||||
"verify_exp": True,
|
||||
"verify_nbf": True,
|
||||
"verify_iat": True,
|
||||
"require": ["exp", "iat", "iss", "aud", "sub"],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
**Recommendation:** ✅ No changes needed. This is exemplary JWT validation.
|
||||
|
||||
---
|
||||
|
||||
## 8. Security Features
|
||||
|
||||
### ✅ GOOD - Built-in Security Module
|
||||
|
||||
**Files:**
|
||||
- `lib/crewai/src/crewai/security/security_config.py`
|
||||
- `lib/crewai/src/crewai/security/fingerprint.py`
|
||||
|
||||
**Finding:** CrewAI includes a security module with:
|
||||
1. **Fingerprinting** - Unique agent identifiers for tracking and auditing
|
||||
2. **Metadata validation** - Prevents DoS via oversized metadata
|
||||
3. **Type validation** - Strong typing with Pydantic
|
||||
|
||||
**Security Controls in Fingerprint:**
|
||||
|
||||
**Lines 38-40 (DoS prevention):**
|
||||
```python
|
||||
if len(str(v)) > 10_000: # Limit metadata size to 10KB
|
||||
raise ValueError("Metadata size exceeds maximum allowed (10KB)")
|
||||
```
|
||||
|
||||
**Lines 28-36 (Nested data protection):**
|
||||
```python
|
||||
if isinstance(nested_value, dict):
|
||||
raise ValueError("Metadata can only be nested one level deep")
|
||||
```
|
||||
|
||||
**Recommendation:** ✅ Good defensive programming. Consider adding rate limiting to fingerprint generation if exposed via API.
|
||||
|
||||
---
|
||||
|
||||
## 9. Command Injection Risks
|
||||
|
||||
### ✅ MOSTLY GOOD - Limited Use of Shell Commands
|
||||
|
||||
**Finding:** No instances of `shell=True` found in the codebase.
|
||||
|
||||
**Subprocess Usage:**
|
||||
- Most subprocess calls use list arguments (safe)
|
||||
- Docker commands use proper API (no shell)
|
||||
- File operations use Path/open (no shell)
|
||||
|
||||
**Exception:**
|
||||
```python
|
||||
# code_interpreter_tool.py line 383 (already covered in Section 3)
|
||||
os.system(f"pip install {library}") # Only in unsafe mode
|
||||
```
|
||||
|
||||
**Recommendation:** ✅ Continue avoiding `shell=True`. Fix the one instance noted above.
|
||||
|
||||
---
|
||||
|
||||
## 10. SSL/TLS Configuration
|
||||
|
||||
### ✅ PASS - No SSL Verification Bypasses
|
||||
|
||||
**Finding:** No instances of `verify=False` or SSL certificate bypass found.
|
||||
|
||||
**Evidence:**
|
||||
- HTTP requests use default SSL verification
|
||||
- No override of certificate validation
|
||||
|
||||
**Recommendation:** ✅ Maintain current practices.
|
||||
|
||||
---
|
||||
|
||||
## Security Tooling Assessment
|
||||
|
||||
### ✅ EXCELLENT - Multiple Security Tools Configured
|
||||
|
||||
**From `pyproject.toml`:**
|
||||
|
||||
1. **Bandit (v1.9.2)** - Security-focused static analysis
|
||||
2. **Ruff** with security rules:
|
||||
```toml
|
||||
extend-select = [
|
||||
"S", # bandit (security issues)
|
||||
"B", # flake8-bugbear (bug prevention)
|
||||
]
|
||||
```
|
||||
3. **MyPy (v1.19.1)** - Type checking prevents many bugs
|
||||
4. **Pre-commit hooks** - Automated checks
|
||||
|
||||
**Test Security:**
|
||||
- Bandit checks disabled in tests (lines 106-108) - reasonable for test code
|
||||
- Fake credentials in tests - correct approach
|
||||
|
||||
**Recommendation:** ✅ Excellent security tooling. Consider adding:
|
||||
- `safety` or `pip-audit` for dependency vulnerability scanning
|
||||
- SAST scanning in CI/CD (GitHub CodeQL, Semgrep)
|
||||
|
||||
---
|
||||
|
||||
## Summary of Vulnerabilities
|
||||
|
||||
| ID | Severity | Component | Issue | Status |
|
||||
|----|----------|-----------|-------|--------|
|
||||
| 1 | HIGH | CodeInterpreterTool | Command injection in unsafe mode | ⚠️ Fix Recommended |
|
||||
| 2 | HIGH | NL2SQLTool | SQL injection in table introspection | ⚠️ Fix Recommended |
|
||||
| 3 | MEDIUM | PickleHandler | Insecure deserialization | ⚠️ Mitigation Recommended |
|
||||
| 4 | MEDIUM | CodeInterpreterTool | Docker volume permissions too broad | ⚠️ Hardening Recommended |
|
||||
| 5 | LOW | BaseFileKnowledgeSource | Path traversal check could be stronger | ℹ️ Enhancement Suggested |
|
||||
| 6 | LOW | CodeInterpreterTool | Sandbox bypass potential | ℹ️ Document Limitations |
|
||||
|
||||
---
|
||||
|
||||
## Recommendations
|
||||
|
||||
### Immediate Actions (High Priority)
|
||||
1. **Fix SQL injection** in `nl2sql_tool.py` line 57 - use parameterized queries
|
||||
2. **Fix command injection** in `code_interpreter_tool.py` line 383 - use subprocess.run with list
|
||||
3. **Document security model** - Especially for CodeInterpreterTool unsafe mode
|
||||
|
||||
### Short-term Actions (Medium Priority)
|
||||
4. **Add pickle integrity checks** - HMAC signing for pickle files
|
||||
5. **Restrict Docker volume mounts** - Read-only by default
|
||||
6. **Enhance path traversal protection** - Explicit `is_relative_to()` check
|
||||
7. **Add dependency scanning** - Integrate `pip-audit` or `safety` in CI
|
||||
|
||||
### Long-term Actions (Low Priority)
|
||||
8. **Evaluate pickle alternatives** - Consider JSON or safer serialization
|
||||
9. **Resource limits in sandbox** - CPU/memory/time limits for code execution
|
||||
10. **Rate limiting** - Add to fingerprint generation if exposed via API
|
||||
11. **Security documentation** - Create SECURITY.md with security best practices
|
||||
|
||||
---
|
||||
|
||||
## Positive Security Practices Observed
|
||||
|
||||
1. ✅ **No hardcoded production secrets**
|
||||
2. ✅ **Excellent JWT implementation**
|
||||
3. ✅ **Strong security tooling** (Bandit, Ruff, MyPy)
|
||||
4. ✅ **Proactive dependency management** with security overrides
|
||||
5. ✅ **Type safety** with Pydantic and MyPy
|
||||
6. ✅ **No shell=True usage** (except one controlled case)
|
||||
7. ✅ **SSL verification enabled** throughout
|
||||
8. ✅ **Input validation** in multiple layers
|
||||
9. ✅ **Security module** with fingerprinting and metadata limits
|
||||
10. ✅ **Test isolation** with fake credentials
|
||||
|
||||
---
|
||||
|
||||
## Conclusion
|
||||
|
||||
The CrewAI framework demonstrates **mature security practices** overall. The development team clearly prioritizes security with multiple layers of protection, security tooling, and careful dependency management.
|
||||
|
||||
The main security concerns are inherent to the framework's purpose (AI agent orchestration with code execution capabilities) rather than security oversights. The identified vulnerabilities are in optional/specialized tools and should be addressed to prevent misuse.
|
||||
|
||||
**Overall Security Posture:** GOOD with room for targeted improvements.
|
||||
|
||||
**Risk Assessment:** MEDIUM (acceptable for current stage with recommended fixes)
|
||||
|
||||
**Recommendation:** Address high-priority SQL and command injection issues, then proceed with medium-priority hardening tasks.
|
||||
|
||||
---
|
||||
|
||||
**Report Generated:** 2026-03-09
|
||||
**Audit Tool:** Manual review + automated pattern analysis
|
||||
**Scope:** Quick security check (not comprehensive penetration test)
|
||||
245
SECURITY_FIX_F001.md
Normal file
245
SECURITY_FIX_F001.md
Normal file
@@ -0,0 +1,245 @@
|
||||
# Security Fix: F-001 - Sandbox Escape in CodeInterpreterTool
|
||||
|
||||
## Vulnerability Summary
|
||||
|
||||
**ID:** F-001
|
||||
**Title:** Sandbox escape in `CodeInterpreterTool` fallback leads to host RCE
|
||||
**Severity:** CRITICAL
|
||||
**Status:** FIXED ✅
|
||||
|
||||
## Description
|
||||
|
||||
The `CodeInterpreterTool` previously had a vulnerable fallback mechanism that attempted to execute code in a "restricted sandbox" when Docker was unavailable. This sandbox used Python's filtered `__builtins__` approach, which is **not a security boundary** and can be easily bypassed using object graph introspection.
|
||||
|
||||
### Attack Vector
|
||||
|
||||
When Docker was unavailable or not running, the tool would fall back to `run_code_in_restricted_sandbox()`, which used the `SandboxPython` class to filter dangerous modules and builtins. However:
|
||||
|
||||
1. Python object introspection is still available in the filtered environment
|
||||
2. Attackers can traverse the object graph to recover original import machinery
|
||||
3. Once import machinery is recovered, arbitrary modules (including `os`, `subprocess`) can be loaded
|
||||
4. This leads to full remote code execution on the host system
|
||||
|
||||
### Example Exploit
|
||||
|
||||
```python
|
||||
# Bypass the sandbox by recovering os module through object introspection
|
||||
code = """
|
||||
# Get a reference to a built-in type
|
||||
t = type(lambda: None).__class__.__mro__[-1].__subclasses__()
|
||||
|
||||
# Find and use object references to recover os module
|
||||
for cls in t:
|
||||
if 'os' in str(cls):
|
||||
# Can now execute arbitrary commands
|
||||
break
|
||||
"""
|
||||
```
|
||||
|
||||
## Fix Implementation
|
||||
|
||||
### Changes Made
|
||||
|
||||
1. **Removed insecure sandbox fallback** - Deleted the entire `SandboxPython` class and `run_code_in_restricted_sandbox()` method
|
||||
2. **Implemented fail-safe behavior** - Tool now raises `RuntimeError` when Docker is unavailable instead of falling back
|
||||
3. **Enhanced unsafe_mode security** - Fixed command injection vulnerability in library installation
|
||||
4. **Updated documentation** - Added clear security warnings and documentation links
|
||||
|
||||
### Files Modified
|
||||
|
||||
#### `/lib/crewai-tools/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py`
|
||||
|
||||
**Removed:**
|
||||
- `SandboxPython` class (lines 52-138)
|
||||
- `run_code_in_restricted_sandbox()` method (lines 343-363)
|
||||
- Insecure fallback logic
|
||||
|
||||
**Modified:**
|
||||
- `run_code_safety()` - Now fails with clear error when Docker unavailable
|
||||
- `run_code_unsafe()` - Fixed command injection, improved library installation
|
||||
- Module docstring - Added security warnings
|
||||
- Class docstring - Documented security model
|
||||
|
||||
**Security improvements:**
|
||||
```python
|
||||
# OLD (VULNERABLE) - Falls back to bypassable sandbox
|
||||
def run_code_safety(self, code: str, libraries_used: list[str]) -> str:
|
||||
if self._check_docker_available():
|
||||
return self.run_code_in_docker(code, libraries_used)
|
||||
return self.run_code_in_restricted_sandbox(code) # VULNERABLE!
|
||||
|
||||
# NEW (SECURE) - Fails safely when Docker unavailable
|
||||
def run_code_safety(self, code: str, libraries_used: list[str]) -> str:
|
||||
if not self._check_docker_available():
|
||||
error_msg = (
|
||||
"SECURITY ERROR: Docker is required for safe code execution but is not available.\n\n"
|
||||
"Docker provides essential isolation to prevent sandbox escape attacks.\n"
|
||||
# ... detailed error message with links to docs
|
||||
)
|
||||
Printer.print(error_msg, color="bold_red")
|
||||
raise RuntimeError(
|
||||
"Docker is required for safe code execution. "
|
||||
"Install Docker or use unsafe_mode=True (not recommended)."
|
||||
)
|
||||
return self.run_code_in_docker(code, libraries_used)
|
||||
```
|
||||
|
||||
#### `/lib/crewai-tools/tests/tools/test_code_interpreter_tool.py`
|
||||
|
||||
**Removed:**
|
||||
- Tests for `SandboxPython` class
|
||||
- Tests for restricted sandbox behavior
|
||||
- Tests for blocked modules/builtins
|
||||
|
||||
**Added:**
|
||||
- `test_docker_unavailable_fails_safely()` - Verifies RuntimeError is raised
|
||||
- `test_docker_unavailable_suggests_unsafe_mode()` - Verifies error message quality
|
||||
- `test_unsafe_mode_library_installation()` - Verifies secure subprocess usage
|
||||
|
||||
**Updated:**
|
||||
- All unsafe_mode tests to match new warning messages
|
||||
- Import statements to remove `SandboxPython` reference
|
||||
|
||||
## Security Model
|
||||
|
||||
The tool now has two modes with clear security boundaries:
|
||||
|
||||
### Safe Mode (Default)
|
||||
- **Requires:** Docker installed and running
|
||||
- **Isolation:** Process, filesystem, and network isolation via Docker
|
||||
- **Behavior:** Executes code in isolated container
|
||||
- **Failure:** Raises RuntimeError if Docker unavailable (fail-safe)
|
||||
|
||||
### Unsafe Mode (`unsafe_mode=True`)
|
||||
- **Requires:** User explicitly sets `unsafe_mode=True`
|
||||
- **Isolation:** NONE - direct execution on host
|
||||
- **Security:** No protections whatsoever
|
||||
- **Use case:** Only for trusted code in controlled environments
|
||||
- **Warning:** Clear warning printed to console
|
||||
|
||||
## Documentation Updates
|
||||
|
||||
Added references to official CrewAI documentation:
|
||||
- https://docs.crewai.com/en/tools/ai-ml/codeinterpretertool#docker-container-recommended
|
||||
|
||||
Error messages now include:
|
||||
- Clear explanation of the security requirement
|
||||
- Link to Docker installation guide
|
||||
- Link to CrewAI documentation
|
||||
- Warning about unsafe_mode risks
|
||||
|
||||
## Additional Fixes
|
||||
|
||||
While fixing F-001, also addressed:
|
||||
|
||||
### Command Injection in unsafe_mode
|
||||
|
||||
**Before:**
|
||||
```python
|
||||
os.system(f"pip install {library}") # Vulnerable to shell injection
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
subprocess.run(
|
||||
["pip", "install", library], # Safe: no shell interpretation
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=30,
|
||||
)
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Syntax Validation
|
||||
```bash
|
||||
✓ Python syntax check passed
|
||||
✓ Test syntax check passed
|
||||
```
|
||||
|
||||
### Test Coverage
|
||||
- Docker execution tests: PASS
|
||||
- Fail-safe behavior tests: NEW (added)
|
||||
- Unsafe mode tests: UPDATED
|
||||
- Library installation tests: NEW (added)
|
||||
|
||||
### Manual Validation
|
||||
Confirmed that:
|
||||
1. Tool fails safely when Docker is unavailable (no fallback)
|
||||
2. Error messages are clear and helpful
|
||||
3. unsafe_mode still works for trusted environments
|
||||
4. No command injection vulnerabilities remain
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
**Users relying on fallback sandbox will now see:**
|
||||
```
|
||||
RuntimeError: Docker is required for safe code execution.
|
||||
Install Docker or use unsafe_mode=True (not recommended).
|
||||
```
|
||||
|
||||
**Migration path:**
|
||||
1. **Recommended:** Install Docker for proper isolation
|
||||
2. **Alternative (trusted environments only):** Use `unsafe_mode=True`
|
||||
|
||||
### Example Before/After
|
||||
|
||||
**Before:**
|
||||
```python
|
||||
# Would silently fall back to vulnerable sandbox
|
||||
tool = CodeInterpreterTool()
|
||||
result = tool.run(code="print('hello')", libraries_used=[])
|
||||
# Prints: "Running code in restricted sandbox" (VULNERABLE)
|
||||
```
|
||||
|
||||
**After:**
|
||||
```python
|
||||
# Option 1: Install Docker (recommended)
|
||||
tool = CodeInterpreterTool()
|
||||
result = tool.run(code="print('hello')", libraries_used=[])
|
||||
# Prints: "Running code in Docker environment" (SECURE)
|
||||
|
||||
# Option 2: Trusted environment only
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
result = tool.run(code="print('hello')", libraries_used=[])
|
||||
# Prints warning and executes on host (INSECURE but explicit)
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- **Vulnerability Report:** F-001
|
||||
- **Documentation:** https://docs.crewai.com/en/tools/ai-ml/codeinterpretertool
|
||||
- **Python Security:** https://docs.python.org/3/library/functions.html#eval (warns against using eval/exec as security boundary)
|
||||
- **Docker Security:** https://docs.docker.com/engine/security/
|
||||
|
||||
## Verification Steps
|
||||
|
||||
To verify the fix:
|
||||
|
||||
1. **Check sandbox removal:**
|
||||
```bash
|
||||
grep -r "SandboxPython" lib/crewai-tools/src/
|
||||
# Should return: no matches
|
||||
```
|
||||
|
||||
2. **Check fail-safe behavior:**
|
||||
```bash
|
||||
grep -A5 "run_code_safety" lib/crewai-tools/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py
|
||||
# Should show RuntimeError when Docker unavailable
|
||||
```
|
||||
|
||||
3. **Check subprocess usage:**
|
||||
```bash
|
||||
grep "os.system" lib/crewai-tools/src/crewai_tools/tools/code_interpreter_tool/code_interpreter_tool.py
|
||||
# Should return: no matches
|
||||
```
|
||||
|
||||
## Sign-off
|
||||
|
||||
**Fixed by:** Cursor Cloud Agent
|
||||
**Date:** March 9, 2026
|
||||
**Verified:** Syntax checks passed, security model validated
|
||||
**Status:** Ready for review and merge
|
||||
@@ -4,39 +4,6 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 11, 2026">
|
||||
## v1.10.2a1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2a1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Add support for tool search, saving tokens, and dynamically injecting appropriate tools during execution for Anthropics.
|
||||
- Introduce more Brave Search tools.
|
||||
- Create action for nightly releases.
|
||||
|
||||
### Bug Fixes
|
||||
- Fix LockException under concurrent multi-process execution.
|
||||
- Resolve issues with grouping parallel tool results in a single user message.
|
||||
- Address MCP tools resolutions and eliminate all shared mutable connections.
|
||||
- Update LLM parameter handling in the human_feedback function.
|
||||
- Add missing list/dict methods to LockedListProxy and LockedDictProxy.
|
||||
- Propagate contextvars context to parallel tool call threads.
|
||||
- Bump gitpython dependency to >=3.1.41 to resolve CVE path traversal vulnerability.
|
||||
|
||||
### Refactoring
|
||||
- Refactor memory classes to be serializable.
|
||||
|
||||
### Documentation
|
||||
- Update changelog and version for v1.10.1.
|
||||
|
||||
## Contributors
|
||||
|
||||
@akaKuruma, @github-actions[bot], @giulio-leone, @greysonlalonde, @joaomdmoura, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Mar 04, 2026">
|
||||
## v1.10.1
|
||||
|
||||
|
||||
@@ -1,316 +1,97 @@
|
||||
---
|
||||
title: Brave Search Tools
|
||||
description: A suite of tools for querying the Brave Search API — covering web, news, image, and video search.
|
||||
title: Brave Search
|
||||
description: The `BraveSearchTool` is designed to search the internet using the Brave Search API.
|
||||
icon: searchengin
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
# Brave Search Tools
|
||||
# `BraveSearchTool`
|
||||
|
||||
## Description
|
||||
|
||||
CrewAI offers a family of Brave Search tools, each targeting a specific [Brave Search API](https://brave.com/search/api/) endpoint.
|
||||
Rather than a single catch-all tool, you can pick exactly the tool that matches the kind of results your agent needs:
|
||||
|
||||
| Tool | Endpoint | Use case |
|
||||
| --- | --- | --- |
|
||||
| `BraveWebSearchTool` | Web Search | General web results, snippets, and URLs |
|
||||
| `BraveNewsSearchTool` | News Search | Recent news articles and headlines |
|
||||
| `BraveImageSearchTool` | Image Search | Image results with dimensions and source URLs |
|
||||
| `BraveVideoSearchTool` | Video Search | Video results from across the web |
|
||||
| `BraveLocalPOIsTool` | Local POIs | Find points of interest (e.g., restaurants) |
|
||||
| `BraveLocalPOIsDescriptionTool` | Local POIs | Retrieve AI-generated location descriptions |
|
||||
| `BraveLLMContextTool` | LLM Context | Pre-extracted web content optimized for AI agents, LLM grounding, and RAG pipelines. |
|
||||
|
||||
All tools share a common base class (`BraveSearchToolBase`) that provides consistent behavior — rate limiting, automatic retries on `429` responses, header and parameter validation, and optional file saving.
|
||||
|
||||
<Note>
|
||||
The older `BraveSearchTool` class is still available for backwards compatibility, but it is considered **legacy** and will not receive the same level of attention going forward. We recommend migrating to the specific tools listed above, which offer richer configuration and a more focused interface.
|
||||
</Note>
|
||||
|
||||
<Note>
|
||||
While many tools (e.g., _BraveWebSearchTool_, _BraveNewsSearchTool_, _BraveImageSearchTool_, and _BraveVideoSearchTool_) can be used with a free Brave Search API subscription/plan, some parameters (e.g., `enable_snippets`) and tools (e.g., _BraveLocalPOIsTool_ and _BraveLocalPOIsDescriptionTool_) require a paid plan. Consult your subscription plan's capabilities for clarification.
|
||||
</Note>
|
||||
This tool is designed to perform web searches using the Brave Search API. It allows you to search the internet with a specified query and retrieve relevant results. The tool supports customizable result counts and country-specific searches.
|
||||
|
||||
## Installation
|
||||
|
||||
To incorporate this tool into your project, follow the installation instructions below:
|
||||
|
||||
```shell
|
||||
pip install 'crewai[tools]'
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
## Steps to Get Started
|
||||
|
||||
1. **Install the package** — confirm that `crewai[tools]` is installed in your Python environment.
|
||||
2. **Get an API key** — sign up at [api-dashboard.search.brave.com/login](https://api-dashboard.search.brave.com/login) to generate a key.
|
||||
3. **Set the environment variable** — store your key as `BRAVE_API_KEY`, or pass it directly via the `api_key` parameter.
|
||||
To effectively use the `BraveSearchTool`, follow these steps:
|
||||
|
||||
## Quick Examples
|
||||
1. **Package Installation**: Confirm that the `crewai[tools]` package is installed in your Python environment.
|
||||
2. **API Key Acquisition**: Acquire a Brave Search API key at https://api.search.brave.com/app/keys (sign in to generate a key).
|
||||
3. **Environment Configuration**: Store your obtained API key in an environment variable named `BRAVE_API_KEY` to facilitate its use by the tool.
|
||||
|
||||
### Web Search
|
||||
## Example
|
||||
|
||||
The following example demonstrates how to initialize the tool and execute a search with a given query:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
from crewai_tools import BraveSearchTool
|
||||
|
||||
tool = BraveWebSearchTool()
|
||||
results = tool.run(q="CrewAI agent framework")
|
||||
# Initialize the tool for internet searching capabilities
|
||||
tool = BraveSearchTool()
|
||||
|
||||
# Execute a search
|
||||
results = tool.run(search_query="CrewAI agent framework")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### News Search
|
||||
## Parameters
|
||||
|
||||
The `BraveSearchTool` accepts the following parameters:
|
||||
|
||||
- **search_query**: Mandatory. The search query you want to use to search the internet.
|
||||
- **country**: Optional. Specify the country for the search results. Default is empty string.
|
||||
- **n_results**: Optional. Number of search results to return. Default is `10`.
|
||||
- **save_file**: Optional. Whether to save the search results to a file. Default is `False`.
|
||||
|
||||
## Example with Parameters
|
||||
|
||||
Here is an example demonstrating how to use the tool with additional parameters:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveNewsSearchTool
|
||||
from crewai_tools import BraveSearchTool
|
||||
|
||||
tool = BraveNewsSearchTool()
|
||||
results = tool.run(q="latest AI breakthroughs")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Image Search
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveImageSearchTool
|
||||
|
||||
tool = BraveImageSearchTool()
|
||||
results = tool.run(q="northern lights photography")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Video Search
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveVideoSearchTool
|
||||
|
||||
tool = BraveVideoSearchTool()
|
||||
results = tool.run(q="how to build AI agents")
|
||||
print(results)
|
||||
```
|
||||
|
||||
### Location POI Descriptions
|
||||
|
||||
```python Code
|
||||
from crewai_tools import (
|
||||
BraveWebSearchTool,
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
# Initialize the tool with custom parameters
|
||||
tool = BraveSearchTool(
|
||||
country="US",
|
||||
n_results=5,
|
||||
save_file=True
|
||||
)
|
||||
|
||||
web_search = BraveWebSearchTool(raw=True)
|
||||
poi_details = BraveLocalPOIsDescriptionTool()
|
||||
|
||||
results = web_search.run(q="italian restaurants in pensacola, florida")
|
||||
|
||||
if "locations" in results:
|
||||
location_ids = [ loc["id"] for loc in results["locations"]["results"] ]
|
||||
if location_ids:
|
||||
descriptions = poi_details.run(ids=location_ids)
|
||||
print(descriptions)
|
||||
```
|
||||
|
||||
## Common Constructor Parameters
|
||||
|
||||
Every Brave Search tool accepts the following parameters at initialization:
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `api_key` | `str \| None` | `None` | Brave API key. Falls back to the `BRAVE_API_KEY` environment variable. |
|
||||
| `headers` | `dict \| None` | `None` | Additional HTTP headers to send with every request (e.g., `api-version`, geolocation headers). |
|
||||
| `requests_per_second` | `float` | `1.0` | Maximum request rate. The tool will sleep between calls to stay within this limit. |
|
||||
| `save_file` | `bool` | `False` | When `True`, each response is written to a timestamped `.txt` file. |
|
||||
| `raw` | `bool` | `False` | When `True`, the full API JSON response is returned without any refinement. |
|
||||
| `timeout` | `int` | `30` | HTTP request timeout in seconds. |
|
||||
| `country` | `str \| None` | `None` | Legacy shorthand for geo-targeting (e.g., `"US"`). Prefer using the `country` query parameter directly. |
|
||||
| `n_results` | `int` | `10` | Legacy shorthand for result count. Prefer using the `count` query parameter directly. |
|
||||
|
||||
<Warning>
|
||||
The `country` and `n_results` constructor parameters exist for backwards compatibility. They are applied as defaults when the corresponding query parameters (`country`, `count`) are not provided at call time. For new code, we recommend passing `country` and `count` directly as query parameters instead.
|
||||
</Warning>
|
||||
|
||||
## Query Parameters
|
||||
|
||||
Each tool validates its query parameters against a Pydantic schema before sending the request.
|
||||
The parameters vary slightly per endpoint — here is a summary of the most commonly used ones:
|
||||
|
||||
### BraveWebSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting (e.g., `"US"`). |
|
||||
| `search_lang` | Two-letter language code for results (e.g., `"en"`). |
|
||||
| `count` | Max number of results to return (1–20). |
|
||||
| `offset` | Skip the first N pages of results (0–9). |
|
||||
| `safesearch` | Content filter: `"off"`, `"moderate"`, or `"strict"`. |
|
||||
| `freshness` | Recency filter: `"pd"` (past day), `"pw"` (past week), `"pm"` (past month), `"py"` (past year), or a date range like `"2025-01-01to2025-06-01"`. |
|
||||
| `extra_snippets` | Include up to 5 additional text snippets per result. |
|
||||
| `goggles` | Brave Goggles URL(s) and/or source for custom re-ranking. |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave Web Search API documentation](https://api-dashboard.search.brave.com/api-reference/web/search/get).
|
||||
|
||||
### BraveNewsSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
| `count` | Max number of results to return (1–50). |
|
||||
| `offset` | Skip the first N pages of results (0–9). |
|
||||
| `safesearch` | Content filter: `"off"`, `"moderate"`, or `"strict"`. |
|
||||
| `freshness` | Recency filter (same options as Web Search). |
|
||||
| `goggles` | Brave Goggles URL(s) and/or source for custom re-ranking. |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave News Search API documentation](https://api-dashboard.search.brave.com/api-reference/news/news_search/get).
|
||||
|
||||
### BraveImageSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
| `count` | Max number of results to return (1–200). |
|
||||
| `safesearch` | Content filter: `"off"` or `"strict"`. |
|
||||
| `spellcheck` | Attempt to correct spelling errors in the query. |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave Image Search API documentation](https://api-dashboard.search.brave.com/api-reference/images/image_search).
|
||||
|
||||
### BraveVideoSearchTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `q` | **(required)** Search query string (max 400 chars). |
|
||||
| `country` | Two-letter country code for geo-targeting. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
| `count` | Max number of results to return (1–50). |
|
||||
| `offset` | Skip the first N pages of results (0–9). |
|
||||
| `safesearch` | Content filter: `"off"`, `"moderate"`, or `"strict"`. |
|
||||
| `freshness` | Recency filter (same options as Web Search). |
|
||||
|
||||
For the complete parameter and header reference, see the [Brave Video Search API documentation](https://api-dashboard.search.brave.com/api-reference/videos/video_search/get).
|
||||
|
||||
### BraveLocalPOIsTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `ids` | **(required)** A list of unique identifiers for the desired locations. |
|
||||
| `search_lang` | Two-letter language code for results. |
|
||||
|
||||
For the complete parameter and header reference, see [Brave Local POIs API documentation](https://api-dashboard.search.brave.com/api-reference/web/local_pois).
|
||||
|
||||
### BraveLocalPOIsDescriptionTool
|
||||
|
||||
| Parameter | Description |
|
||||
| --- | --- |
|
||||
| `ids` | **(required)** A list of unique identifiers for the desired locations. |
|
||||
|
||||
For the complete parameter and header reference, see [Brave POI Descriptions API documentation](https://api-dashboard.search.brave.com/api-reference/web/poi_descriptions).
|
||||
|
||||
## Custom Headers
|
||||
|
||||
All tools support custom HTTP request headers. The Web Search tool, for example, accepts geolocation headers for location-aware results:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(
|
||||
headers={
|
||||
"x-loc-lat": "37.7749",
|
||||
"x-loc-long": "-122.4194",
|
||||
"x-loc-city": "San Francisco",
|
||||
"x-loc-state": "CA",
|
||||
"x-loc-country": "US",
|
||||
}
|
||||
)
|
||||
|
||||
results = tool.run(q="best coffee shops nearby")
|
||||
```
|
||||
|
||||
You can also update headers after initialization using the `set_headers()` method:
|
||||
|
||||
```python Code
|
||||
tool.set_headers({"api-version": "2025-01-01"})
|
||||
```
|
||||
|
||||
## Raw Mode
|
||||
|
||||
By default, each tool refines the API response into a concise list of results. If you need the full, unprocessed API response, enable raw mode:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(raw=True)
|
||||
full_response = tool.run(q="Brave Search API")
|
||||
# Execute a search
|
||||
results = tool.run(search_query="Latest AI developments")
|
||||
print(results)
|
||||
```
|
||||
|
||||
## Agent Integration Example
|
||||
|
||||
Here's how to equip a CrewAI agent with multiple Brave Search tools:
|
||||
Here's how to integrate the `BraveSearchTool` with a CrewAI agent:
|
||||
|
||||
```python Code
|
||||
from crewai import Agent
|
||||
from crewai.project import agent
|
||||
from crewai_tools import BraveWebSearchTool, BraveNewsSearchTool
|
||||
from crewai_tools import BraveSearchTool
|
||||
|
||||
web_search = BraveWebSearchTool()
|
||||
news_search = BraveNewsSearchTool()
|
||||
# Initialize the tool
|
||||
brave_search_tool = BraveSearchTool()
|
||||
|
||||
# Define an agent with the BraveSearchTool
|
||||
@agent
|
||||
def researcher(self) -> Agent:
|
||||
return Agent(
|
||||
config=self.agents_config["researcher"],
|
||||
tools=[web_search, news_search],
|
||||
allow_delegation=False,
|
||||
tools=[brave_search_tool]
|
||||
)
|
||||
```
|
||||
|
||||
## Advanced Example
|
||||
|
||||
Combining multiple parameters for a targeted search:
|
||||
|
||||
```python Code
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(
|
||||
requests_per_second=0.5, # conservative rate limit
|
||||
save_file=True,
|
||||
)
|
||||
|
||||
results = tool.run(
|
||||
q="artificial intelligence news",
|
||||
country="US",
|
||||
search_lang="en",
|
||||
count=5,
|
||||
freshness="pm", # past month only
|
||||
extra_snippets=True,
|
||||
)
|
||||
print(results)
|
||||
```
|
||||
|
||||
## Migrating from `BraveSearchTool` (Legacy)
|
||||
|
||||
If you are currently using `BraveSearchTool`, switching to the new tools is straightforward:
|
||||
|
||||
```python Code
|
||||
# Before (legacy)
|
||||
from crewai_tools import BraveSearchTool
|
||||
|
||||
tool = BraveSearchTool(country="US", n_results=5, save_file=True)
|
||||
results = tool.run(search_query="AI agents")
|
||||
|
||||
# After (recommended)
|
||||
from crewai_tools import BraveWebSearchTool
|
||||
|
||||
tool = BraveWebSearchTool(save_file=True)
|
||||
results = tool.run(q="AI agents", country="US", count=5)
|
||||
```
|
||||
|
||||
Key differences:
|
||||
- **Import**: Use `BraveWebSearchTool` (or the news/image/video variant) instead of `BraveSearchTool`.
|
||||
- **Query parameter**: Use `q` instead of `search_query`. (Both `search_query` and `query` are still accepted for convenience, but `q` is the preferred parameter.)
|
||||
- **Result count**: Pass `count` as a query parameter instead of `n_results` at init time.
|
||||
- **Country**: Pass `country` as a query parameter instead of at init time.
|
||||
- **API key**: Can now be passed directly via `api_key=` in addition to the `BRAVE_API_KEY` environment variable.
|
||||
- **Rate limiting**: Configurable via `requests_per_second` with automatic retry on `429` responses.
|
||||
|
||||
## Conclusion
|
||||
|
||||
The Brave Search tool suite gives your CrewAI agents flexible, endpoint-specific access to the Brave Search API. Whether you need web pages, breaking news, images, or videos, there is a dedicated tool with validated parameters and built-in resilience. Pick the tool that fits your use case, and refer to the [Brave Search API documentation](https://brave.com/search/api/) for the full details on available parameters and response formats.
|
||||
By integrating the `BraveSearchTool` into Python projects, users gain the ability to conduct real-time, relevant searches across the internet directly from their applications. The tool provides a simple interface to the powerful Brave Search API, making it easy to retrieve and process search results programmatically. By adhering to the setup and usage guidelines provided, incorporating this tool into projects is streamlined and straightforward.
|
||||
@@ -4,39 +4,6 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 11일">
|
||||
## v1.10.2a1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2a1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Anthropics에 대한 도구 검색 지원 추가, 토큰 저장, 실행 중 적절한 도구를 동적으로 주입하는 기능 추가.
|
||||
- 더 많은 Brave Search 도구 도입.
|
||||
- 야간 릴리스를 위한 액션 생성.
|
||||
|
||||
### 버그 수정
|
||||
- 동시 다중 프로세스 실행 중 LockException 수정.
|
||||
- 단일 사용자 메시지에서 병렬 도구 결과 그룹화 문제 해결.
|
||||
- MCP 도구 해상도 문제 해결 및 모든 공유 가변 연결 제거.
|
||||
- human_feedback 함수에서 LLM 매개변수 처리 업데이트.
|
||||
- LockedListProxy 및 LockedDictProxy에 누락된 list/dict 메서드 추가.
|
||||
- 병렬 도구 호출 스레드에 contextvars 컨텍스트 전파.
|
||||
- CVE 경로 탐색 취약점을 해결하기 위해 gitpython 의존성을 >=3.1.41로 업데이트.
|
||||
|
||||
### 리팩토링
|
||||
- 메모리 클래스를 직렬화 가능하도록 리팩토링.
|
||||
|
||||
### 문서
|
||||
- v1.10.1에 대한 변경 로그 및 버전 업데이트.
|
||||
|
||||
## 기여자
|
||||
|
||||
@akaKuruma, @github-actions[bot], @giulio-leone, @greysonlalonde, @joaomdmoura, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 3월 4일">
|
||||
## v1.10.1
|
||||
|
||||
|
||||
@@ -4,39 +4,6 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="11 mar 2026">
|
||||
## v1.10.2a1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.2a1)
|
||||
|
||||
## O que mudou
|
||||
|
||||
### Recursos
|
||||
- Adicionar suporte para busca de ferramentas, salvamento de tokens e injeção dinâmica de ferramentas apropriadas durante a execução para Anthropics.
|
||||
- Introduzir mais ferramentas de Busca Brave.
|
||||
- Criar ação para lançamentos noturnos.
|
||||
|
||||
### Correções de Bugs
|
||||
- Corrigir LockException durante a execução concorrente de múltiplos processos.
|
||||
- Resolver problemas com a agrupação de resultados de ferramentas paralelas em uma única mensagem de usuário.
|
||||
- Abordar resoluções de ferramentas MCP e eliminar todas as conexões mutáveis compartilhadas.
|
||||
- Atualizar o manuseio de parâmetros LLM na função human_feedback.
|
||||
- Adicionar métodos de lista/dicionário ausentes a LockedListProxy e LockedDictProxy.
|
||||
- Propagar o contexto de contextvars para as threads de chamada de ferramentas paralelas.
|
||||
- Atualizar a dependência gitpython para >=3.1.41 para resolver a vulnerabilidade de travessia de diretórios CVE.
|
||||
|
||||
### Refatoração
|
||||
- Refatorar classes de memória para serem serializáveis.
|
||||
|
||||
### Documentação
|
||||
- Atualizar o changelog e a versão para v1.10.1.
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@akaKuruma, @github-actions[bot], @giulio-leone, @greysonlalonde, @joaomdmoura, @jonathansampson, @lorenzejay, @lucasgomide, @mattatcha
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="04 mar 2026">
|
||||
## v1.10.1
|
||||
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.10.2a1"
|
||||
__version__ = "1.10.1"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.10.2a1",
|
||||
"crewai==1.10.1",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -10,18 +10,7 @@ from crewai_tools.aws.s3.writer_tool import S3WriterTool
|
||||
from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool
|
||||
from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool
|
||||
from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_image_tool import BraveImageSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_llm_context_tool import (
|
||||
BraveLLMContextTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_local_pois_tool import (
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
BraveLocalPOIsTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_news_tool import BraveNewsSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_search_tool import BraveSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_video_tool import BraveVideoSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_web_tool import BraveWebSearchTool
|
||||
from crewai_tools.tools.brightdata_tool.brightdata_dataset import (
|
||||
BrightDataDatasetTool,
|
||||
)
|
||||
@@ -211,14 +200,7 @@ __all__ = [
|
||||
"ArxivPaperTool",
|
||||
"BedrockInvokeAgentTool",
|
||||
"BedrockKBRetrieverTool",
|
||||
"BraveImageSearchTool",
|
||||
"BraveLLMContextTool",
|
||||
"BraveLocalPOIsDescriptionTool",
|
||||
"BraveLocalPOIsTool",
|
||||
"BraveNewsSearchTool",
|
||||
"BraveSearchTool",
|
||||
"BraveVideoSearchTool",
|
||||
"BraveWebSearchTool",
|
||||
"BrightDataDatasetTool",
|
||||
"BrightDataSearchTool",
|
||||
"BrightDataWebUnlockerTool",
|
||||
@@ -309,4 +291,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.10.2a1"
|
||||
__version__ = "1.10.1"
|
||||
|
||||
@@ -1,18 +1,7 @@
|
||||
from crewai_tools.tools.ai_mind_tool.ai_mind_tool import AIMindTool
|
||||
from crewai_tools.tools.apify_actors_tool.apify_actors_tool import ApifyActorsTool
|
||||
from crewai_tools.tools.arxiv_paper_tool.arxiv_paper_tool import ArxivPaperTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_image_tool import BraveImageSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_llm_context_tool import (
|
||||
BraveLLMContextTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_local_pois_tool import (
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
BraveLocalPOIsTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_news_tool import BraveNewsSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_search_tool import BraveSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_video_tool import BraveVideoSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_web_tool import BraveWebSearchTool
|
||||
from crewai_tools.tools.brightdata_tool import (
|
||||
BrightDataDatasetTool,
|
||||
BrightDataSearchTool,
|
||||
@@ -196,14 +185,7 @@ __all__ = [
|
||||
"AIMindTool",
|
||||
"ApifyActorsTool",
|
||||
"ArxivPaperTool",
|
||||
"BraveImageSearchTool",
|
||||
"BraveLLMContextTool",
|
||||
"BraveLocalPOIsDescriptionTool",
|
||||
"BraveLocalPOIsTool",
|
||||
"BraveNewsSearchTool",
|
||||
"BraveSearchTool",
|
||||
"BraveVideoSearchTool",
|
||||
"BraveWebSearchTool",
|
||||
"BrightDataDatasetTool",
|
||||
"BrightDataSearchTool",
|
||||
"BrightDataWebUnlockerTool",
|
||||
|
||||
@@ -1,322 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from crewai.tools import BaseTool, EnvVar
|
||||
from pydantic import BaseModel, Field
|
||||
import requests
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Brave API error codes that indicate non-retryable quota/usage exhaustion.
|
||||
_QUOTA_CODES = frozenset({"QUOTA_LIMITED", "USAGE_LIMIT_EXCEEDED"})
|
||||
|
||||
|
||||
def _save_results_to_file(content: str) -> None:
|
||||
"""Saves the search results to a file."""
|
||||
filename = f"search_results_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt"
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
|
||||
def _parse_error_body(resp: requests.Response) -> dict[str, Any] | None:
|
||||
"""Extract the structured "error" object from a Brave API error response."""
|
||||
try:
|
||||
body = resp.json()
|
||||
error = body.get("error")
|
||||
return error if isinstance(error, dict) else None
|
||||
except (ValueError, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
def _raise_for_error(resp: requests.Response) -> None:
|
||||
"""Brave Search API error responses contain helpful JSON payloads"""
|
||||
status = resp.status_code
|
||||
try:
|
||||
body = json.dumps(resp.json())
|
||||
except (ValueError, KeyError):
|
||||
body = resp.text[:500]
|
||||
|
||||
raise RuntimeError(f"Brave Search API error (HTTP {status}): {body}")
|
||||
|
||||
|
||||
def _is_retryable(resp: requests.Response) -> bool:
|
||||
"""Return True for transient failures that are worth retrying.
|
||||
|
||||
* 429 + RATE_LIMITED — the per-second sliding window is full.
|
||||
* 5xx — transient server-side errors.
|
||||
|
||||
Quota exhaustion (QUOTA_LIMITED, USAGE_LIMIT_EXCEEDED) is
|
||||
explicitly excluded: retrying will never succeed until the billing
|
||||
period resets.
|
||||
"""
|
||||
if resp.status_code == 429:
|
||||
error = _parse_error_body(resp) or {}
|
||||
return error.get("code") not in _QUOTA_CODES
|
||||
return 500 <= resp.status_code < 600
|
||||
|
||||
|
||||
def _retry_delay(resp: requests.Response, attempt: int) -> float:
|
||||
"""Compute wait time before the next retry attempt.
|
||||
|
||||
Prefers the server-supplied Retry-After header when available;
|
||||
falls back to exponential backoff (1s, 2s, 4s, ...).
|
||||
"""
|
||||
retry_after = resp.headers.get("Retry-After")
|
||||
if retry_after is not None:
|
||||
try:
|
||||
return max(0.0, float(retry_after))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
return float(2**attempt)
|
||||
|
||||
|
||||
class BraveSearchToolBase(BaseTool, ABC):
|
||||
"""
|
||||
Base class for Brave Search API interactions.
|
||||
|
||||
Individual tool subclasses must provide the following:
|
||||
- search_url
|
||||
- header_schema (pydantic model)
|
||||
- args_schema (pydantic model)
|
||||
- _refine_payload() -> dict[str, Any]
|
||||
"""
|
||||
|
||||
search_url: str
|
||||
raw: bool = False
|
||||
args_schema: type[BaseModel]
|
||||
header_schema: type[BaseModel]
|
||||
|
||||
# Tool options (legacy parameters)
|
||||
country: str | None = None
|
||||
save_file: bool = False
|
||||
n_results: int = 10
|
||||
|
||||
env_vars: list[EnvVar] = Field(
|
||||
default_factory=lambda: [
|
||||
EnvVar(
|
||||
name="BRAVE_API_KEY",
|
||||
description="API key for Brave Search",
|
||||
required=True,
|
||||
),
|
||||
]
|
||||
)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
api_key: str | None = None,
|
||||
headers: dict[str, Any] | None = None,
|
||||
requests_per_second: float = 1.0,
|
||||
save_file: bool = False,
|
||||
raw: bool = False,
|
||||
timeout: int = 30,
|
||||
**kwargs: Any,
|
||||
):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._api_key = api_key or os.environ.get("BRAVE_API_KEY")
|
||||
if not self._api_key:
|
||||
raise ValueError("BRAVE_API_KEY environment variable is required")
|
||||
|
||||
self.raw = bool(raw)
|
||||
self._timeout = int(timeout)
|
||||
self.save_file = bool(save_file)
|
||||
self._requests_per_second = float(requests_per_second)
|
||||
self._headers = self._build_and_validate_headers(headers or {})
|
||||
# Per-instance rate limiting: each instance has its own clock and lock.
|
||||
# Total process rate is the sum of limits of instances you create.
|
||||
self._last_request_time: float = 0
|
||||
self._rate_limit_lock = threading.Lock()
|
||||
|
||||
@property
|
||||
def api_key(self) -> str:
|
||||
return self._api_key
|
||||
|
||||
@property
|
||||
def headers(self) -> dict[str, Any]:
|
||||
return self._headers
|
||||
|
||||
def set_headers(self, headers: dict[str, Any]) -> BraveSearchToolBase:
|
||||
merged = {**self._headers, **{k.lower(): v for k, v in headers.items()}}
|
||||
self._headers = self._build_and_validate_headers(merged)
|
||||
return self
|
||||
|
||||
def _build_and_validate_headers(self, headers: dict[str, Any]) -> dict[str, Any]:
|
||||
normalized = {k.lower(): v for k, v in headers.items()}
|
||||
normalized.setdefault("x-subscription-token", self._api_key)
|
||||
normalized.setdefault("accept", "application/json")
|
||||
|
||||
try:
|
||||
self.header_schema(**normalized)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid headers: {e}") from e
|
||||
|
||||
return normalized
|
||||
|
||||
def _rate_limit(self) -> None:
|
||||
"""Enforce minimum interval between requests for this instance. Thread-safe."""
|
||||
if self._requests_per_second <= 0:
|
||||
return
|
||||
|
||||
min_interval = 1.0 / self._requests_per_second
|
||||
with self._rate_limit_lock:
|
||||
now = time.time()
|
||||
next_allowed = self._last_request_time + min_interval
|
||||
if now < next_allowed:
|
||||
time.sleep(next_allowed - now)
|
||||
now = time.time()
|
||||
self._last_request_time = now
|
||||
|
||||
def _make_request(
|
||||
self, params: dict[str, Any], *, _max_retries: int = 3
|
||||
) -> dict[str, Any]:
|
||||
"""Execute an HTTP GET against the Brave Search API with retry logic."""
|
||||
last_resp: requests.Response | None = None
|
||||
|
||||
# Retry the request up to _max_retries times
|
||||
for attempt in range(_max_retries):
|
||||
self._rate_limit()
|
||||
|
||||
# Make the request
|
||||
try:
|
||||
resp = requests.get(
|
||||
self.search_url,
|
||||
headers=self._headers,
|
||||
params=params,
|
||||
timeout=self._timeout,
|
||||
)
|
||||
except requests.ConnectionError as exc:
|
||||
raise RuntimeError(
|
||||
f"Brave Search API connection failed: {exc}"
|
||||
) from exc
|
||||
except requests.Timeout as exc:
|
||||
raise RuntimeError(
|
||||
f"Brave Search API request timed out after {self._timeout}s: {exc}"
|
||||
) from exc
|
||||
|
||||
# Log the rate limit headers and request details
|
||||
logger.debug(
|
||||
"Brave Search API request: %s %s -> %d",
|
||||
"GET",
|
||||
resp.url,
|
||||
resp.status_code,
|
||||
)
|
||||
|
||||
# Response was OK, return the JSON body
|
||||
if resp.ok:
|
||||
try:
|
||||
return resp.json()
|
||||
except ValueError as exc:
|
||||
raise RuntimeError(
|
||||
f"Brave Search API returned invalid JSON (HTTP {resp.status_code}): {exc}"
|
||||
) from exc
|
||||
|
||||
# Response was not OK, but is retryable
|
||||
# (e.g., 429 Too Many Requests, 500 Internal Server Error)
|
||||
if _is_retryable(resp) and attempt < _max_retries - 1:
|
||||
delay = _retry_delay(resp, attempt)
|
||||
logger.warning(
|
||||
"Brave Search API returned %d. Retrying in %.1fs (attempt %d/%d)",
|
||||
resp.status_code,
|
||||
delay,
|
||||
attempt + 1,
|
||||
_max_retries,
|
||||
)
|
||||
time.sleep(delay)
|
||||
last_resp = resp
|
||||
continue
|
||||
|
||||
# Response was not OK, nor was it retryable
|
||||
# (e.g., 422 Unprocessable Entity, 400 Bad Request (OPTION_NOT_IN_PLAN))
|
||||
_raise_for_error(resp)
|
||||
|
||||
# All retries exhausted
|
||||
_raise_for_error(last_resp or resp) # type: ignore[possibly-undefined]
|
||||
return {} # unreachable (here to satisfy the type checker and linter)
|
||||
|
||||
def _run(self, q: str | None = None, **params: Any) -> Any:
|
||||
# Allow positional usage: tool.run("latest Brave browser features")
|
||||
if q is not None:
|
||||
params["q"] = q
|
||||
|
||||
params = self._common_payload_refinement(params)
|
||||
|
||||
# Validate only schema fields
|
||||
schema_keys = self.args_schema.model_fields
|
||||
payload_in = {k: v for k, v in params.items() if k in schema_keys}
|
||||
|
||||
try:
|
||||
validated = self.args_schema(**payload_in)
|
||||
except Exception as e:
|
||||
raise ValueError(f"Invalid parameters: {e}") from e
|
||||
|
||||
# The subclass may have additional refinements to apply to the payload, such as goggles or other parameters
|
||||
payload = self._refine_request_payload(validated.model_dump(exclude_none=True))
|
||||
response = self._make_request(payload)
|
||||
|
||||
if not self.raw:
|
||||
response = self._refine_response(response)
|
||||
|
||||
if self.save_file:
|
||||
_save_results_to_file(json.dumps(response, indent=2))
|
||||
|
||||
return response
|
||||
|
||||
@abstractmethod
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Subclass must implement: transform validated params dict into API request params."""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def _refine_response(self, response: dict[str, Any]) -> Any:
|
||||
"""Subclass must implement: transform response dict into a more useful format."""
|
||||
raise NotImplementedError
|
||||
|
||||
_EMPTY_VALUES: ClassVar[tuple[None, str, str, list[Any]]] = (None, "", "null", [])
|
||||
|
||||
def _common_payload_refinement(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Common payload refinement for all tools."""
|
||||
# crewAI's schema pipeline (ensure_all_properties_required in
|
||||
# pydantic_schema_utils.py) marks every property as required so
|
||||
# that OpenAI strict-mode structured outputs work correctly.
|
||||
# The side-effect is that the LLM fills in *every* parameter —
|
||||
# even truly optional ones — using placeholder values such as
|
||||
# None, "", "null", or []. Only optional fields are affected,
|
||||
# so we limit the check to those.
|
||||
fields = self.args_schema.model_fields
|
||||
params = {
|
||||
k: v
|
||||
for k, v in params.items()
|
||||
# Permit custom and required fields, and fields with non-empty values
|
||||
if k not in fields or fields[k].is_required() or v not in self._EMPTY_VALUES
|
||||
}
|
||||
|
||||
# Make sure params has "q" for query instead of "query" or "search_query"
|
||||
query = params.get("query") or params.get("search_query")
|
||||
if query is not None and "q" not in params:
|
||||
params["q"] = query
|
||||
params.pop("query", None)
|
||||
params.pop("search_query", None)
|
||||
|
||||
# If "count" was not explicitly provided, use n_results
|
||||
# (only when the schema actually supports a "count" field)
|
||||
if "count" in self.args_schema.model_fields:
|
||||
if "count" not in params and self.n_results is not None:
|
||||
params["count"] = self.n_results
|
||||
|
||||
# If "country" was not explicitly provided, but self.country is set, use it
|
||||
# (only when the schema actually supports a "country" field)
|
||||
if "country" in self.args_schema.model_fields:
|
||||
if "country" not in params and self.country is not None:
|
||||
params["country"] = self.country
|
||||
|
||||
return params
|
||||
@@ -1,42 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
ImageSearchHeaders,
|
||||
ImageSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveImageSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs image searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Image Search"
|
||||
args_schema: type[BaseModel] = ImageSearchParams
|
||||
header_schema: type[BaseModel] = ImageSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs image searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/images/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"title": result.get("title"),
|
||||
"url": result.get("properties", {}).get("url"),
|
||||
"dimensions": f"{w}x{h}"
|
||||
if (w := result.get("properties", {}).get("width"))
|
||||
and (h := result.get("properties", {}).get("height"))
|
||||
else None,
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -1,32 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.response_types import LLMContext
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
LLMContextHeaders,
|
||||
LLMContextParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveLLMContextTool(BraveSearchToolBase):
|
||||
"""A tool that retrieves context for LLM usage from the Brave Search API."""
|
||||
|
||||
name: str = "Brave LLM Context"
|
||||
args_schema: type[BaseModel] = LLMContextParams
|
||||
header_schema: type[BaseModel] = LLMContextHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that retrieves context for LLM usage from the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/llm/context"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: LLMContext.Response) -> LLMContext.Response:
|
||||
"""The LLM Context response schema is fairly simple. Return as is."""
|
||||
return response
|
||||
@@ -1,109 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.response_types import LocalPOIs
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
LocalPOIsDescriptionHeaders,
|
||||
LocalPOIsDescriptionParams,
|
||||
LocalPOIsHeaders,
|
||||
LocalPOIsParams,
|
||||
)
|
||||
|
||||
|
||||
DayOpeningHours = LocalPOIs.DayOpeningHours
|
||||
OpeningHours = LocalPOIs.OpeningHours
|
||||
LocationResult = LocalPOIs.LocationResult
|
||||
LocalPOIsResponse = LocalPOIs.Response
|
||||
|
||||
|
||||
def _flatten_slots(slots: list[DayOpeningHours]) -> list[dict[str, str]]:
|
||||
"""Convert a list of DayOpeningHours dicts into simplified entries."""
|
||||
return [
|
||||
{
|
||||
"day": slot["full_name"].lower(),
|
||||
"opens": slot["opens"],
|
||||
"closes": slot["closes"],
|
||||
}
|
||||
for slot in slots
|
||||
]
|
||||
|
||||
|
||||
def _simplify_opening_hours(result: LocationResult) -> list[dict[str, str]] | None:
|
||||
"""Collapse opening_hours into a flat list of {day, opens, closes} dicts."""
|
||||
hours = result.get("opening_hours")
|
||||
if not hours:
|
||||
return None
|
||||
|
||||
entries: list[dict[str, str]] = []
|
||||
|
||||
current = hours.get("current_day")
|
||||
if current:
|
||||
entries.extend(_flatten_slots(current))
|
||||
|
||||
days = hours.get("days")
|
||||
if days:
|
||||
for day_slots in days:
|
||||
entries.extend(_flatten_slots(day_slots))
|
||||
|
||||
return entries or None
|
||||
|
||||
|
||||
class BraveLocalPOIsTool(BraveSearchToolBase):
|
||||
"""A tool that retrieves local POIs using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Local POIs"
|
||||
args_schema: type[BaseModel] = LocalPOIsParams
|
||||
header_schema: type[BaseModel] = LocalPOIsHeaders
|
||||
description: str = (
|
||||
"A tool that retrieves local POIs using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
search_url: str = "https://api.search.brave.com/res/v1/local/pois"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: LocalPOIsResponse) -> list[dict[str, Any]]:
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"title": result.get("title"),
|
||||
"url": result.get("url"),
|
||||
"description": result.get("description"),
|
||||
"address": result.get("postal_address", {}).get("displayAddress"),
|
||||
"contact": result.get("contact", {}).get("telephone")
|
||||
or result.get("contact", {}).get("email")
|
||||
or None,
|
||||
"opening_hours": _simplify_opening_hours(result),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
|
||||
|
||||
class BraveLocalPOIsDescriptionTool(BraveSearchToolBase):
|
||||
"""A tool that retrieves AI-generated descriptions for local POIs using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Local POI Descriptions"
|
||||
args_schema: type[BaseModel] = LocalPOIsDescriptionParams
|
||||
header_schema: type[BaseModel] = LocalPOIsDescriptionHeaders
|
||||
description: str = (
|
||||
"A tool that retrieves AI-generated descriptions for local POIs using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
search_url: str = "https://api.search.brave.com/res/v1/local/descriptions"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: LocalPOIsResponse) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"id": result.get("id"),
|
||||
"description": result.get("description"),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -1,39 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
NewsSearchHeaders,
|
||||
NewsSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveNewsSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs news searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave News Search"
|
||||
args_schema: type[BaseModel] = NewsSearchParams
|
||||
header_schema: type[BaseModel] = NewsSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs news searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/news/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"url": result.get("url"),
|
||||
"title": result.get("title"),
|
||||
"description": result.get("description"),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -10,13 +10,17 @@ from pydantic import BaseModel, Field
|
||||
from pydantic.types import StringConstraints
|
||||
import requests
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.schemas import WebSearchParams
|
||||
from crewai_tools.tools.brave_search_tool.base import _save_results_to_file
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def _save_results_to_file(content: str) -> None:
|
||||
"""Saves the search results to a file."""
|
||||
filename = f"search_results_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.txt"
|
||||
with open(filename, "w") as file:
|
||||
file.write(content)
|
||||
|
||||
|
||||
FreshnessPreset = Literal["pd", "pw", "pm", "py"]
|
||||
FreshnessRange = Annotated[
|
||||
str, StringConstraints(pattern=r"^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$")
|
||||
@@ -25,6 +29,51 @@ Freshness = FreshnessPreset | FreshnessRange
|
||||
SafeSearch = Literal["off", "moderate", "strict"]
|
||||
|
||||
|
||||
class BraveSearchToolSchema(BaseModel):
|
||||
"""Input for BraveSearchTool"""
|
||||
|
||||
query: str = Field(..., description="Search query to perform")
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
)
|
||||
search_language: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return. Actual number may be less.",
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None, description="Skip the first N result sets/pages. Max is 9."
|
||||
)
|
||||
safesearch: SafeSearch | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
text_decorations: bool | None = Field(
|
||||
default=None,
|
||||
description="Include markup to highlight search terms in the results.",
|
||||
)
|
||||
extra_snippets: bool | None = Field(
|
||||
default=None,
|
||||
description="Include up to 5 text snippets for each page if possible.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
# TODO: Extend support to additional endpoints (e.g., /images, /news, etc.)
|
||||
class BraveSearchTool(BaseTool):
|
||||
"""A tool that performs web searches using the Brave Search API."""
|
||||
@@ -34,7 +83,7 @@ class BraveSearchTool(BaseTool):
|
||||
"A tool that performs web searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
args_schema: type[BaseModel] = WebSearchParams
|
||||
args_schema: type[BaseModel] = BraveSearchToolSchema
|
||||
search_url: str = "https://api.search.brave.com/res/v1/web/search"
|
||||
n_results: int = 10
|
||||
save_file: bool = False
|
||||
@@ -71,8 +120,8 @@ class BraveSearchTool(BaseTool):
|
||||
|
||||
# Construct and send the request
|
||||
try:
|
||||
# Fallback to "query" or "search_query" for backwards compatibility
|
||||
query = kwargs.get("q") or kwargs.get("query") or kwargs.get("search_query")
|
||||
# Maintain both "search_query" and "query" for backwards compatibility
|
||||
query = kwargs.get("search_query") or kwargs.get("query")
|
||||
if not query:
|
||||
raise ValueError("Query is required")
|
||||
|
||||
@@ -81,11 +130,8 @@ class BraveSearchTool(BaseTool):
|
||||
if country := kwargs.get("country"):
|
||||
payload["country"] = country
|
||||
|
||||
# Fallback to "search_language" for backwards compatibility
|
||||
if search_lang := kwargs.get("search_lang") or kwargs.get(
|
||||
"search_language"
|
||||
):
|
||||
payload["search_lang"] = search_lang
|
||||
if search_language := kwargs.get("search_language"):
|
||||
payload["search_language"] = search_language
|
||||
|
||||
# Fallback to deprecated n_results parameter if no count is provided
|
||||
count = kwargs.get("count")
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
VideoSearchHeaders,
|
||||
VideoSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveVideoSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs video searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Video Search"
|
||||
args_schema: type[BaseModel] = VideoSearchParams
|
||||
header_schema: type[BaseModel] = VideoSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs video searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/videos/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
# Make the response more concise, and easier to consume
|
||||
results = response.get("results", [])
|
||||
return [
|
||||
{
|
||||
"url": result.get("url"),
|
||||
"title": result.get("title"),
|
||||
"description": result.get("description"),
|
||||
}
|
||||
for result in results
|
||||
]
|
||||
@@ -1,45 +0,0 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
WebSearchHeaders,
|
||||
WebSearchParams,
|
||||
)
|
||||
|
||||
|
||||
class BraveWebSearchTool(BraveSearchToolBase):
|
||||
"""A tool that performs web searches using the Brave Search API."""
|
||||
|
||||
name: str = "Brave Web Search"
|
||||
args_schema: type[BaseModel] = WebSearchParams
|
||||
header_schema: type[BaseModel] = WebSearchHeaders
|
||||
|
||||
description: str = (
|
||||
"A tool that performs web searches using the Brave Search API. "
|
||||
"Results are returned as structured JSON data."
|
||||
)
|
||||
|
||||
search_url: str = "https://api.search.brave.com/res/v1/web/search"
|
||||
|
||||
def _refine_request_payload(self, params: dict[str, Any]) -> dict[str, Any]:
|
||||
return params
|
||||
|
||||
def _refine_response(self, response: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
results = response.get("web", {}).get("results", [])
|
||||
refined = []
|
||||
for result in results:
|
||||
snippets = result.get("extra_snippets") or []
|
||||
if not snippets:
|
||||
desc = result.get("description")
|
||||
if desc:
|
||||
snippets = [desc]
|
||||
refined.append(
|
||||
{
|
||||
"url": result.get("url"),
|
||||
"title": result.get("title"),
|
||||
"snippets": snippets,
|
||||
}
|
||||
)
|
||||
return refined
|
||||
@@ -1,67 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Literal, TypedDict
|
||||
|
||||
|
||||
class LocalPOIs:
|
||||
class PostalAddress(TypedDict, total=False):
|
||||
type: Literal["PostalAddress"]
|
||||
country: str
|
||||
postalCode: str
|
||||
streetAddress: str
|
||||
addressRegion: str
|
||||
addressLocality: str
|
||||
displayAddress: str
|
||||
|
||||
class DayOpeningHours(TypedDict):
|
||||
abbr_name: str
|
||||
full_name: str
|
||||
opens: str
|
||||
closes: str
|
||||
|
||||
class OpeningHours(TypedDict, total=False):
|
||||
current_day: list[LocalPOIs.DayOpeningHours]
|
||||
days: list[list[LocalPOIs.DayOpeningHours]]
|
||||
|
||||
class LocationResult(TypedDict, total=False):
|
||||
provider_url: str
|
||||
title: str
|
||||
url: str
|
||||
id: str | None
|
||||
opening_hours: LocalPOIs.OpeningHours | None
|
||||
postal_address: LocalPOIs.PostalAddress | None
|
||||
|
||||
class Response(TypedDict, total=False):
|
||||
type: Literal["local_pois"]
|
||||
results: list[LocalPOIs.LocationResult]
|
||||
|
||||
|
||||
class LLMContext:
|
||||
class LLMContextItem(TypedDict, total=False):
|
||||
snippets: list[str]
|
||||
title: str
|
||||
url: str
|
||||
|
||||
class LLMContextMapItem(TypedDict, total=False):
|
||||
name: str
|
||||
snippets: list[str]
|
||||
title: str
|
||||
url: str
|
||||
|
||||
class LLMContextPOIItem(TypedDict, total=False):
|
||||
name: str
|
||||
snippets: list[str]
|
||||
title: str
|
||||
url: str
|
||||
|
||||
class Grounding(TypedDict, total=False):
|
||||
generic: list[LLMContext.LLMContextItem]
|
||||
poi: LLMContext.LLMContextPOIItem
|
||||
map: list[LLMContext.LLMContextMapItem]
|
||||
|
||||
class Sources(TypedDict, total=False):
|
||||
pass
|
||||
|
||||
class Response(TypedDict, total=False):
|
||||
grounding: LLMContext.Grounding
|
||||
sources: LLMContext.Sources
|
||||
@@ -1,525 +0,0 @@
|
||||
from typing import Annotated, Literal
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic.types import StringConstraints
|
||||
|
||||
|
||||
# Common types
|
||||
Units = Literal["metric", "imperial"]
|
||||
SafeSearch = Literal["off", "moderate", "strict"]
|
||||
Freshness = (
|
||||
Literal["pd", "pw", "pm", "py"]
|
||||
| Annotated[
|
||||
str, StringConstraints(pattern=r"^\d{4}-\d{2}-\d{2}to\d{4}-\d{2}-\d{2}$")
|
||||
]
|
||||
)
|
||||
ResultFilter = list[
|
||||
Literal[
|
||||
"discussions",
|
||||
"faq",
|
||||
"infobox",
|
||||
"news",
|
||||
"query",
|
||||
"summarizer",
|
||||
"videos",
|
||||
"web",
|
||||
"locations",
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
class LLMContextParams(BaseModel):
|
||||
"""Parameters for Brave LLM Context endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return. Actual number may be less.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
maximum_number_of_urls: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of URLs to include in the context.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
maximum_number_of_tokens: int | None = Field(
|
||||
default=None,
|
||||
description="The approximate maximum number of tokens to include in the context.",
|
||||
ge=1,
|
||||
le=32768,
|
||||
)
|
||||
maximum_number_of_snippets: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of different snippets to include in the context.",
|
||||
ge=1,
|
||||
le=100,
|
||||
)
|
||||
context_threshold_mode: (
|
||||
Literal["disabled", "strict", "lenient", "balanced"] | None
|
||||
) = Field(
|
||||
default=None,
|
||||
description="The mode to use for the context thresholding.",
|
||||
)
|
||||
maximum_number_of_tokens_per_url: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of tokens to include for each URL in the context.",
|
||||
ge=1,
|
||||
le=8192,
|
||||
)
|
||||
maximum_number_of_snippets_per_url: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of snippets to include per URL.",
|
||||
ge=1,
|
||||
le=100,
|
||||
)
|
||||
goggles: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Goggles act as a custom re-ranking mechanism. Goggle source or URLs.",
|
||||
)
|
||||
enable_local: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable local recall. Not setting this value means auto-detect and uses local recall if any of the localization headers are provided.",
|
||||
)
|
||||
|
||||
|
||||
class WebSearchParams(BaseModel):
|
||||
"""Parameters for Brave Web Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return. Actual number may be less.",
|
||||
ge=1,
|
||||
le=20,
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None,
|
||||
description="Skip the first N result sets/pages. Max is 9.",
|
||||
ge=0,
|
||||
le=9,
|
||||
)
|
||||
safesearch: Literal["off", "moderate", "strict"] | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
text_decorations: bool | None = Field(
|
||||
default=None,
|
||||
description="Include markup to highlight search terms in the results.",
|
||||
)
|
||||
extra_snippets: bool | None = Field(
|
||||
default=None,
|
||||
description="Include up to 5 text snippets for each page if possible.",
|
||||
)
|
||||
result_filter: ResultFilter | None = Field(
|
||||
default=None,
|
||||
description="Filter the results by type. Options: discussions/faq/infobox/news/query/summarizer/videos/web/locations. Note: The `count` parameter is applied only to the `web` results.",
|
||||
)
|
||||
units: Units | None = Field(
|
||||
default=None,
|
||||
description="The units to use for the results. Options: metric/imperial",
|
||||
)
|
||||
goggles: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Goggles act as a custom re-ranking mechanism. Goggle source or URLs.",
|
||||
)
|
||||
summary: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to generate a summarizer ID for the results.",
|
||||
)
|
||||
enable_rich_callback: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to enable rich callbacks for the results. Requires Pro level subscription.",
|
||||
)
|
||||
include_fetch_metadata: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to include fetch metadata (e.g., last fetch time) in the results.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsParams(BaseModel):
|
||||
"""Parameters for Brave Local POIs endpoint."""
|
||||
|
||||
ids: list[str] = Field(
|
||||
description="List of POI IDs to retrieve. Maximum of 20. IDs are valid for 8 hours.",
|
||||
min_length=1,
|
||||
max_length=20,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
units: Units | None = Field(
|
||||
default=None,
|
||||
description="The units to use for the results. Options: metric/imperial",
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsDescriptionParams(BaseModel):
|
||||
"""Parameters for Brave Local POI Descriptions endpoint."""
|
||||
|
||||
ids: list[str] = Field(
|
||||
description="List of POI IDs to retrieve. Maximum of 20. IDs are valid for 8 hours.",
|
||||
min_length=1,
|
||||
max_length=20,
|
||||
)
|
||||
|
||||
|
||||
class ImageSearchParams(BaseModel):
|
||||
"""Parameters for Brave Image Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
safesearch: Literal["off", "strict"] | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Default is strict.",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return.",
|
||||
ge=1,
|
||||
le=200,
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
|
||||
|
||||
class VideoSearchParams(BaseModel):
|
||||
"""Parameters for Brave Video Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
safesearch: SafeSearch | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None,
|
||||
description="Skip the first N result sets/pages. Max is 9.",
|
||||
ge=0,
|
||||
le=9,
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
include_fetch_metadata: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to include fetch metadata (e.g., last fetch time) in the results.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
class NewsSearchParams(BaseModel):
|
||||
"""Parameters for Brave News Search endpoint."""
|
||||
|
||||
q: str = Field(
|
||||
description="Search query to perform",
|
||||
min_length=1,
|
||||
max_length=400,
|
||||
)
|
||||
search_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the search results (e.g., 'en', 'es').",
|
||||
pattern=r"^[a-z]{2}$",
|
||||
)
|
||||
ui_lang: str | None = Field(
|
||||
default=None,
|
||||
description="Language code for the user interface (e.g., 'en-US', 'es-AR').",
|
||||
pattern=r"^[a-z]{2}-[A-Z]{2}$",
|
||||
)
|
||||
country: str | None = Field(
|
||||
default=None,
|
||||
description="Country code for geo-targeting (e.g., 'US', 'BR').",
|
||||
pattern=r"^[A-Z]{2}$",
|
||||
)
|
||||
safesearch: Literal["off", "moderate", "strict"] | None = Field(
|
||||
default=None,
|
||||
description="Filter out explicit content. Options: off/moderate/strict",
|
||||
)
|
||||
count: int | None = Field(
|
||||
default=None,
|
||||
description="The maximum number of results to return.",
|
||||
ge=1,
|
||||
le=50,
|
||||
)
|
||||
offset: int | None = Field(
|
||||
default=None,
|
||||
description="Skip the first N result sets/pages. Max is 9.",
|
||||
ge=0,
|
||||
le=9,
|
||||
)
|
||||
spellcheck: bool | None = Field(
|
||||
default=None,
|
||||
description="Attempt to correct spelling errors in the search query.",
|
||||
)
|
||||
freshness: Freshness | None = Field(
|
||||
default=None,
|
||||
description="Enforce freshness of results. Options: pd/pw/pm/py, or YYYY-MM-DDtoYYYY-MM-DD",
|
||||
)
|
||||
extra_snippets: bool | None = Field(
|
||||
default=None,
|
||||
description="Include up to 5 text snippets for each page if possible.",
|
||||
)
|
||||
goggles: str | list[str] | None = Field(
|
||||
default=None,
|
||||
description="Goggles act as a custom re-ranking mechanism. Goggle source or URLs.",
|
||||
)
|
||||
include_fetch_metadata: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to include fetch metadata in the results.",
|
||||
)
|
||||
operators: bool | None = Field(
|
||||
default=None,
|
||||
description="Whether to apply search operators (e.g., site:example.com).",
|
||||
)
|
||||
|
||||
|
||||
class BaseSearchHeaders(BaseModel):
|
||||
"""Common headers for Brave Search endpoints."""
|
||||
|
||||
x_subscription_token: str = Field(
|
||||
alias="x-subscription-token",
|
||||
description="API key for Brave Search",
|
||||
)
|
||||
api_version: str | None = Field(
|
||||
alias="api-version",
|
||||
default=None,
|
||||
description="API version to use. Default is latest available.",
|
||||
pattern=r"^\d{4}-\d{2}-\d{2}$", # YYYY-MM-DD
|
||||
)
|
||||
accept: Literal["application/json"] | Literal["*/*"] | None = Field(
|
||||
default=None,
|
||||
description="Accept header for the request.",
|
||||
)
|
||||
cache_control: Literal["no-cache"] | None = Field(
|
||||
alias="cache-control",
|
||||
default=None,
|
||||
description="Cache control header for the request.",
|
||||
)
|
||||
user_agent: str | None = Field(
|
||||
alias="user-agent",
|
||||
default=None,
|
||||
description="User agent for the request.",
|
||||
)
|
||||
|
||||
|
||||
class LLMContextHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave LLM Context endpoint."""
|
||||
|
||||
x_loc_lat: float | None = Field(
|
||||
alias="x-loc-lat",
|
||||
default=None,
|
||||
description="Latitude of the user's location.",
|
||||
ge=-90.0,
|
||||
le=90.0,
|
||||
)
|
||||
x_loc_long: float | None = Field(
|
||||
alias="x-loc-long",
|
||||
default=None,
|
||||
description="Longitude of the user's location.",
|
||||
ge=-180.0,
|
||||
le=180.0,
|
||||
)
|
||||
x_loc_city: str | None = Field(
|
||||
alias="x-loc-city",
|
||||
default=None,
|
||||
description="City of the user's location.",
|
||||
)
|
||||
x_loc_state: str | None = Field(
|
||||
alias="x-loc-state",
|
||||
default=None,
|
||||
description="State of the user's location.",
|
||||
)
|
||||
x_loc_state_name: str | None = Field(
|
||||
alias="x-loc-state-name",
|
||||
default=None,
|
||||
description="Name of the state of the user's location.",
|
||||
)
|
||||
x_loc_country: str | None = Field(
|
||||
alias="x-loc-country",
|
||||
default=None,
|
||||
description="The ISO 3166-1 alpha-2 country code of the user's location.",
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Local POIs endpoint."""
|
||||
|
||||
x_loc_lat: float | None = Field(
|
||||
alias="x-loc-lat",
|
||||
default=None,
|
||||
description="Latitude of the user's location.",
|
||||
ge=-90.0,
|
||||
le=90.0,
|
||||
)
|
||||
x_loc_long: float | None = Field(
|
||||
alias="x-loc-long",
|
||||
default=None,
|
||||
description="Longitude of the user's location.",
|
||||
ge=-180.0,
|
||||
le=180.0,
|
||||
)
|
||||
|
||||
|
||||
class LocalPOIsDescriptionHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Local POI Descriptions endpoint."""
|
||||
|
||||
|
||||
class VideoSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Video Search endpoint."""
|
||||
|
||||
|
||||
class ImageSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Image Search endpoint."""
|
||||
|
||||
|
||||
class NewsSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave News Search endpoint."""
|
||||
|
||||
|
||||
class WebSearchHeaders(BaseSearchHeaders):
|
||||
"""Headers for Brave Web Search endpoint."""
|
||||
|
||||
x_loc_lat: float | None = Field(
|
||||
alias="x-loc-lat",
|
||||
default=None,
|
||||
description="Latitude of the user's location.",
|
||||
ge=-90.0,
|
||||
le=90.0,
|
||||
)
|
||||
x_loc_long: float | None = Field(
|
||||
alias="x-loc-long",
|
||||
default=None,
|
||||
description="Longitude of the user's location.",
|
||||
ge=-180.0,
|
||||
le=180.0,
|
||||
)
|
||||
x_loc_timezone: str | None = Field(
|
||||
alias="x-loc-timezone",
|
||||
default=None,
|
||||
description="Timezone of the user's location.",
|
||||
)
|
||||
x_loc_city: str | None = Field(
|
||||
alias="x-loc-city",
|
||||
default=None,
|
||||
description="City of the user's location.",
|
||||
)
|
||||
x_loc_state: str | None = Field(
|
||||
alias="x-loc-state",
|
||||
default=None,
|
||||
description="State of the user's location.",
|
||||
)
|
||||
x_loc_state_name: str | None = Field(
|
||||
alias="x-loc-state-name",
|
||||
default=None,
|
||||
description="Name of the state of the user's location.",
|
||||
)
|
||||
x_loc_country: str | None = Field(
|
||||
alias="x-loc-country",
|
||||
default=None,
|
||||
description="The ISO 3166-1 alpha-2 country code of the user's location.",
|
||||
)
|
||||
x_loc_postal_code: str | None = Field(
|
||||
alias="x-loc-postal-code",
|
||||
default=None,
|
||||
description="The postal code of the user's location.",
|
||||
)
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Code Interpreter Tool for executing Python code in isolated environments.
|
||||
|
||||
This module provides a tool for executing Python code either in a Docker container for
|
||||
safe isolation or directly in a restricted sandbox. It includes mechanisms for blocking
|
||||
potentially unsafe operations and importing restricted modules.
|
||||
This module provides a tool for executing Python code in a Docker container for
|
||||
safe isolation. Docker is required for secure code execution.
|
||||
|
||||
SECURITY: This tool executes arbitrary code. Docker isolation is mandatory for
|
||||
untrusted code. The tool will fail if Docker is not available to prevent
|
||||
sandbox escape vulnerabilities.
|
||||
"""
|
||||
|
||||
import importlib.util
|
||||
import os
|
||||
import subprocess
|
||||
from types import ModuleType
|
||||
from typing import Any, ClassVar, TypedDict
|
||||
from typing import Any, TypedDict
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
from docker import ( # type: ignore[import-untyped]
|
||||
@@ -49,104 +51,23 @@ class CodeInterpreterSchema(BaseModel):
|
||||
)
|
||||
|
||||
|
||||
class SandboxPython:
|
||||
"""A restricted Python execution environment for running code safely.
|
||||
|
||||
This class provides methods to safely execute Python code by restricting access to
|
||||
potentially dangerous modules and built-in functions. It creates a sandboxed
|
||||
environment where harmful operations are blocked.
|
||||
"""
|
||||
|
||||
BLOCKED_MODULES: ClassVar[set[str]] = {
|
||||
"os",
|
||||
"sys",
|
||||
"subprocess",
|
||||
"shutil",
|
||||
"importlib",
|
||||
"inspect",
|
||||
"tempfile",
|
||||
"sysconfig",
|
||||
"builtins",
|
||||
}
|
||||
|
||||
UNSAFE_BUILTINS: ClassVar[set[str]] = {
|
||||
"exec",
|
||||
"eval",
|
||||
"open",
|
||||
"compile",
|
||||
"input",
|
||||
"globals",
|
||||
"locals",
|
||||
"vars",
|
||||
"help",
|
||||
"dir",
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def restricted_import(
|
||||
name: str,
|
||||
custom_globals: dict[str, Any] | None = None,
|
||||
custom_locals: dict[str, Any] | None = None,
|
||||
fromlist: list[str] | None = None,
|
||||
level: int = 0,
|
||||
) -> ModuleType:
|
||||
"""A restricted import function that blocks importing of unsafe modules.
|
||||
|
||||
Args:
|
||||
name: The name of the module to import.
|
||||
custom_globals: Global namespace to use.
|
||||
custom_locals: Local namespace to use.
|
||||
fromlist: List of items to import from the module.
|
||||
level: The level value passed to __import__.
|
||||
|
||||
Returns:
|
||||
The imported module if allowed.
|
||||
|
||||
Raises:
|
||||
ImportError: If the module is in the blocked modules list.
|
||||
"""
|
||||
if name in SandboxPython.BLOCKED_MODULES:
|
||||
raise ImportError(f"Importing '{name}' is not allowed.")
|
||||
return __import__(name, custom_globals, custom_locals, fromlist or (), level)
|
||||
|
||||
@staticmethod
|
||||
def safe_builtins() -> dict[str, Any]:
|
||||
"""Creates a dictionary of built-in functions with unsafe ones removed.
|
||||
|
||||
Returns:
|
||||
A dictionary of safe built-in functions and objects.
|
||||
"""
|
||||
import builtins
|
||||
|
||||
safe_builtins = {
|
||||
k: v
|
||||
for k, v in builtins.__dict__.items()
|
||||
if k not in SandboxPython.UNSAFE_BUILTINS
|
||||
}
|
||||
safe_builtins["__import__"] = SandboxPython.restricted_import
|
||||
return safe_builtins
|
||||
|
||||
@staticmethod
|
||||
def exec(code: str, locals_: dict[str, Any]) -> None:
|
||||
"""Executes Python code in a restricted environment.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
locals_: A dictionary that will be used for local variable storage.
|
||||
"""
|
||||
exec(code, {"__builtins__": SandboxPython.safe_builtins()}, locals_) # noqa: S102
|
||||
|
||||
|
||||
class CodeInterpreterTool(BaseTool):
|
||||
"""A tool for executing Python code in isolated environments.
|
||||
"""A tool for executing Python code in isolated Docker containers.
|
||||
|
||||
This tool provides functionality to run Python code either in a Docker container
|
||||
for safe isolation or directly in a restricted sandbox. It can handle installing
|
||||
Python packages and executing arbitrary Python code.
|
||||
This tool provides functionality to run Python code in a Docker container
|
||||
for safe isolation. Docker is required for secure code execution.
|
||||
|
||||
Security Model:
|
||||
- Docker container provides process, filesystem, and network isolation
|
||||
- Code execution fails if Docker is unavailable (fail-safe)
|
||||
- unsafe_mode bypasses all protections (use only in trusted environments)
|
||||
|
||||
For more information, see:
|
||||
https://docs.crewai.com/en/tools/ai-ml/codeinterpretertool#docker-container-recommended
|
||||
"""
|
||||
|
||||
name: str = "Code Interpreter"
|
||||
description: str = "Interprets Python3 code strings with a final print statement."
|
||||
description: str = "Interprets Python3 code strings with a final print statement. Requires Docker for secure execution."
|
||||
args_schema: type[BaseModel] = CodeInterpreterSchema
|
||||
default_image_tag: str = "code-interpreter:latest"
|
||||
code: str | None = None
|
||||
@@ -271,12 +192,10 @@ class CodeInterpreterTool(BaseTool):
|
||||
"""Checks if Docker is available and running on the system.
|
||||
|
||||
Attempts to run the 'docker info' command to verify Docker availability.
|
||||
Prints appropriate messages if Docker is not installed or not running.
|
||||
|
||||
Returns:
|
||||
True if Docker is available and running, False otherwise.
|
||||
"""
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["docker", "info"], # noqa: S607
|
||||
@@ -286,32 +205,44 @@ class CodeInterpreterTool(BaseTool):
|
||||
timeout=1,
|
||||
)
|
||||
return True
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired):
|
||||
Printer.print(
|
||||
"Docker is installed but not running or inaccessible.",
|
||||
color="bold_purple",
|
||||
)
|
||||
return False
|
||||
except FileNotFoundError:
|
||||
Printer.print("Docker is not installed", color="bold_purple")
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired, FileNotFoundError):
|
||||
return False
|
||||
|
||||
def run_code_safety(self, code: str, libraries_used: list[str]) -> str:
|
||||
"""Runs code in the safest available environment.
|
||||
"""Runs code in a Docker container for safe isolation.
|
||||
|
||||
Attempts to run code in Docker if available, falls back to a restricted
|
||||
sandbox if Docker is not available.
|
||||
Requires Docker to be installed and running. Fails with an error message
|
||||
if Docker is not available, preventing sandbox escape vulnerabilities.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
libraries_used: A list of Python library names to install before execution.
|
||||
|
||||
Returns:
|
||||
The output of the executed code as a string.
|
||||
The output of the executed code as a string, or an error message if
|
||||
Docker is not available.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If Docker is not available and code execution is attempted.
|
||||
"""
|
||||
if self._check_docker_available():
|
||||
return self.run_code_in_docker(code, libraries_used)
|
||||
return self.run_code_in_restricted_sandbox(code)
|
||||
if not self._check_docker_available():
|
||||
error_msg = (
|
||||
"SECURITY ERROR: Docker is required for safe code execution but is not available.\n\n"
|
||||
"Docker provides essential isolation to prevent sandbox escape attacks.\n"
|
||||
"Please install and start Docker, then try again.\n\n"
|
||||
"For installation instructions, see:\n"
|
||||
"- https://docs.docker.com/get-docker/\n"
|
||||
"- https://docs.crewai.com/en/tools/ai-ml/codeinterpretertool#docker-container-recommended\n\n"
|
||||
"If you are in a trusted environment and understand the risks, you can use unsafe_mode=True,\n"
|
||||
"but this is NOT recommended for production use or untrusted code."
|
||||
)
|
||||
Printer.print(error_msg, color="bold_red")
|
||||
raise RuntimeError(
|
||||
"Docker is required for safe code execution. "
|
||||
"Install Docker or use unsafe_mode=True (not recommended)."
|
||||
)
|
||||
|
||||
return self.run_code_in_docker(code, libraries_used)
|
||||
|
||||
def run_code_in_docker(self, code: str, libraries_used: list[str]) -> str:
|
||||
"""Runs Python code in a Docker container for safe isolation.
|
||||
@@ -340,34 +271,20 @@ class CodeInterpreterTool(BaseTool):
|
||||
return f"Something went wrong while running the code: \n{exec_result.output.decode('utf-8')}"
|
||||
return exec_result.output.decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def run_code_in_restricted_sandbox(code: str) -> str:
|
||||
"""Runs Python code in a restricted sandbox environment.
|
||||
|
||||
Executes the code with restricted access to potentially dangerous modules and
|
||||
built-in functions for basic safety when Docker is not available.
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
|
||||
Returns:
|
||||
The value of the 'result' variable from the executed code,
|
||||
or an error message if execution failed.
|
||||
"""
|
||||
Printer.print("Running code in restricted sandbox", color="yellow")
|
||||
exec_locals: dict[str, Any] = {}
|
||||
try:
|
||||
SandboxPython.exec(code=code, locals_=exec_locals)
|
||||
return exec_locals.get("result", "No result variable found.")
|
||||
except Exception as e:
|
||||
return f"An error occurred: {e!s}"
|
||||
|
||||
@staticmethod
|
||||
def run_code_unsafe(code: str, libraries_used: list[str]) -> str:
|
||||
"""Runs code directly on the host machine without any safety restrictions.
|
||||
|
||||
WARNING: This mode is unsafe and should only be used in trusted environments
|
||||
with code from trusted sources.
|
||||
WARNING: This mode bypasses all security controls and executes code directly
|
||||
on the host system. Use ONLY in trusted environments with trusted code.
|
||||
|
||||
SECURITY RISKS:
|
||||
- No process isolation
|
||||
- No filesystem restrictions
|
||||
- No network restrictions
|
||||
- Full access to host system resources
|
||||
- Potential for system compromise
|
||||
|
||||
Args:
|
||||
code: The Python code to execute as a string.
|
||||
@@ -377,12 +294,23 @@ class CodeInterpreterTool(BaseTool):
|
||||
The value of the 'result' variable from the executed code,
|
||||
or an error message if execution failed.
|
||||
"""
|
||||
Printer.print("WARNING: Running code in unsafe mode", color="bold_magenta")
|
||||
# Install libraries on the host machine
|
||||
for library in libraries_used:
|
||||
os.system(f"pip install {library}") # noqa: S605
|
||||
Printer.print(
|
||||
"⚠️ WARNING: Running code in UNSAFE mode - no security controls active!",
|
||||
color="bold_red",
|
||||
)
|
||||
|
||||
for library in libraries_used:
|
||||
try:
|
||||
subprocess.run(
|
||||
["pip", "install", library],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=30,
|
||||
)
|
||||
except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e:
|
||||
return f"Failed to install library '{library}': {e!s}"
|
||||
|
||||
# Execute the code
|
||||
try:
|
||||
exec_locals: dict[str, Any] = {}
|
||||
exec(code, {}, exec_locals) # noqa: S102
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import contextvars
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
@@ -138,9 +137,7 @@ class StagehandTool(BaseTool):
|
||||
- 'observe': For finding elements in a specific area
|
||||
"""
|
||||
args_schema: type[BaseModel] = StagehandToolSchema
|
||||
package_dependencies: list[str] = Field(
|
||||
default_factory=lambda: ["stagehand<=0.5.9"]
|
||||
)
|
||||
package_dependencies: list[str] = Field(default_factory=lambda: ["stagehand<=0.5.9"])
|
||||
env_vars: list[EnvVar] = Field(
|
||||
default_factory=lambda: [
|
||||
EnvVar(
|
||||
@@ -623,12 +620,9 @@ class StagehandTool(BaseTool):
|
||||
# We're in an existing event loop, use it
|
||||
import concurrent.futures
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(
|
||||
ctx.run,
|
||||
asyncio.run,
|
||||
self._async_run(instruction, url, command_type),
|
||||
asyncio.run, self._async_run(instruction, url, command_type)
|
||||
)
|
||||
result = future.result()
|
||||
else:
|
||||
@@ -712,12 +706,11 @@ class StagehandTool(BaseTool):
|
||||
if loop.is_running():
|
||||
import concurrent.futures
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
with (
|
||||
concurrent.futures.ThreadPoolExecutor() as executor
|
||||
):
|
||||
future = executor.submit(
|
||||
ctx.run, asyncio.run, self._async_close()
|
||||
asyncio.run, self._async_close()
|
||||
)
|
||||
future.result()
|
||||
else:
|
||||
|
||||
@@ -1,777 +1,80 @@
|
||||
import os
|
||||
from unittest.mock import MagicMock, patch
|
||||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
import requests as requests_lib
|
||||
|
||||
from crewai_tools.tools.brave_search_tool.base import BraveSearchToolBase
|
||||
from crewai_tools.tools.brave_search_tool.brave_web_tool import BraveWebSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_image_tool import BraveImageSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_news_tool import BraveNewsSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_video_tool import BraveVideoSearchTool
|
||||
from crewai_tools.tools.brave_search_tool.brave_llm_context_tool import (
|
||||
BraveLLMContextTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.brave_local_pois_tool import (
|
||||
BraveLocalPOIsTool,
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
)
|
||||
from crewai_tools.tools.brave_search_tool.schemas import (
|
||||
WebSearchParams,
|
||||
WebSearchHeaders,
|
||||
ImageSearchParams,
|
||||
ImageSearchHeaders,
|
||||
NewsSearchParams,
|
||||
NewsSearchHeaders,
|
||||
VideoSearchParams,
|
||||
VideoSearchHeaders,
|
||||
LLMContextParams,
|
||||
LLMContextHeaders,
|
||||
LocalPOIsParams,
|
||||
LocalPOIsHeaders,
|
||||
LocalPOIsDescriptionParams,
|
||||
LocalPOIsDescriptionHeaders,
|
||||
)
|
||||
|
||||
|
||||
def _mock_response(
|
||||
status_code: int = 200,
|
||||
json_data: dict | None = None,
|
||||
headers: dict | None = None,
|
||||
text: str = "",
|
||||
) -> MagicMock:
|
||||
"""Build a ``requests.Response``-like mock with the attributes used by ``_make_request``."""
|
||||
resp = MagicMock(spec=requests_lib.Response)
|
||||
resp.status_code = status_code
|
||||
resp.ok = 200 <= status_code < 400
|
||||
resp.url = "https://api.search.brave.com/res/v1/web/search?q=test"
|
||||
resp.text = text or (str(json_data) if json_data else "")
|
||||
resp.headers = headers or {}
|
||||
resp.json.return_value = json_data if json_data is not None else {}
|
||||
return resp
|
||||
|
||||
|
||||
# Fixtures
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _brave_env_and_rate_limit():
|
||||
"""Set BRAVE_API_KEY for every test. Rate limiting is per-instance (each tool starts with a fresh clock)."""
|
||||
with patch.dict(os.environ, {"BRAVE_API_KEY": "test-api-key"}):
|
||||
yield
|
||||
from crewai_tools.tools.brave_search_tool.brave_search_tool import BraveSearchTool
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def web_tool():
|
||||
return BraveWebSearchTool()
|
||||
def brave_tool():
|
||||
return BraveSearchTool(n_results=2)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def image_tool():
|
||||
return BraveImageSearchTool()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def news_tool():
|
||||
return BraveNewsSearchTool()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def video_tool():
|
||||
return BraveVideoSearchTool()
|
||||
|
||||
|
||||
# Initialization
|
||||
|
||||
ALL_TOOL_CLASSES = [
|
||||
BraveWebSearchTool,
|
||||
BraveImageSearchTool,
|
||||
BraveNewsSearchTool,
|
||||
BraveVideoSearchTool,
|
||||
BraveLLMContextTool,
|
||||
BraveLocalPOIsTool,
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool_cls", ALL_TOOL_CLASSES)
|
||||
def test_instantiation_with_env_var(tool_cls):
|
||||
"""Each tool can be created when BRAVE_API_KEY is in the environment."""
|
||||
tool = tool_cls()
|
||||
assert tool.api_key == "test-api-key"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tool_cls", ALL_TOOL_CLASSES)
|
||||
def test_instantiation_with_explicit_key(tool_cls):
|
||||
"""An explicit api_key takes precedence over the environment."""
|
||||
tool = tool_cls(api_key="explicit-key")
|
||||
assert tool.api_key == "explicit-key"
|
||||
|
||||
|
||||
def test_missing_api_key_raises():
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
with pytest.raises(ValueError, match="BRAVE_API_KEY"):
|
||||
BraveWebSearchTool()
|
||||
|
||||
|
||||
def test_default_attributes():
|
||||
tool = BraveWebSearchTool()
|
||||
assert tool.save_file is False
|
||||
def test_brave_tool_initialization():
|
||||
tool = BraveSearchTool()
|
||||
assert tool.n_results == 10
|
||||
assert tool._timeout == 30
|
||||
assert tool._requests_per_second == 1.0
|
||||
assert tool.raw is False
|
||||
assert tool.save_file is False
|
||||
|
||||
|
||||
def test_custom_constructor_args():
|
||||
tool = BraveWebSearchTool(
|
||||
save_file=True,
|
||||
timeout=60,
|
||||
n_results=5,
|
||||
requests_per_second=0.5,
|
||||
raw=True,
|
||||
)
|
||||
assert tool.save_file is True
|
||||
assert tool._timeout == 60
|
||||
assert tool.n_results == 5
|
||||
assert tool._requests_per_second == 0.5
|
||||
assert tool.raw is True
|
||||
|
||||
|
||||
# Headers
|
||||
|
||||
|
||||
def test_default_headers():
|
||||
tool = BraveWebSearchTool()
|
||||
assert tool.headers["x-subscription-token"] == "test-api-key"
|
||||
assert tool.headers["accept"] == "application/json"
|
||||
|
||||
|
||||
def test_set_headers_merges_and_normalizes():
|
||||
tool = BraveWebSearchTool()
|
||||
tool.set_headers({"Cache-Control": "no-cache"})
|
||||
assert tool.headers["cache-control"] == "no-cache"
|
||||
assert tool.headers["x-subscription-token"] == "test-api-key"
|
||||
|
||||
|
||||
def test_set_headers_returns_self_for_chaining():
|
||||
tool = BraveWebSearchTool()
|
||||
assert tool.set_headers({"Cache-Control": "no-cache"}) is tool
|
||||
|
||||
|
||||
def test_invalid_header_value_raises():
|
||||
tool = BraveImageSearchTool()
|
||||
with pytest.raises(ValueError, match="Invalid headers"):
|
||||
tool.set_headers({"Accept": "text/xml"})
|
||||
|
||||
|
||||
# Endpoint & Schema Wiring
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"tool_cls, expected_url, expected_params, expected_headers",
|
||||
[
|
||||
(
|
||||
BraveWebSearchTool,
|
||||
"https://api.search.brave.com/res/v1/web/search",
|
||||
WebSearchParams,
|
||||
WebSearchHeaders,
|
||||
),
|
||||
(
|
||||
BraveImageSearchTool,
|
||||
"https://api.search.brave.com/res/v1/images/search",
|
||||
ImageSearchParams,
|
||||
ImageSearchHeaders,
|
||||
),
|
||||
(
|
||||
BraveNewsSearchTool,
|
||||
"https://api.search.brave.com/res/v1/news/search",
|
||||
NewsSearchParams,
|
||||
NewsSearchHeaders,
|
||||
),
|
||||
(
|
||||
BraveVideoSearchTool,
|
||||
"https://api.search.brave.com/res/v1/videos/search",
|
||||
VideoSearchParams,
|
||||
VideoSearchHeaders,
|
||||
),
|
||||
(
|
||||
BraveLLMContextTool,
|
||||
"https://api.search.brave.com/res/v1/llm/context",
|
||||
LLMContextParams,
|
||||
LLMContextHeaders,
|
||||
),
|
||||
(
|
||||
BraveLocalPOIsTool,
|
||||
"https://api.search.brave.com/res/v1/local/pois",
|
||||
LocalPOIsParams,
|
||||
LocalPOIsHeaders,
|
||||
),
|
||||
(
|
||||
BraveLocalPOIsDescriptionTool,
|
||||
"https://api.search.brave.com/res/v1/local/descriptions",
|
||||
LocalPOIsDescriptionParams,
|
||||
LocalPOIsDescriptionHeaders,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_tool_wiring(tool_cls, expected_url, expected_params, expected_headers):
|
||||
tool = tool_cls()
|
||||
assert tool.search_url == expected_url
|
||||
assert tool.args_schema is expected_params
|
||||
assert tool.header_schema is expected_headers
|
||||
|
||||
|
||||
# Payload Refinement (e.g., `query` -> `q`, `count` fallback, param pass-through)
|
||||
|
||||
|
||||
def test_web_refine_request_payload_passes_all_params(web_tool):
|
||||
params = web_tool._common_payload_refinement(
|
||||
{
|
||||
"query": "test",
|
||||
"country": "US",
|
||||
"search_lang": "en",
|
||||
"count": 5,
|
||||
"offset": 2,
|
||||
"safesearch": "moderate",
|
||||
"freshness": "pw",
|
||||
}
|
||||
)
|
||||
refined_params = web_tool._refine_request_payload(params)
|
||||
|
||||
assert refined_params["q"] == "test"
|
||||
assert "query" not in refined_params
|
||||
assert refined_params["count"] == 5
|
||||
assert refined_params["country"] == "US"
|
||||
assert refined_params["search_lang"] == "en"
|
||||
assert refined_params["offset"] == 2
|
||||
assert refined_params["safesearch"] == "moderate"
|
||||
assert refined_params["freshness"] == "pw"
|
||||
|
||||
|
||||
def test_image_refine_request_payload_passes_all_params(image_tool):
|
||||
params = image_tool._common_payload_refinement(
|
||||
{
|
||||
"query": "cat photos",
|
||||
"country": "US",
|
||||
"search_lang": "en",
|
||||
"safesearch": "strict",
|
||||
"count": 50,
|
||||
"spellcheck": True,
|
||||
}
|
||||
)
|
||||
refined_params = image_tool._refine_request_payload(params)
|
||||
|
||||
assert refined_params["q"] == "cat photos"
|
||||
assert "query" not in refined_params
|
||||
assert refined_params["country"] == "US"
|
||||
assert refined_params["safesearch"] == "strict"
|
||||
assert refined_params["count"] == 50
|
||||
assert refined_params["spellcheck"] is True
|
||||
|
||||
|
||||
def test_news_refine_request_payload_passes_all_params(news_tool):
|
||||
params = news_tool._common_payload_refinement(
|
||||
{
|
||||
"query": "breaking news",
|
||||
"country": "US",
|
||||
"count": 10,
|
||||
"offset": 1,
|
||||
"freshness": "pd",
|
||||
"extra_snippets": True,
|
||||
}
|
||||
)
|
||||
refined_params = news_tool._refine_request_payload(params)
|
||||
|
||||
assert refined_params["q"] == "breaking news"
|
||||
assert "query" not in refined_params
|
||||
assert refined_params["country"] == "US"
|
||||
assert refined_params["offset"] == 1
|
||||
assert refined_params["freshness"] == "pd"
|
||||
assert refined_params["extra_snippets"] is True
|
||||
|
||||
|
||||
def test_video_refine_request_payload_passes_all_params(video_tool):
|
||||
params = video_tool._common_payload_refinement(
|
||||
{
|
||||
"query": "tutorial",
|
||||
"country": "US",
|
||||
"count": 25,
|
||||
"offset": 0,
|
||||
"safesearch": "strict",
|
||||
"freshness": "pm",
|
||||
}
|
||||
)
|
||||
refined_params = video_tool._refine_request_payload(params)
|
||||
|
||||
assert refined_params["q"] == "tutorial"
|
||||
assert "query" not in refined_params
|
||||
assert refined_params["country"] == "US"
|
||||
assert refined_params["offset"] == 0
|
||||
assert refined_params["freshness"] == "pm"
|
||||
|
||||
|
||||
def test_legacy_constructor_params_flow_into_query_params():
|
||||
"""The legacy n_results and country constructor params are applied as defaults
|
||||
when count/country are not explicitly provided at call time."""
|
||||
tool = BraveWebSearchTool(n_results=3, country="BR")
|
||||
params = tool._common_payload_refinement({"query": "test"})
|
||||
|
||||
assert params["count"] == 3
|
||||
assert params["country"] == "BR"
|
||||
|
||||
|
||||
def test_legacy_constructor_params_do_not_override_explicit_query_params():
|
||||
"""Explicit query-time count/country take precedence over constructor defaults."""
|
||||
tool = BraveWebSearchTool(n_results=3, country="BR")
|
||||
params = tool._common_payload_refinement(
|
||||
{"query": "test", "count": 10, "country": "US"}
|
||||
)
|
||||
|
||||
assert params["count"] == 10
|
||||
assert params["country"] == "US"
|
||||
|
||||
|
||||
def test_refine_request_payload_passes_multiple_goggles_as_multiple_params(web_tool):
|
||||
result = web_tool._refine_request_payload(
|
||||
{
|
||||
"query": "test",
|
||||
"goggles": ["goggle1", "goggle2"],
|
||||
}
|
||||
)
|
||||
assert result["goggles"] == ["goggle1", "goggle2"]
|
||||
|
||||
|
||||
# Null-like / empty value stripping
|
||||
#
|
||||
# crewAI's ensure_all_properties_required (pydantic_schema_utils.py) marks
|
||||
# every schema property as required for OpenAI strict-mode compatibility.
|
||||
# Because optional Brave API parameters look required to the LLM, it fills
|
||||
# them with placeholder junk — None, "", "null", or []. The test below
|
||||
# verifies that _common_payload_refinement strips these from optional fields.
|
||||
|
||||
|
||||
def test_common_refinement_strips_null_like_values(web_tool):
|
||||
"""_common_payload_refinement drops optional keys with None / '' / 'null' / []."""
|
||||
params = web_tool._common_payload_refinement(
|
||||
{
|
||||
"query": "test",
|
||||
"country": "US",
|
||||
"search_lang": "",
|
||||
"freshness": "null",
|
||||
"count": 5,
|
||||
"goggles": [],
|
||||
}
|
||||
)
|
||||
assert params["q"] == "test"
|
||||
assert params["country"] == "US"
|
||||
assert params["count"] == 5
|
||||
assert "search_lang" not in params
|
||||
assert "freshness" not in params
|
||||
assert "goggles" not in params
|
||||
|
||||
|
||||
# End-to-End _run() with Mocked HTTP Response
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_web_search_end_to_end(mock_get, web_tool):
|
||||
web_tool.raw = True
|
||||
data = {"web": {"results": [{"title": "R", "url": "http://r.co"}]}}
|
||||
mock_get.return_value = _mock_response(json_data=data)
|
||||
|
||||
result = web_tool._run(query="test")
|
||||
|
||||
mock_get.assert_called_once()
|
||||
call_args = mock_get.call_args.kwargs
|
||||
assert call_args["params"]["q"] == "test"
|
||||
assert call_args["headers"]["x-subscription-token"] == "test-api-key"
|
||||
assert result == data
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_image_search_end_to_end(mock_get, image_tool):
|
||||
image_tool.raw = True
|
||||
data = {"results": [{"url": "http://img.co/a.jpg"}]}
|
||||
mock_get.return_value = _mock_response(json_data=data)
|
||||
|
||||
assert image_tool._run(query="cats") == data
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_news_search_end_to_end(mock_get, news_tool):
|
||||
news_tool.raw = True
|
||||
data = {"results": [{"title": "News", "url": "http://n.co"}]}
|
||||
mock_get.return_value = _mock_response(json_data=data)
|
||||
|
||||
assert news_tool._run(query="headlines") == data
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_video_search_end_to_end(mock_get, video_tool):
|
||||
video_tool.raw = True
|
||||
data = {"results": [{"title": "Vid", "url": "http://v.co"}]}
|
||||
mock_get.return_value = _mock_response(json_data=data)
|
||||
|
||||
assert video_tool._run(query="python tutorial") == data
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_raw_false_calls_refine_response(mock_get, web_tool):
|
||||
"""With raw=False (the default), _refine_response transforms the API response."""
|
||||
api_response = {
|
||||
@patch("requests.get")
|
||||
def test_brave_tool_search(mock_get, brave_tool):
|
||||
mock_response = {
|
||||
"web": {
|
||||
"results": [
|
||||
{
|
||||
"title": "CrewAI",
|
||||
"url": "https://crewai.com",
|
||||
"description": "AI agent framework",
|
||||
"title": "Test Title",
|
||||
"url": "http://test.com",
|
||||
"description": "Test Description",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
mock_get.return_value = _mock_response(json_data=api_response)
|
||||
|
||||
assert web_tool.raw is False
|
||||
result = web_tool._run(query="crewai")
|
||||
|
||||
# The web tool's _refine_response extracts and reshapes results.
|
||||
# The key assertion: we should NOT get back the raw API envelope.
|
||||
assert result != api_response
|
||||
|
||||
|
||||
# Backward Compatibility & Legacy Parameter Support
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_positional_query_argument(mock_get, web_tool):
|
||||
"""tool.run('my query') works as a positional argument."""
|
||||
mock_get.return_value = _mock_response(json_data={})
|
||||
|
||||
web_tool._run("positional test")
|
||||
|
||||
assert mock_get.call_args.kwargs["params"]["q"] == "positional test"
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_search_query_backward_compat(mock_get, web_tool):
|
||||
"""The legacy 'search_query' param is mapped to 'query'."""
|
||||
mock_get.return_value = _mock_response(json_data={})
|
||||
|
||||
web_tool._run(search_query="legacy test")
|
||||
|
||||
assert mock_get.call_args.kwargs["params"]["q"] == "legacy test"
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base._save_results_to_file")
|
||||
def test_save_file_called_when_enabled(mock_save, mock_get):
|
||||
mock_get.return_value = _mock_response(json_data={"results": []})
|
||||
|
||||
tool = BraveWebSearchTool(save_file=True)
|
||||
tool._run(query="test")
|
||||
|
||||
mock_save.assert_called_once()
|
||||
|
||||
|
||||
# Error Handling
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_connection_error_raises_runtime_error(mock_get, web_tool):
|
||||
mock_get.side_effect = requests_lib.exceptions.ConnectionError("refused")
|
||||
with pytest.raises(RuntimeError, match="Brave Search API connection failed"):
|
||||
web_tool._run(query="test")
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_timeout_raises_runtime_error(mock_get, web_tool):
|
||||
mock_get.side_effect = requests_lib.exceptions.Timeout("timed out")
|
||||
with pytest.raises(RuntimeError, match="timed out"):
|
||||
web_tool._run(query="test")
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_invalid_params_raises_value_error(mock_get, web_tool):
|
||||
"""count=999 exceeds WebSearchParams.count le=20."""
|
||||
with pytest.raises(ValueError, match="Invalid parameters"):
|
||||
web_tool._run(query="test", count=999)
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_4xx_error_raises_with_api_detail(mock_get, web_tool):
|
||||
"""A 422 with a structured error body includes code and detail in the message."""
|
||||
mock_get.return_value = _mock_response(
|
||||
status_code=422,
|
||||
json_data={
|
||||
"error": {
|
||||
"id": "abc-123",
|
||||
"status": 422,
|
||||
"code": "OPTION_NOT_IN_PLAN",
|
||||
"detail": "extra_snippets requires a Pro plan",
|
||||
}
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="OPTION_NOT_IN_PLAN") as exc_info:
|
||||
web_tool._run(query="test")
|
||||
assert "extra_snippets requires a Pro plan" in str(exc_info.value)
|
||||
assert "HTTP 422" in str(exc_info.value)
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_auth_error_raises_immediately(mock_get, web_tool):
|
||||
"""A 401 with SUBSCRIPTION_TOKEN_INVALID is not retried."""
|
||||
mock_get.return_value = _mock_response(
|
||||
status_code=401,
|
||||
json_data={
|
||||
"error": {
|
||||
"id": "xyz",
|
||||
"status": 401,
|
||||
"code": "SUBSCRIPTION_TOKEN_INVALID",
|
||||
"detail": "The subscription token is invalid",
|
||||
}
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="SUBSCRIPTION_TOKEN_INVALID"):
|
||||
web_tool._run(query="test")
|
||||
# Should NOT have retried — only one call.
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_quota_limited_429_raises_immediately(mock_get, web_tool):
|
||||
"""A 429 with QUOTA_LIMITED is NOT retried — quota exhaustion is terminal."""
|
||||
mock_get.return_value = _mock_response(
|
||||
status_code=429,
|
||||
json_data={
|
||||
"error": {
|
||||
"id": "ql-1",
|
||||
"status": 429,
|
||||
"code": "QUOTA_LIMITED",
|
||||
"detail": "Monthly quota exceeded",
|
||||
}
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="QUOTA_LIMITED") as exc_info:
|
||||
web_tool._run(query="test")
|
||||
assert "Monthly quota exceeded" in str(exc_info.value)
|
||||
# Terminal — only one HTTP call, no retries.
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_usage_limit_exceeded_429_raises_immediately(mock_get, web_tool):
|
||||
"""USAGE_LIMIT_EXCEEDED is also non-retryable, just like QUOTA_LIMITED."""
|
||||
mock_get.return_value = _mock_response(
|
||||
status_code=429,
|
||||
json_data={
|
||||
"error": {
|
||||
"id": "ule-1",
|
||||
"status": 429,
|
||||
"code": "USAGE_LIMIT_EXCEEDED",
|
||||
}
|
||||
},
|
||||
text="usage limit exceeded",
|
||||
)
|
||||
with pytest.raises(RuntimeError, match="USAGE_LIMIT_EXCEEDED"):
|
||||
web_tool._run(query="test")
|
||||
assert mock_get.call_count == 1
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_error_body_is_fully_included_in_message(mock_get, web_tool):
|
||||
"""The full JSON error body is included in the RuntimeError message."""
|
||||
mock_get.return_value = _mock_response(
|
||||
status_code=429,
|
||||
json_data={
|
||||
"error": {
|
||||
"id": "x",
|
||||
"status": 429,
|
||||
"code": "QUOTA_LIMITED",
|
||||
"detail": "Exceeded",
|
||||
"meta": {"plan": "free", "limit": 1000},
|
||||
}
|
||||
},
|
||||
)
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
web_tool._run(query="test")
|
||||
msg = str(exc_info.value)
|
||||
assert "HTTP 429" in msg
|
||||
assert "QUOTA_LIMITED" in msg
|
||||
assert "free" in msg
|
||||
assert "1000" in msg
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_error_without_json_body_falls_back_to_text(mock_get, web_tool):
|
||||
"""When the error response isn't valid JSON, resp.text is used as the detail."""
|
||||
resp = _mock_response(status_code=500, text="Internal Server Error")
|
||||
resp.json.side_effect = ValueError("No JSON")
|
||||
mock_get.return_value = resp
|
||||
|
||||
with pytest.raises(RuntimeError, match="Internal Server Error"):
|
||||
web_tool._run(query="test")
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
def test_invalid_json_on_success_raises_runtime_error(mock_get, web_tool):
|
||||
"""A 200 OK with a non-JSON body raises RuntimeError."""
|
||||
resp = _mock_response(status_code=200)
|
||||
resp.json.side_effect = ValueError("Expecting value")
|
||||
mock_get.return_value = resp
|
||||
|
||||
with pytest.raises(RuntimeError, match="invalid JSON"):
|
||||
web_tool._run(query="test")
|
||||
|
||||
|
||||
# Rate Limiting
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_rate_limit_sleeps_when_too_fast(mock_time, mock_get, web_tool):
|
||||
"""Back-to-back calls within the interval trigger a sleep."""
|
||||
mock_get.return_value = _mock_response(json_data={})
|
||||
|
||||
# Simulate: last request was at t=100, "now" is t=100.2 (only 0.2s elapsed).
|
||||
# With default 1 req/s the min interval is 1.0s, so it should sleep ~0.8s.
|
||||
mock_time.time.return_value = 100.2
|
||||
web_tool._last_request_time = 100.0
|
||||
|
||||
web_tool._run(query="test")
|
||||
|
||||
mock_time.sleep.assert_called_once()
|
||||
sleep_duration = mock_time.sleep.call_args[0][0]
|
||||
assert 0.7 < sleep_duration < 0.9 # approximately 0.8s
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_rate_limit_skips_sleep_when_enough_time_passed(mock_time, mock_get, web_tool):
|
||||
"""No sleep when the elapsed time already exceeds the interval."""
|
||||
mock_get.return_value = _mock_response(json_data={})
|
||||
|
||||
# Last request was at t=100, "now" is t=102 (2s elapsed > 1s interval).
|
||||
mock_time.time.return_value = 102.0
|
||||
web_tool._last_request_time = 100.0
|
||||
|
||||
web_tool._run(query="test")
|
||||
|
||||
mock_time.sleep.assert_not_called()
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_rate_limit_disabled_when_zero(mock_time, mock_get, web_tool):
|
||||
"""requests_per_second=0 disables rate limiting entirely."""
|
||||
mock_get.return_value = _mock_response(json_data={})
|
||||
|
||||
web_tool._last_request_time = 100.0
|
||||
mock_time.time.return_value = 100.0 # same instant
|
||||
|
||||
web_tool._run(query="test")
|
||||
|
||||
mock_time.sleep.assert_not_called()
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_rate_limit_per_instance_independent(mock_time, mock_get, web_tool, image_tool):
|
||||
"""Each instance has its own rate-limit clock; a request on one does not delay the other."""
|
||||
mock_get.return_value = _mock_response(json_data={})
|
||||
|
||||
# Web tool fires at t=100 (its clock goes 0 -> 100).
|
||||
mock_time.time.return_value = 100.0
|
||||
web_tool._run(query="test")
|
||||
|
||||
# Image tool fires at t=100.3. Its clock is still 0 (separate instance), so
|
||||
# next_allowed = 1.0 and 100.3 > 1.0 — no sleep. Total process rate can be sum of instance limits.
|
||||
mock_time.time.return_value = 100.3
|
||||
image_tool._run(query="cats")
|
||||
|
||||
mock_time.sleep.assert_not_called()
|
||||
|
||||
|
||||
# Retry Behavior
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_429_rate_limited_retries_then_succeeds(mock_time, mock_get, web_tool):
|
||||
"""A transient RATE_LIMITED 429 is retried; success on the second attempt."""
|
||||
mock_time.time.return_value = 200.0
|
||||
|
||||
resp_429 = _mock_response(
|
||||
status_code=429,
|
||||
json_data={"error": {"id": "r", "status": 429, "code": "RATE_LIMITED"}},
|
||||
headers={"Retry-After": "2"},
|
||||
)
|
||||
resp_200 = _mock_response(status_code=200, json_data={"web": {"results": []}})
|
||||
mock_get.side_effect = [resp_429, resp_200]
|
||||
|
||||
web_tool.raw = True
|
||||
result = web_tool._run(query="test")
|
||||
|
||||
assert result == {"web": {"results": []}}
|
||||
assert mock_get.call_count == 2
|
||||
# Slept for the Retry-After value.
|
||||
retry_sleeps = [c for c in mock_time.sleep.call_args_list if c[0][0] == 2.0]
|
||||
assert len(retry_sleeps) == 1
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_5xx_is_retried(mock_time, mock_get, web_tool):
|
||||
"""A 502 server error is retried; success on the second attempt."""
|
||||
mock_time.time.return_value = 200.0
|
||||
|
||||
resp_502 = _mock_response(status_code=502, text="Bad Gateway")
|
||||
resp_502.json.side_effect = ValueError("no json")
|
||||
resp_200 = _mock_response(status_code=200, json_data={"web": {"results": []}})
|
||||
mock_get.side_effect = [resp_502, resp_200]
|
||||
|
||||
web_tool.raw = True
|
||||
result = web_tool._run(query="test")
|
||||
|
||||
assert result == {"web": {"results": []}}
|
||||
assert mock_get.call_count == 2
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_429_rate_limited_exhausts_retries(mock_time, mock_get, web_tool):
|
||||
"""Persistent RATE_LIMITED 429s exhaust retries and raise RuntimeError."""
|
||||
mock_time.time.return_value = 200.0
|
||||
|
||||
resp_429 = _mock_response(
|
||||
status_code=429,
|
||||
json_data={"error": {"id": "r", "status": 429, "code": "RATE_LIMITED"}},
|
||||
)
|
||||
mock_get.return_value = resp_429
|
||||
|
||||
with pytest.raises(RuntimeError, match="RATE_LIMITED"):
|
||||
web_tool._run(query="test")
|
||||
# 3 attempts (default _max_retries).
|
||||
assert mock_get.call_count == 3
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.requests.get")
|
||||
@patch("crewai_tools.tools.brave_search_tool.base.time")
|
||||
def test_retry_uses_exponential_backoff_when_no_retry_after(
|
||||
mock_time, mock_get, web_tool
|
||||
):
|
||||
"""Without Retry-After, backoff is 2^attempt (1s, 2s, ...)."""
|
||||
mock_time.time.return_value = 200.0
|
||||
|
||||
resp_503 = _mock_response(status_code=503, text="Service Unavailable")
|
||||
resp_503.json.side_effect = ValueError("no json")
|
||||
resp_200 = _mock_response(status_code=200, json_data={"ok": True})
|
||||
mock_get.side_effect = [resp_503, resp_503, resp_200]
|
||||
|
||||
web_tool.raw = True
|
||||
web_tool._run(query="test")
|
||||
|
||||
# Two retries: attempt 0 → sleep(1.0), attempt 1 → sleep(2.0).
|
||||
retry_sleeps = [c[0][0] for c in mock_time.sleep.call_args_list]
|
||||
assert 1.0 in retry_sleeps
|
||||
assert 2.0 in retry_sleeps
|
||||
mock_get.return_value.json.return_value = mock_response
|
||||
|
||||
result = brave_tool.run(query="test")
|
||||
data = json.loads(result)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1
|
||||
assert data[0]["title"] == "Test Title"
|
||||
assert data[0]["url"] == "http://test.com"
|
||||
|
||||
|
||||
@patch("requests.get")
|
||||
def test_brave_tool(mock_get):
|
||||
mock_response = {
|
||||
"web": {
|
||||
"results": [
|
||||
{
|
||||
"title": "Brave Browser",
|
||||
"url": "https://brave.com",
|
||||
"description": "Brave Browser description",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
mock_get.return_value.json.return_value = mock_response
|
||||
|
||||
tool = BraveSearchTool(n_results=2)
|
||||
result = tool.run(query="Brave Browser")
|
||||
assert result is not None
|
||||
|
||||
# Parse JSON so we can examine the structure
|
||||
data = json.loads(result)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) >= 1
|
||||
|
||||
# First item should have expected fields: title, url, and description
|
||||
first = data[0]
|
||||
assert "title" in first
|
||||
assert first["title"] == "Brave Browser"
|
||||
assert "url" in first
|
||||
assert first["url"] == "https://brave.com"
|
||||
assert "description" in first
|
||||
assert first["description"] == "Brave Browser description"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
test_brave_tool()
|
||||
test_brave_tool_initialization()
|
||||
# test_brave_tool_search(brave_tool)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import subprocess
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai_tools.tools.code_interpreter_tool.code_interpreter_tool import (
|
||||
CodeInterpreterTool,
|
||||
SandboxPython,
|
||||
)
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -76,99 +77,91 @@ print("This is line 2")"""
|
||||
)
|
||||
|
||||
|
||||
def test_restricted_sandbox_basic_code_execution(printer_mock, docker_unavailable_mock):
|
||||
"""Test basic code execution."""
|
||||
def test_docker_unavailable_fails_safely(printer_mock, docker_unavailable_mock):
|
||||
"""Test that code execution fails when Docker is unavailable."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = """
|
||||
result = 2 + 2
|
||||
print(result)
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
assert result == 4
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
tool.run(code=code, libraries_used=[])
|
||||
|
||||
assert "Docker is required for safe code execution" in str(exc_info.value)
|
||||
assert printer_mock.called
|
||||
call_args = printer_mock.call_args
|
||||
assert "SECURITY ERROR" in call_args[0][0]
|
||||
assert call_args[1]["color"] == "bold_red"
|
||||
|
||||
|
||||
def test_restricted_sandbox_running_with_blocked_modules(
|
||||
printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test that restricted modules cannot be imported."""
|
||||
def test_docker_unavailable_suggests_unsafe_mode(printer_mock, docker_unavailable_mock):
|
||||
"""Test that error message suggests unsafe_mode as alternative."""
|
||||
tool = CodeInterpreterTool()
|
||||
restricted_modules = SandboxPython.BLOCKED_MODULES
|
||||
code = "result = 1 + 1"
|
||||
|
||||
for module in restricted_modules:
|
||||
code = f"""
|
||||
import {module}
|
||||
result = "Import succeeded"
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
with pytest.raises(RuntimeError) as exc_info:
|
||||
tool.run(code=code, libraries_used=[])
|
||||
|
||||
assert f"An error occurred: Importing '{module}' is not allowed" in result
|
||||
|
||||
|
||||
def test_restricted_sandbox_running_with_blocked_builtins(
|
||||
printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test that restricted builtins are not available."""
|
||||
tool = CodeInterpreterTool()
|
||||
restricted_builtins = SandboxPython.UNSAFE_BUILTINS
|
||||
|
||||
for builtin in restricted_builtins:
|
||||
code = f"""
|
||||
{builtin}("test")
|
||||
result = "Builtin available"
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
assert f"An error occurred: name '{builtin}' is not defined" in result
|
||||
|
||||
|
||||
def test_restricted_sandbox_running_with_no_result_variable(
|
||||
printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test behavior when no result variable is set."""
|
||||
tool = CodeInterpreterTool()
|
||||
code = """
|
||||
x = 10
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"Running code in restricted sandbox", color="yellow"
|
||||
)
|
||||
assert result == "No result variable found."
|
||||
error_output = printer_mock.call_args[0][0]
|
||||
assert "unsafe_mode=True" in error_output
|
||||
assert "NOT recommended" in error_output
|
||||
assert "docs.crewai.com" in error_output
|
||||
|
||||
|
||||
def test_unsafe_mode_running_with_no_result_variable(
|
||||
printer_mock, docker_unavailable_mock
|
||||
):
|
||||
"""Test behavior when no result variable is set."""
|
||||
"""Test behavior when no result variable is set in unsafe mode."""
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
code = """
|
||||
x = 10
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"WARNING: Running code in unsafe mode", color="bold_magenta"
|
||||
"⚠️ WARNING: Running code in UNSAFE mode - no security controls active!",
|
||||
color="bold_red",
|
||||
)
|
||||
assert result == "No result variable found."
|
||||
|
||||
|
||||
def test_unsafe_mode_running_unsafe_code(printer_mock, docker_unavailable_mock):
|
||||
"""Test behavior when no result variable is set."""
|
||||
"""Test that unsafe mode allows unrestricted code execution."""
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
code = """
|
||||
import os
|
||||
os.system("ls -la")
|
||||
result = eval("5/1")
|
||||
"""
|
||||
result = tool.run(code=code, libraries_used=[])
|
||||
printer_mock.assert_called_with(
|
||||
"WARNING: Running code in unsafe mode", color="bold_magenta"
|
||||
"⚠️ WARNING: Running code in UNSAFE mode - no security controls active!",
|
||||
color="bold_red",
|
||||
)
|
||||
assert 5.0 == result
|
||||
|
||||
|
||||
@patch("crewai_tools.tools.code_interpreter_tool.code_interpreter_tool.subprocess.run")
|
||||
def test_unsafe_mode_library_installation(subprocess_mock, printer_mock, docker_unavailable_mock):
|
||||
"""Test that unsafe mode properly installs libraries using subprocess."""
|
||||
tool = CodeInterpreterTool(unsafe_mode=True)
|
||||
code = "result = 42"
|
||||
libraries = ["numpy", "pandas"]
|
||||
|
||||
subprocess_mock.return_value = None
|
||||
|
||||
tool.run(code=code, libraries_used=libraries)
|
||||
|
||||
assert subprocess_mock.call_count == 2
|
||||
subprocess_mock.assert_any_call(
|
||||
["pip", "install", "numpy"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=30,
|
||||
)
|
||||
subprocess_mock.assert_any_call(
|
||||
["pip", "install", "pandas"],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -53,7 +53,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.10.2a1",
|
||||
"crewai-tools==1.10.1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import contextvars
|
||||
import threading
|
||||
from typing import Any
|
||||
import urllib.request
|
||||
@@ -41,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.10.2a1"
|
||||
__version__ = "1.10.1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
@@ -67,8 +66,7 @@ def _track_install() -> None:
|
||||
def _track_install_async() -> None:
|
||||
"""Track installation in background thread to avoid blocking imports."""
|
||||
if not Telemetry._is_telemetry_disabled():
|
||||
ctx = contextvars.copy_context()
|
||||
thread = threading.Thread(target=ctx.run, args=(_track_install,), daemon=True)
|
||||
thread = threading.Thread(target=_track_install, daemon=True)
|
||||
thread.start()
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import MutableMapping
|
||||
import concurrent.futures
|
||||
import contextvars
|
||||
from functools import lru_cache
|
||||
import ssl
|
||||
import time
|
||||
@@ -148,9 +147,8 @@ def fetch_agent_card(
|
||||
has_running_loop = False
|
||||
|
||||
if has_running_loop:
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
return pool.submit(ctx.run, asyncio.run, coro).result()
|
||||
return pool.submit(asyncio.run, coro).result()
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
@@ -217,9 +215,8 @@ def _fetch_agent_card_cached(
|
||||
has_running_loop = False
|
||||
|
||||
if has_running_loop:
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
return pool.submit(ctx.run, asyncio.run, coro).result()
|
||||
return pool.submit(asyncio.run, coro).result()
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import base64
|
||||
from collections.abc import AsyncIterator, Callable, MutableMapping
|
||||
import concurrent.futures
|
||||
from contextlib import asynccontextmanager
|
||||
import contextvars
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
import uuid
|
||||
@@ -230,9 +229,8 @@ def execute_a2a_delegation(
|
||||
has_running_loop = False
|
||||
|
||||
if has_running_loop:
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
return pool.submit(ctx.run, asyncio.run, coro).result()
|
||||
return pool.submit(asyncio.run, coro).result()
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine, Mapping
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import contextvars
|
||||
from functools import wraps
|
||||
import json
|
||||
from types import MethodType
|
||||
@@ -279,9 +278,7 @@ def _fetch_agent_cards_concurrently(
|
||||
max_workers = min(len(a2a_agents), 10)
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||||
futures = {
|
||||
executor.submit(
|
||||
contextvars.copy_context().run, _fetch_card_from_config, config
|
||||
): config
|
||||
executor.submit(_fetch_card_from_config, config): config
|
||||
for config in a2a_agents
|
||||
}
|
||||
for future in as_completed(futures):
|
||||
|
||||
@@ -2,7 +2,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine, Sequence
|
||||
import contextvars
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
@@ -514,13 +513,9 @@ class Agent(BaseAgent):
|
||||
"""
|
||||
import concurrent.futures
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(
|
||||
ctx.run,
|
||||
self._execute_without_timeout,
|
||||
task_prompt=task_prompt,
|
||||
task=task,
|
||||
self._execute_without_timeout, task_prompt=task_prompt, task=task
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -38,7 +38,7 @@ from crewai.utilities.string_utils import interpolate_only
|
||||
|
||||
|
||||
_SLUG_RE: Final[re.Pattern[str]] = re.compile(
|
||||
r"^(?:crewai-amp:)?[a-zA-Z0-9][a-zA-Z0-9_-]*(?:#[\w-]+)?$"
|
||||
r"^(?:crewai-amp:)?[a-zA-Z0-9][a-zA-Z0-9_-]*(?:#\w+)?$"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,8 +8,8 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import contextvars
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import inspect
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Literal, cast
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import contextvars
|
||||
import json
|
||||
from pathlib import Path
|
||||
import platform
|
||||
@@ -81,10 +80,7 @@ def run_chat() -> None:
|
||||
|
||||
# Start loading indicator
|
||||
loading_complete = threading.Event()
|
||||
ctx = contextvars.copy_context()
|
||||
loading_thread = threading.Thread(
|
||||
target=ctx.run, args=(show_loading, loading_complete)
|
||||
)
|
||||
loading_thread = threading.Thread(target=show_loading, args=(loading_complete,))
|
||||
loading_thread.start()
|
||||
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.2a1"
|
||||
"crewai[tools]==1.10.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.2a1"
|
||||
"crewai[tools]==1.10.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.2a1"
|
||||
"crewai[tools]==1.10.1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from collections.abc import Callable
|
||||
import contextvars
|
||||
from contextvars import ContextVar, Token
|
||||
from datetime import datetime
|
||||
import getpass
|
||||
@@ -510,8 +509,7 @@ def prompt_user_for_trace_viewing(timeout_seconds: int = 20) -> bool:
|
||||
# Handle all input-related errors silently
|
||||
result[0] = False
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
input_thread = threading.Thread(target=ctx.run, args=(get_input,), daemon=True)
|
||||
input_thread = threading.Thread(target=get_input, daemon=True)
|
||||
input_thread.start()
|
||||
input_thread.join(timeout=timeout_seconds)
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
from collections.abc import Callable, Coroutine
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
import inspect
|
||||
import json
|
||||
|
||||
@@ -17,7 +17,6 @@ from collections.abc import (
|
||||
ValuesView,
|
||||
)
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import contextvars
|
||||
import copy
|
||||
import enum
|
||||
import inspect
|
||||
@@ -498,52 +497,6 @@ class LockedListProxy(list, Generic[T]): # type: ignore[type-arg]
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._list)
|
||||
|
||||
def index(
|
||||
self, value: T, start: SupportsIndex = 0, stop: SupportsIndex | None = None
|
||||
) -> int: # type: ignore[override]
|
||||
if stop is None:
|
||||
return self._list.index(value, start)
|
||||
return self._list.index(value, start, stop)
|
||||
|
||||
def count(self, value: T) -> int:
|
||||
return self._list.count(value)
|
||||
|
||||
def sort(self, *, key: Any = None, reverse: bool = False) -> None:
|
||||
with self._lock:
|
||||
self._list.sort(key=key, reverse=reverse)
|
||||
|
||||
def reverse(self) -> None:
|
||||
with self._lock:
|
||||
self._list.reverse()
|
||||
|
||||
def copy(self) -> list[T]:
|
||||
return self._list.copy()
|
||||
|
||||
def __add__(self, other: list[T]) -> list[T]:
|
||||
return self._list + other
|
||||
|
||||
def __radd__(self, other: list[T]) -> list[T]:
|
||||
return other + self._list
|
||||
|
||||
def __iadd__(self, other: Iterable[T]) -> LockedListProxy[T]:
|
||||
with self._lock:
|
||||
self._list += list(other)
|
||||
return self
|
||||
|
||||
def __mul__(self, n: SupportsIndex) -> list[T]:
|
||||
return self._list * n
|
||||
|
||||
def __rmul__(self, n: SupportsIndex) -> list[T]:
|
||||
return self._list * n
|
||||
|
||||
def __imul__(self, n: SupportsIndex) -> LockedListProxy[T]:
|
||||
with self._lock:
|
||||
self._list *= n
|
||||
return self
|
||||
|
||||
def __reversed__(self) -> Iterator[T]:
|
||||
return reversed(self._list)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Compare based on the underlying list contents."""
|
||||
if isinstance(other, LockedListProxy):
|
||||
@@ -626,23 +579,6 @@ class LockedDictProxy(dict, Generic[T]): # type: ignore[type-arg]
|
||||
def __bool__(self) -> bool:
|
||||
return bool(self._dict)
|
||||
|
||||
def copy(self) -> dict[str, T]:
|
||||
return self._dict.copy()
|
||||
|
||||
def __or__(self, other: dict[str, T]) -> dict[str, T]:
|
||||
return self._dict | other
|
||||
|
||||
def __ror__(self, other: dict[str, T]) -> dict[str, T]:
|
||||
return other | self._dict
|
||||
|
||||
def __ior__(self, other: dict[str, T]) -> LockedDictProxy[T]:
|
||||
with self._lock:
|
||||
self._dict |= other
|
||||
return self
|
||||
|
||||
def __reversed__(self) -> Iterator[str]:
|
||||
return reversed(self._dict)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
"""Compare based on the underlying dict contents."""
|
||||
if isinstance(other, LockedDictProxy):
|
||||
@@ -684,10 +620,6 @@ class StateProxy(Generic[T]):
|
||||
if name in ("_proxy_state", "_proxy_lock"):
|
||||
object.__setattr__(self, name, value)
|
||||
else:
|
||||
if isinstance(value, LockedListProxy):
|
||||
value = value._list
|
||||
elif isinstance(value, LockedDictProxy):
|
||||
value = value._dict
|
||||
with object.__getattribute__(self, "_proxy_lock"):
|
||||
setattr(object.__getattribute__(self, "_proxy_state"), name, value)
|
||||
|
||||
@@ -1814,9 +1746,8 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
ctx = contextvars.copy_context()
|
||||
with ThreadPoolExecutor(max_workers=1) as pool:
|
||||
return pool.submit(ctx.run, asyncio.run, _run_flow()).result()
|
||||
return pool.submit(asyncio.run, _run_flow()).result()
|
||||
except RuntimeError:
|
||||
return asyncio.run(_run_flow())
|
||||
|
||||
@@ -2240,6 +2171,8 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
else:
|
||||
# Run sync methods in thread pool for isolation
|
||||
# This allows Agent.kickoff() to work synchronously inside Flow methods
|
||||
import contextvars
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
result = await asyncio.to_thread(ctx.run, method, *args, **kwargs)
|
||||
finally:
|
||||
@@ -2858,9 +2791,8 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
# Manual executor management to avoid shutdown(wait=True)
|
||||
# deadlock when the provider call outlives the timeout.
|
||||
executor = ThreadPoolExecutor(max_workers=1)
|
||||
ctx = contextvars.copy_context()
|
||||
future = executor.submit(
|
||||
ctx.run, provider.request_input, message, self, metadata
|
||||
provider.request_input, message, self, metadata
|
||||
)
|
||||
try:
|
||||
raw = future.result(timeout=timeout)
|
||||
|
||||
@@ -408,7 +408,7 @@ def human_feedback(
|
||||
emit=list(emit) if emit else None,
|
||||
default_outcome=default_outcome,
|
||||
metadata=metadata or {},
|
||||
llm=llm if isinstance(llm, str) else getattr(llm, "model", None),
|
||||
llm=llm if isinstance(llm, str) else None,
|
||||
)
|
||||
|
||||
# Determine effective provider:
|
||||
|
||||
@@ -72,8 +72,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
|
||||
def init_db(self) -> None:
|
||||
"""Create the necessary tables if they don't exist."""
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
# Main state table
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -137,7 +136,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
f"state_data must be either a Pydantic BaseModel or dict, got {type(state_data)}"
|
||||
)
|
||||
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO flow_states (
|
||||
@@ -164,7 +163,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
Returns:
|
||||
The most recent state as a dictionary, or None if no state exists
|
||||
"""
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT state_json
|
||||
@@ -214,7 +213,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
self.save_state(flow_uuid, context.method_name, state_data)
|
||||
|
||||
# Save pending feedback context
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
# Use INSERT OR REPLACE to handle re-triggering feedback on same flow
|
||||
conn.execute(
|
||||
"""
|
||||
@@ -249,7 +248,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
# Import here to avoid circular imports
|
||||
from crewai.flow.async_feedback.types import PendingFeedbackContext
|
||||
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.execute(
|
||||
"""
|
||||
SELECT state_json, context_json
|
||||
@@ -273,7 +272,7 @@ class SQLiteFlowPersistence(FlowPersistence):
|
||||
Args:
|
||||
flow_uuid: Unique identifier for the flow instance
|
||||
"""
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute(
|
||||
"""
|
||||
DELETE FROM pending_feedback
|
||||
|
||||
@@ -22,12 +22,7 @@ if TYPE_CHECKING:
|
||||
|
||||
try:
|
||||
from anthropic import Anthropic, AsyncAnthropic, transform_schema
|
||||
from anthropic.types import (
|
||||
Message,
|
||||
TextBlock,
|
||||
ThinkingBlock,
|
||||
ToolUseBlock,
|
||||
)
|
||||
from anthropic.types import Message, TextBlock, ThinkingBlock, ToolUseBlock
|
||||
from anthropic.types.beta import BetaMessage, BetaTextBlock, BetaToolUseBlock
|
||||
import httpx
|
||||
except ImportError:
|
||||
@@ -36,11 +31,6 @@ except ImportError:
|
||||
) from None
|
||||
|
||||
|
||||
TOOL_SEARCH_TOOL_TYPES: Final[tuple[str, ...]] = (
|
||||
"tool_search_tool_regex_20251119",
|
||||
"tool_search_tool_bm25_20251119",
|
||||
)
|
||||
|
||||
ANTHROPIC_FILES_API_BETA: Final = "files-api-2025-04-14"
|
||||
ANTHROPIC_STRUCTURED_OUTPUTS_BETA: Final = "structured-outputs-2025-11-13"
|
||||
|
||||
@@ -127,22 +117,6 @@ class AnthropicThinkingConfig(BaseModel):
|
||||
budget_tokens: int | None = None
|
||||
|
||||
|
||||
class AnthropicToolSearchConfig(BaseModel):
|
||||
"""Configuration for Anthropic's server-side tool search.
|
||||
|
||||
When enabled, tools marked with defer_loading=True are not loaded into
|
||||
context immediately. Instead, Claude uses the tool search tool to
|
||||
dynamically discover and load relevant tools on-demand.
|
||||
|
||||
Attributes:
|
||||
type: The tool search variant to use.
|
||||
- "regex": Claude constructs regex patterns to search tool names/descriptions.
|
||||
- "bm25": Claude uses natural language queries to search tools.
|
||||
"""
|
||||
|
||||
type: Literal["regex", "bm25"] = "bm25"
|
||||
|
||||
|
||||
class AnthropicCompletion(BaseLLM):
|
||||
"""Anthropic native completion implementation.
|
||||
|
||||
@@ -166,7 +140,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
interceptor: BaseInterceptor[httpx.Request, httpx.Response] | None = None,
|
||||
thinking: AnthropicThinkingConfig | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
tool_search: AnthropicToolSearchConfig | bool | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Anthropic chat completion client.
|
||||
@@ -186,10 +159,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
interceptor: HTTP interceptor for modifying requests/responses at transport level.
|
||||
response_format: Pydantic model for structured output. When provided, responses
|
||||
will be validated against this model schema.
|
||||
tool_search: Enable Anthropic's server-side tool search. When True, uses "bm25"
|
||||
variant by default. Pass an AnthropicToolSearchConfig to choose "regex" or
|
||||
"bm25". When enabled, tools are automatically marked with defer_loading=True
|
||||
and a tool search tool is injected into the tools list.
|
||||
**kwargs: Additional parameters
|
||||
"""
|
||||
super().__init__(
|
||||
@@ -221,13 +190,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
self.thinking = thinking
|
||||
self.previous_thinking_blocks: list[ThinkingBlock] = []
|
||||
self.response_format = response_format
|
||||
# Tool search config
|
||||
if tool_search is True:
|
||||
self.tool_search = AnthropicToolSearchConfig()
|
||||
elif isinstance(tool_search, AnthropicToolSearchConfig):
|
||||
self.tool_search = tool_search
|
||||
else:
|
||||
self.tool_search = None
|
||||
# Model-specific settings
|
||||
self.is_claude_3 = "claude-3" in model.lower()
|
||||
self.supports_tools = True
|
||||
@@ -470,23 +432,10 @@ class AnthropicCompletion(BaseLLM):
|
||||
# Handle tools for Claude 3+
|
||||
if tools and self.supports_tools:
|
||||
converted_tools = self._convert_tools_for_interference(tools)
|
||||
|
||||
# When tool_search is enabled and there are 2+ regular tools,
|
||||
# inject the search tool and mark regular tools with defer_loading.
|
||||
# With only 1 tool there's nothing to search — skip tool search
|
||||
# entirely so the normal forced tool_choice optimisation still works.
|
||||
regular_tools = [
|
||||
t
|
||||
for t in converted_tools
|
||||
if t.get("type", "") not in TOOL_SEARCH_TOOL_TYPES
|
||||
]
|
||||
if self.tool_search is not None and len(regular_tools) >= 2:
|
||||
converted_tools = self._apply_tool_search(converted_tools)
|
||||
|
||||
params["tools"] = converted_tools
|
||||
|
||||
if available_functions and len(regular_tools) == 1:
|
||||
tool_name = regular_tools[0].get("name")
|
||||
if available_functions and len(converted_tools) == 1:
|
||||
tool_name = converted_tools[0].get("name")
|
||||
if tool_name and tool_name in available_functions:
|
||||
params["tool_choice"] = {"type": "tool", "name": tool_name}
|
||||
|
||||
@@ -505,12 +454,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
anthropic_tools = []
|
||||
|
||||
for tool in tools:
|
||||
# Pass through tool search tool definitions unchanged
|
||||
tool_type = tool.get("type", "")
|
||||
if tool_type in TOOL_SEARCH_TOOL_TYPES:
|
||||
anthropic_tools.append(tool)
|
||||
continue
|
||||
|
||||
if "input_schema" in tool and "name" in tool and "description" in tool:
|
||||
anthropic_tools.append(tool)
|
||||
continue
|
||||
@@ -523,15 +466,15 @@ class AnthropicCompletion(BaseLLM):
|
||||
logging.error(f"Error converting tool to Anthropic format: {e}")
|
||||
raise e
|
||||
|
||||
anthropic_tool: dict[str, Any] = {
|
||||
anthropic_tool = {
|
||||
"name": name,
|
||||
"description": description,
|
||||
}
|
||||
|
||||
if parameters and isinstance(parameters, dict):
|
||||
anthropic_tool["input_schema"] = parameters
|
||||
anthropic_tool["input_schema"] = parameters # type: ignore[assignment]
|
||||
else:
|
||||
anthropic_tool["input_schema"] = {
|
||||
anthropic_tool["input_schema"] = { # type: ignore[assignment]
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"required": [],
|
||||
@@ -541,55 +484,6 @@ class AnthropicCompletion(BaseLLM):
|
||||
|
||||
return anthropic_tools
|
||||
|
||||
def _apply_tool_search(self, tools: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""Inject tool search tool and mark regular tools with defer_loading.
|
||||
|
||||
When tool_search is enabled, this method:
|
||||
1. Adds the appropriate tool search tool definition (regex or bm25)
|
||||
2. Marks all regular tools with defer_loading=True so they are only
|
||||
loaded when Claude discovers them via search
|
||||
|
||||
Args:
|
||||
tools: Converted tool definitions in Anthropic format.
|
||||
|
||||
Returns:
|
||||
Updated tools list with tool search tool prepended and
|
||||
regular tools marked as deferred.
|
||||
"""
|
||||
if self.tool_search is None:
|
||||
return tools
|
||||
|
||||
# Check if a tool search tool is already present (user passed one manually)
|
||||
has_search_tool = any(
|
||||
t.get("type", "") in TOOL_SEARCH_TOOL_TYPES for t in tools
|
||||
)
|
||||
|
||||
result: list[dict[str, Any]] = []
|
||||
|
||||
if not has_search_tool:
|
||||
# Map config type to API type identifier
|
||||
type_map = {
|
||||
"regex": "tool_search_tool_regex_20251119",
|
||||
"bm25": "tool_search_tool_bm25_20251119",
|
||||
}
|
||||
tool_type = type_map[self.tool_search.type]
|
||||
# Tool search tool names follow the convention: tool_search_tool_{variant}
|
||||
tool_name = f"tool_search_tool_{self.tool_search.type}"
|
||||
result.append({"type": tool_type, "name": tool_name})
|
||||
|
||||
for tool in tools:
|
||||
# Don't modify tool search tools
|
||||
if tool.get("type", "") in TOOL_SEARCH_TOOL_TYPES:
|
||||
result.append(tool)
|
||||
continue
|
||||
|
||||
# Mark regular tools as deferred if not already set
|
||||
if "defer_loading" not in tool:
|
||||
tool = {**tool, "defer_loading": True}
|
||||
result.append(tool)
|
||||
|
||||
return result
|
||||
|
||||
def _extract_thinking_block(
|
||||
self, content_block: Any
|
||||
) -> ThinkingBlock | dict[str, Any] | None:
|
||||
|
||||
@@ -1781,7 +1781,6 @@ class BedrockCompletion(BaseLLM):
|
||||
|
||||
converse_messages: list[LLMMessage] = []
|
||||
system_message: str | None = None
|
||||
pending_tool_results: list[dict[str, Any]] = []
|
||||
|
||||
for message in formatted_messages:
|
||||
role = message.get("role")
|
||||
@@ -1795,56 +1794,53 @@ class BedrockCompletion(BaseLLM):
|
||||
system_message += f"\n\n{content}"
|
||||
else:
|
||||
system_message = cast(str, content)
|
||||
elif role == "assistant" and tool_calls:
|
||||
# Convert OpenAI-style tool_calls to Bedrock toolUse format
|
||||
bedrock_content = []
|
||||
for tc in tool_calls:
|
||||
func = tc.get("function", {})
|
||||
tool_use_block = {
|
||||
"toolUse": {
|
||||
"toolUseId": tc.get("id", f"call_{id(tc)}"),
|
||||
"name": func.get("name", ""),
|
||||
"input": func.get("arguments", {})
|
||||
if isinstance(func.get("arguments"), dict)
|
||||
else json.loads(func.get("arguments", "{}") or "{}"),
|
||||
}
|
||||
}
|
||||
bedrock_content.append(tool_use_block)
|
||||
converse_messages.append(
|
||||
{"role": "assistant", "content": bedrock_content}
|
||||
)
|
||||
elif role == "tool":
|
||||
if not tool_call_id:
|
||||
raise ValueError("Tool message missing required tool_call_id")
|
||||
pending_tool_results.append(
|
||||
converse_messages.append(
|
||||
{
|
||||
"toolResult": {
|
||||
"toolUseId": tool_call_id,
|
||||
"content": [{"text": str(content) if content else ""}],
|
||||
}
|
||||
"role": "user",
|
||||
"content": [
|
||||
{
|
||||
"toolResult": {
|
||||
"toolUseId": tool_call_id,
|
||||
"content": [
|
||||
{"text": str(content) if content else ""}
|
||||
],
|
||||
}
|
||||
}
|
||||
],
|
||||
}
|
||||
)
|
||||
else:
|
||||
if pending_tool_results:
|
||||
converse_messages.append(
|
||||
{"role": "user", "content": pending_tool_results}
|
||||
)
|
||||
pending_tool_results = []
|
||||
|
||||
if role == "assistant" and tool_calls:
|
||||
# Convert OpenAI-style tool_calls to Bedrock toolUse format
|
||||
bedrock_content = []
|
||||
for tc in tool_calls:
|
||||
func = tc.get("function", {})
|
||||
tool_use_block = {
|
||||
"toolUse": {
|
||||
"toolUseId": tc.get("id", f"call_{id(tc)}"),
|
||||
"name": func.get("name", ""),
|
||||
"input": func.get("arguments", {})
|
||||
if isinstance(func.get("arguments"), dict)
|
||||
else json.loads(func.get("arguments", "{}") or "{}"),
|
||||
}
|
||||
}
|
||||
bedrock_content.append(tool_use_block)
|
||||
converse_messages.append(
|
||||
{"role": "assistant", "content": bedrock_content}
|
||||
)
|
||||
# Convert to Converse API format with proper content structure
|
||||
if isinstance(content, list):
|
||||
# Already formatted as multimodal content blocks
|
||||
converse_messages.append({"role": role, "content": content})
|
||||
else:
|
||||
# Convert to Converse API format with proper content structure
|
||||
if isinstance(content, list):
|
||||
# Already formatted as multimodal content blocks
|
||||
converse_messages.append({"role": role, "content": content})
|
||||
else:
|
||||
# String content - wrap in text block
|
||||
text_content = content if content else ""
|
||||
converse_messages.append(
|
||||
{"role": role, "content": [{"text": text_content}]}
|
||||
)
|
||||
|
||||
if pending_tool_results:
|
||||
converse_messages.append({"role": "user", "content": pending_tool_results})
|
||||
# String content - wrap in text block
|
||||
text_content = content if content else ""
|
||||
converse_messages.append(
|
||||
{"role": role, "content": [{"text": text_content}]}
|
||||
)
|
||||
|
||||
# CRITICAL: Handle model-specific conversation requirements
|
||||
# Cohere and some other models require conversation to end with user message
|
||||
|
||||
@@ -11,7 +11,6 @@ into a standalone MCPToolResolver. It handles three flavours of MCP reference:
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Any, Final, cast
|
||||
from urllib.parse import urlparse
|
||||
@@ -26,7 +25,6 @@ from crewai.mcp.config import (
|
||||
from crewai.mcp.transports.http import HTTPTransport
|
||||
from crewai.mcp.transports.sse import SSETransport
|
||||
from crewai.mcp.transports.stdio import StdioTransport
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -76,9 +74,10 @@ class MCPToolResolver:
|
||||
elif isinstance(mcp_config, str):
|
||||
amp_refs.append(self._parse_amp_ref(mcp_config))
|
||||
else:
|
||||
tools, clients = self._resolve_native(mcp_config)
|
||||
tools, client = self._resolve_native(mcp_config)
|
||||
all_tools.extend(tools)
|
||||
self._clients.extend(clients)
|
||||
if client:
|
||||
self._clients.append(client)
|
||||
|
||||
if amp_refs:
|
||||
tools, clients = self._resolve_amp(amp_refs)
|
||||
@@ -132,7 +131,7 @@ class MCPToolResolver:
|
||||
all_tools: list[BaseTool] = []
|
||||
all_clients: list[Any] = []
|
||||
|
||||
resolved_cache: dict[str, tuple[list[BaseTool], list[Any]]] = {}
|
||||
resolved_cache: dict[str, tuple[list[BaseTool], Any | None]] = {}
|
||||
|
||||
for slug in unique_slugs:
|
||||
config_dict = amp_configs_map.get(slug)
|
||||
@@ -150,9 +149,10 @@ class MCPToolResolver:
|
||||
mcp_server_config = self._build_mcp_config_from_dict(config_dict)
|
||||
|
||||
try:
|
||||
tools, clients = self._resolve_native(mcp_server_config)
|
||||
resolved_cache[slug] = (tools, clients)
|
||||
all_clients.extend(clients)
|
||||
tools, client = self._resolve_native(mcp_server_config)
|
||||
resolved_cache[slug] = (tools, client)
|
||||
if client:
|
||||
all_clients.append(client)
|
||||
except Exception as e:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
@@ -170,9 +170,8 @@ class MCPToolResolver:
|
||||
|
||||
slug_tools, _ = cached
|
||||
if specific_tool:
|
||||
sanitized = sanitize_tool_name(specific_tool)
|
||||
all_tools.extend(
|
||||
t for t in slug_tools if t.name.endswith(f"_{sanitized}")
|
||||
t for t in slug_tools if t.name.endswith(f"_{specific_tool}")
|
||||
)
|
||||
else:
|
||||
all_tools.extend(slug_tools)
|
||||
@@ -199,6 +198,7 @@ class MCPToolResolver:
|
||||
|
||||
plus_api = PlusAPI(api_key=get_platform_integration_token())
|
||||
response = plus_api.get_mcp_configs(slugs)
|
||||
|
||||
if response.status_code == 200:
|
||||
configs: dict[str, dict[str, Any]] = response.json().get("configs", {})
|
||||
return configs
|
||||
@@ -218,7 +218,6 @@ class MCPToolResolver:
|
||||
|
||||
def _resolve_external(self, mcp_ref: str) -> list[BaseTool]:
|
||||
"""Resolve an HTTPS MCP server URL into tools."""
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.mcp_tool_wrapper import MCPToolWrapper
|
||||
|
||||
if "#" in mcp_ref:
|
||||
@@ -228,9 +227,6 @@ class MCPToolResolver:
|
||||
|
||||
server_params = {"url": server_url}
|
||||
server_name = self._extract_server_name(server_url)
|
||||
sanitized_specific_tool = (
|
||||
sanitize_tool_name(specific_tool) if specific_tool else None
|
||||
)
|
||||
|
||||
try:
|
||||
tool_schemas = self._get_mcp_tool_schemas(server_params)
|
||||
@@ -243,7 +239,7 @@ class MCPToolResolver:
|
||||
|
||||
tools = []
|
||||
for tool_name, schema in tool_schemas.items():
|
||||
if sanitized_specific_tool and tool_name != sanitized_specific_tool:
|
||||
if specific_tool and tool_name != specific_tool:
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -275,16 +271,14 @@ class MCPToolResolver:
|
||||
)
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def _create_transport(
|
||||
mcp_config: MCPServerConfig,
|
||||
) -> tuple[StdioTransport | HTTPTransport | SSETransport, str]:
|
||||
"""Create a fresh transport instance from an MCP server config.
|
||||
def _resolve_native(
|
||||
self, mcp_config: MCPServerConfig
|
||||
) -> tuple[list[BaseTool], Any | None]:
|
||||
"""Resolve an ``MCPServerConfig`` into tools, returning the client for cleanup."""
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.mcp_native_tool import MCPNativeTool
|
||||
|
||||
Returns a ``(transport, server_name)`` tuple. Each call produces an
|
||||
independent transport so that parallel tool executions never share
|
||||
state.
|
||||
"""
|
||||
transport: StdioTransport | HTTPTransport | SSETransport
|
||||
if isinstance(mcp_config, MCPServerStdio):
|
||||
transport = StdioTransport(
|
||||
command=mcp_config.command,
|
||||
@@ -298,54 +292,38 @@ class MCPToolResolver:
|
||||
headers=mcp_config.headers,
|
||||
streamable=mcp_config.streamable,
|
||||
)
|
||||
server_name = MCPToolResolver._extract_server_name(mcp_config.url)
|
||||
server_name = self._extract_server_name(mcp_config.url)
|
||||
elif isinstance(mcp_config, MCPServerSSE):
|
||||
transport = SSETransport(
|
||||
url=mcp_config.url,
|
||||
headers=mcp_config.headers,
|
||||
)
|
||||
server_name = MCPToolResolver._extract_server_name(mcp_config.url)
|
||||
server_name = self._extract_server_name(mcp_config.url)
|
||||
else:
|
||||
raise ValueError(f"Unsupported MCP server config type: {type(mcp_config)}")
|
||||
return transport, server_name
|
||||
|
||||
def _resolve_native(
|
||||
self, mcp_config: MCPServerConfig
|
||||
) -> tuple[list[BaseTool], list[Any]]:
|
||||
"""Resolve an ``MCPServerConfig`` into tools.
|
||||
|
||||
Returns ``(tools, clients)`` where *clients* is always empty for
|
||||
native tools (clients are now created on-demand per invocation).
|
||||
A ``client_factory`` closure is passed to each ``MCPNativeTool`` so
|
||||
every call -- even concurrent calls to the *same* tool -- gets its
|
||||
own ``MCPClient`` + transport with no shared mutable state.
|
||||
"""
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.mcp_native_tool import MCPNativeTool
|
||||
|
||||
discovery_transport, server_name = self._create_transport(mcp_config)
|
||||
discovery_client = MCPClient(
|
||||
transport=discovery_transport,
|
||||
client = MCPClient(
|
||||
transport=transport,
|
||||
cache_tools_list=mcp_config.cache_tools_list,
|
||||
)
|
||||
|
||||
async def _setup_client_and_list_tools() -> list[dict[str, Any]]:
|
||||
try:
|
||||
if not discovery_client.connected:
|
||||
await discovery_client.connect()
|
||||
if not client.connected:
|
||||
await client.connect()
|
||||
|
||||
tools_list = await discovery_client.list_tools()
|
||||
tools_list = await client.list_tools()
|
||||
|
||||
try:
|
||||
await discovery_client.disconnect()
|
||||
await client.disconnect()
|
||||
await asyncio.sleep(0.1)
|
||||
except Exception as e:
|
||||
self._logger.log("error", f"Error during disconnect: {e}")
|
||||
|
||||
return tools_list
|
||||
except Exception as e:
|
||||
if discovery_client.connected:
|
||||
await discovery_client.disconnect()
|
||||
if client.connected:
|
||||
await client.disconnect()
|
||||
await asyncio.sleep(0.1)
|
||||
raise RuntimeError(
|
||||
f"Error during setup client and list tools: {e}"
|
||||
@@ -356,10 +334,9 @@ class MCPToolResolver:
|
||||
asyncio.get_running_loop()
|
||||
import concurrent.futures
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
future = executor.submit(
|
||||
ctx.run, asyncio.run, _setup_client_and_list_tools()
|
||||
asyncio.run, _setup_client_and_list_tools()
|
||||
)
|
||||
tools_list = future.result()
|
||||
except RuntimeError:
|
||||
@@ -399,13 +376,6 @@ class MCPToolResolver:
|
||||
filtered_tools.append(tool)
|
||||
tools_list = filtered_tools
|
||||
|
||||
def _client_factory() -> MCPClient:
|
||||
transport, _ = self._create_transport(mcp_config)
|
||||
return MCPClient(
|
||||
transport=transport,
|
||||
cache_tools_list=mcp_config.cache_tools_list,
|
||||
)
|
||||
|
||||
tools = []
|
||||
for tool_def in tools_list:
|
||||
tool_name = tool_def.get("name", "")
|
||||
@@ -426,7 +396,7 @@ class MCPToolResolver:
|
||||
|
||||
try:
|
||||
native_tool = MCPNativeTool(
|
||||
client_factory=_client_factory,
|
||||
mcp_client=client,
|
||||
tool_name=tool_name,
|
||||
tool_schema=tool_schema,
|
||||
server_name=server_name,
|
||||
@@ -437,10 +407,10 @@ class MCPToolResolver:
|
||||
self._logger.log("error", f"Failed to create native MCP tool: {e}")
|
||||
continue
|
||||
|
||||
return cast(list[BaseTool], tools), []
|
||||
return cast(list[BaseTool], tools), client
|
||||
except Exception as e:
|
||||
if discovery_client.connected:
|
||||
asyncio.run(discovery_client.disconnect())
|
||||
if client.connected:
|
||||
asyncio.run(client.disconnect())
|
||||
|
||||
raise RuntimeError(f"Failed to get native MCP tools: {e}") from e
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Orchestrates the encoding side of memory in a single Flow with 5 steps:
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
import math
|
||||
from typing import Any
|
||||
@@ -165,20 +164,14 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
def parallel_find_similar(self) -> None:
|
||||
"""Search storage for similar records, concurrently for all active items."""
|
||||
items = list(self.state.items)
|
||||
active = [
|
||||
(i, item)
|
||||
for i, item in enumerate(items)
|
||||
if not item.dropped and item.embedding
|
||||
]
|
||||
active = [(i, item) for i, item in enumerate(items) if not item.dropped and item.embedding]
|
||||
|
||||
if not active:
|
||||
return
|
||||
|
||||
def _search_one(
|
||||
item: ItemState,
|
||||
) -> list[tuple[MemoryRecord, float]]:
|
||||
def _search_one(item: ItemState) -> list[tuple[MemoryRecord, float]]:
|
||||
scope_prefix = item.scope if item.scope and item.scope.strip("/") else None
|
||||
return self._storage.search( # type: ignore[no-any-return]
|
||||
return self._storage.search(
|
||||
item.embedding,
|
||||
scope_prefix=scope_prefix,
|
||||
categories=None,
|
||||
@@ -193,14 +186,7 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
item.top_similarity = float(raw[0][1]) if raw else 0.0
|
||||
else:
|
||||
with ThreadPoolExecutor(max_workers=min(len(active), 8)) as pool:
|
||||
futures = [
|
||||
(
|
||||
i,
|
||||
item,
|
||||
pool.submit(contextvars.copy_context().run, _search_one, item),
|
||||
)
|
||||
for i, item in active
|
||||
]
|
||||
futures = [(i, item, pool.submit(_search_one, item)) for i, item in active]
|
||||
for _, item, future in futures:
|
||||
raw = future.result()
|
||||
item.similar_records = [r for r, _ in raw]
|
||||
@@ -264,38 +250,24 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
# Group B: consolidation only
|
||||
self._apply_defaults(item)
|
||||
consol_futures[i] = pool.submit(
|
||||
contextvars.copy_context().run,
|
||||
analyze_for_consolidation,
|
||||
item.content,
|
||||
list(item.similar_records),
|
||||
self._llm,
|
||||
item.content, list(item.similar_records), self._llm,
|
||||
)
|
||||
elif not fields_provided and not has_similar:
|
||||
# Group C: field resolution only
|
||||
save_futures[i] = pool.submit(
|
||||
contextvars.copy_context().run,
|
||||
analyze_for_save,
|
||||
item.content,
|
||||
existing_scopes,
|
||||
existing_categories,
|
||||
self._llm,
|
||||
item.content, existing_scopes, existing_categories, self._llm,
|
||||
)
|
||||
else:
|
||||
# Group D: both in parallel
|
||||
save_futures[i] = pool.submit(
|
||||
contextvars.copy_context().run,
|
||||
analyze_for_save,
|
||||
item.content,
|
||||
existing_scopes,
|
||||
existing_categories,
|
||||
self._llm,
|
||||
item.content, existing_scopes, existing_categories, self._llm,
|
||||
)
|
||||
consol_futures[i] = pool.submit(
|
||||
contextvars.copy_context().run,
|
||||
analyze_for_consolidation,
|
||||
item.content,
|
||||
list(item.similar_records),
|
||||
self._llm,
|
||||
item.content, list(item.similar_records), self._llm,
|
||||
)
|
||||
|
||||
# Collect field-resolution results
|
||||
@@ -328,8 +300,8 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
item.plan = ConsolidationPlan(actions=[], insert_new=True)
|
||||
|
||||
# Collect consolidation results
|
||||
for i, consol_future in consol_futures.items():
|
||||
items[i].plan = consol_future.result()
|
||||
for i, future in consol_futures.items():
|
||||
items[i].plan = future.result()
|
||||
finally:
|
||||
pool.shutdown(wait=False)
|
||||
|
||||
@@ -367,9 +339,7 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
# similar_records overlap). Collect one action per record_id, first wins.
|
||||
# Also build a map from record_id to the original MemoryRecord for updates.
|
||||
dedup_deletes: set[str] = set() # record_ids to delete
|
||||
dedup_updates: dict[
|
||||
str, tuple[int, str]
|
||||
] = {} # record_id -> (item_idx, new_content)
|
||||
dedup_updates: dict[str, tuple[int, str]] = {} # record_id -> (item_idx, new_content)
|
||||
all_similar: dict[str, MemoryRecord] = {} # record_id -> MemoryRecord
|
||||
|
||||
for i, item in enumerate(items):
|
||||
@@ -380,24 +350,13 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
all_similar[r.id] = r
|
||||
for action in item.plan.actions:
|
||||
rid = action.record_id
|
||||
if (
|
||||
action.action == "delete"
|
||||
and rid not in dedup_deletes
|
||||
and rid not in dedup_updates
|
||||
):
|
||||
if action.action == "delete" and rid not in dedup_deletes and rid not in dedup_updates:
|
||||
dedup_deletes.add(rid)
|
||||
elif (
|
||||
action.action == "update"
|
||||
and action.new_content
|
||||
and rid not in dedup_deletes
|
||||
and rid not in dedup_updates
|
||||
):
|
||||
elif action.action == "update" and action.new_content and rid not in dedup_deletes and rid not in dedup_updates:
|
||||
dedup_updates[rid] = (i, action.new_content)
|
||||
|
||||
# --- Batch re-embed all update contents in ONE call ---
|
||||
update_list = list(
|
||||
dedup_updates.items()
|
||||
) # [(record_id, (item_idx, new_content)), ...]
|
||||
update_list = list(dedup_updates.items()) # [(record_id, (item_idx, new_content)), ...]
|
||||
update_embeddings: list[list[float]] = []
|
||||
if update_list:
|
||||
update_contents = [content for _, (_, content) in update_list]
|
||||
@@ -418,21 +377,16 @@ class EncodingFlow(Flow[EncodingState]):
|
||||
if item.dropped or item.plan is None:
|
||||
continue
|
||||
if item.plan.insert_new:
|
||||
to_insert.append(
|
||||
(
|
||||
i,
|
||||
MemoryRecord(
|
||||
content=item.content,
|
||||
scope=item.resolved_scope,
|
||||
categories=item.resolved_categories,
|
||||
metadata=item.resolved_metadata,
|
||||
importance=item.resolved_importance,
|
||||
embedding=item.embedding if item.embedding else None,
|
||||
source=item.resolved_source,
|
||||
private=item.resolved_private,
|
||||
),
|
||||
)
|
||||
)
|
||||
to_insert.append((i, MemoryRecord(
|
||||
content=item.content,
|
||||
scope=item.resolved_scope,
|
||||
categories=item.resolved_categories,
|
||||
metadata=item.resolved_metadata,
|
||||
importance=item.resolved_importance,
|
||||
embedding=item.embedding if item.embedding else None,
|
||||
source=item.resolved_source,
|
||||
private=item.resolved_private,
|
||||
)))
|
||||
|
||||
# All storage mutations under one lock so no other pipeline can
|
||||
# interleave and cause version conflicts. The lock is reentrant
|
||||
|
||||
@@ -11,7 +11,6 @@ Implements adaptive-depth retrieval with:
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
@@ -104,12 +103,13 @@ class RecallFlow(Flow[RecallState]):
|
||||
)
|
||||
# Post-filter by time cutoff
|
||||
if self.state.time_cutoff and raw:
|
||||
raw = [(r, s) for r, s in raw if r.created_at >= self.state.time_cutoff]
|
||||
raw = [
|
||||
(r, s) for r, s in raw if r.created_at >= self.state.time_cutoff
|
||||
]
|
||||
# Privacy filter
|
||||
if not self.state.include_private and raw:
|
||||
raw = [
|
||||
(r, s)
|
||||
for r, s in raw
|
||||
(r, s) for r, s in raw
|
||||
if not r.private or r.source == self.state.source
|
||||
]
|
||||
return scope, raw
|
||||
@@ -130,20 +130,15 @@ class RecallFlow(Flow[RecallState]):
|
||||
top_composite, _ = compute_composite_score(
|
||||
results[0][0], results[0][1], self._config
|
||||
)
|
||||
findings.append(
|
||||
{
|
||||
"scope": scope,
|
||||
"results": results,
|
||||
"top_score": top_composite,
|
||||
}
|
||||
)
|
||||
findings.append({
|
||||
"scope": scope,
|
||||
"results": results,
|
||||
"top_score": top_composite,
|
||||
})
|
||||
else:
|
||||
with ThreadPoolExecutor(max_workers=min(len(tasks), 4)) as pool:
|
||||
futures = {
|
||||
pool.submit(contextvars.copy_context().run, _search_one, emb, sc): (
|
||||
emb,
|
||||
sc,
|
||||
)
|
||||
pool.submit(_search_one, emb, sc): (emb, sc)
|
||||
for emb, sc in tasks
|
||||
}
|
||||
for future in as_completed(futures):
|
||||
@@ -152,16 +147,16 @@ class RecallFlow(Flow[RecallState]):
|
||||
top_composite, _ = compute_composite_score(
|
||||
results[0][0], results[0][1], self._config
|
||||
)
|
||||
findings.append(
|
||||
{
|
||||
"scope": scope,
|
||||
"results": results,
|
||||
"top_score": top_composite,
|
||||
}
|
||||
)
|
||||
findings.append({
|
||||
"scope": scope,
|
||||
"results": results,
|
||||
"top_score": top_composite,
|
||||
})
|
||||
|
||||
self.state.chunk_findings = findings
|
||||
self.state.confidence = max((f["top_score"] for f in findings), default=0.0)
|
||||
self.state.confidence = max(
|
||||
(f["top_score"] for f in findings), default=0.0
|
||||
)
|
||||
return findings
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -215,16 +210,12 @@ class RecallFlow(Flow[RecallState]):
|
||||
# Parse time_filter into a datetime cutoff
|
||||
if analysis.time_filter:
|
||||
try:
|
||||
self.state.time_cutoff = datetime.fromisoformat(
|
||||
analysis.time_filter
|
||||
)
|
||||
self.state.time_cutoff = datetime.fromisoformat(analysis.time_filter)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
# Batch-embed all sub-queries in ONE call
|
||||
queries = (
|
||||
analysis.recall_queries if analysis.recall_queries else [self.state.query]
|
||||
)
|
||||
queries = analysis.recall_queries if analysis.recall_queries else [self.state.query]
|
||||
queries = queries[:3]
|
||||
embeddings = embed_texts(self._embedder, queries)
|
||||
pairs: list[tuple[str, list[float]]] = [
|
||||
@@ -305,21 +296,17 @@ class RecallFlow(Flow[RecallState]):
|
||||
response = self._llm.call([{"role": "user", "content": prompt}])
|
||||
if isinstance(response, str) and "missing" in response.lower():
|
||||
self.state.evidence_gaps.append(response[:200])
|
||||
enhanced.append(
|
||||
{
|
||||
"scope": finding["scope"],
|
||||
"extraction": response,
|
||||
"results": finding["results"],
|
||||
}
|
||||
)
|
||||
enhanced.append({
|
||||
"scope": finding["scope"],
|
||||
"extraction": response,
|
||||
"results": finding["results"],
|
||||
})
|
||||
except Exception:
|
||||
enhanced.append(
|
||||
{
|
||||
"scope": finding["scope"],
|
||||
"extraction": "",
|
||||
"results": finding["results"],
|
||||
}
|
||||
)
|
||||
enhanced.append({
|
||||
"scope": finding["scope"],
|
||||
"extraction": "",
|
||||
"results": finding["results"],
|
||||
})
|
||||
self.state.chunk_findings = enhanced
|
||||
return enhanced
|
||||
|
||||
@@ -331,7 +318,7 @@ class RecallFlow(Flow[RecallState]):
|
||||
@router(re_search)
|
||||
def re_decide_depth(self) -> str:
|
||||
"""Re-evaluate depth after re-search. Same logic as decide_depth."""
|
||||
return self.decide_depth() # type: ignore[call-arg]
|
||||
return self.decide_depth()
|
||||
|
||||
@listen("synthesize")
|
||||
def synthesize_results(self) -> list[MemoryMatch]:
|
||||
|
||||
@@ -38,8 +38,7 @@ class KickoffTaskOutputsSQLiteStorage:
|
||||
DatabaseOperationError: If database initialization fails due to SQLite errors.
|
||||
"""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
"""
|
||||
@@ -83,7 +82,7 @@ class KickoffTaskOutputsSQLiteStorage:
|
||||
"""
|
||||
inputs = inputs or {}
|
||||
try:
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute(
|
||||
@@ -126,7 +125,7 @@ class KickoffTaskOutputsSQLiteStorage:
|
||||
DatabaseOperationError: If updating the task output fails due to SQLite errors.
|
||||
"""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
cursor = conn.cursor()
|
||||
|
||||
@@ -167,7 +166,7 @@ class KickoffTaskOutputsSQLiteStorage:
|
||||
DatabaseOperationError: If loading task outputs fails due to SQLite errors.
|
||||
"""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT *
|
||||
@@ -206,7 +205,7 @@ class KickoffTaskOutputsSQLiteStorage:
|
||||
DatabaseOperationError: If deleting task outputs fails due to SQLite errors.
|
||||
"""
|
||||
try:
|
||||
with sqlite3.connect(self.db_path, timeout=30) as conn:
|
||||
with sqlite3.connect(self.db_path) as conn:
|
||||
conn.execute("BEGIN TRANSACTION")
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("DELETE FROM latest_kickoff_task_outputs")
|
||||
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from contextlib import AbstractContextManager
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
import json
|
||||
import logging
|
||||
@@ -16,7 +14,6 @@ from typing import Any, ClassVar
|
||||
import lancedb
|
||||
|
||||
from crewai.memory.types import MemoryRecord, ScopeInfo
|
||||
from crewai.utilities.lock_store import lock as store_lock
|
||||
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
@@ -93,7 +90,6 @@ class LanceDBStorage:
|
||||
# Raise it proactively so scans on large tables never hit OS error 24.
|
||||
try:
|
||||
import resource
|
||||
|
||||
soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
|
||||
if soft < 4096:
|
||||
resource.setrlimit(resource.RLIMIT_NOFILE, (min(hard, 4096), hard))
|
||||
@@ -103,8 +99,7 @@ class LanceDBStorage:
|
||||
self._compact_every = compact_every
|
||||
self._save_count = 0
|
||||
|
||||
self._lock_name = f"lancedb:{self._path.resolve()}"
|
||||
|
||||
# Get or create a shared write lock for this database path.
|
||||
resolved = str(self._path.resolve())
|
||||
with LanceDBStorage._path_locks_guard:
|
||||
if resolved not in LanceDBStorage._path_locks:
|
||||
@@ -115,13 +110,10 @@ class LanceDBStorage:
|
||||
# If no table exists yet, defer creation until the first save so the
|
||||
# dimension can be auto-detected from the embedder's actual output.
|
||||
try:
|
||||
self._table: lancedb.table.Table | None = self._db.open_table(
|
||||
self._table_name
|
||||
)
|
||||
self._table: lancedb.table.Table | None = self._db.open_table(self._table_name)
|
||||
self._vector_dim: int = self._infer_dim_from_table(self._table)
|
||||
# Best-effort: create the scope index if it doesn't exist yet.
|
||||
with self._file_lock():
|
||||
self._ensure_scope_index()
|
||||
self._ensure_scope_index()
|
||||
# Compact in the background if the table has accumulated many
|
||||
# fragments from previous runs (each save() creates one).
|
||||
self._compact_if_needed()
|
||||
@@ -132,8 +124,7 @@ class LanceDBStorage:
|
||||
# Explicit dim provided: create the table immediately if it doesn't exist.
|
||||
if self._table is None and vector_dim is not None:
|
||||
self._vector_dim = vector_dim
|
||||
with self._file_lock():
|
||||
self._table = self._create_table(vector_dim)
|
||||
self._table = self._create_table(vector_dim)
|
||||
|
||||
@property
|
||||
def write_lock(self) -> threading.RLock:
|
||||
@@ -158,14 +149,18 @@ class LanceDBStorage:
|
||||
break
|
||||
return DEFAULT_VECTOR_DIM
|
||||
|
||||
def _file_lock(self) -> AbstractContextManager[None]:
|
||||
"""Return a cross-process lock for serialising writes."""
|
||||
return store_lock(self._lock_name)
|
||||
def _retry_write(self, op: str, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Execute a table operation with retry on LanceDB commit conflicts.
|
||||
|
||||
def _do_write(self, op: str, *args: Any, **kwargs: Any) -> Any:
|
||||
"""Execute a single table write with retry on commit conflicts.
|
||||
Args:
|
||||
op: Method name on the table object (e.g. "add", "delete").
|
||||
*args, **kwargs: Passed to the table method.
|
||||
|
||||
Caller must already hold the cross-process file lock.
|
||||
LanceDB uses optimistic concurrency: if two transactions overlap,
|
||||
the second to commit fails with an ``OSError`` containing
|
||||
"Commit conflict". This helper retries with exponential backoff,
|
||||
refreshing the table reference before each retry so the retried
|
||||
call uses the latest committed version (not a stale reference).
|
||||
"""
|
||||
delay = _RETRY_BASE_DELAY
|
||||
for attempt in range(_MAX_RETRIES + 1):
|
||||
@@ -176,24 +171,20 @@ class LanceDBStorage:
|
||||
raise
|
||||
_logger.debug(
|
||||
"LanceDB commit conflict on %s (attempt %d/%d), retrying in %.1fs",
|
||||
op,
|
||||
attempt + 1,
|
||||
_MAX_RETRIES,
|
||||
delay,
|
||||
op, attempt + 1, _MAX_RETRIES, delay,
|
||||
)
|
||||
# Refresh table to pick up the latest version before retrying.
|
||||
# The next getattr(self._table, op) will use the fresh table.
|
||||
try:
|
||||
self._table = self._db.open_table(self._table_name)
|
||||
except Exception: # noqa: S110
|
||||
pass
|
||||
pass # table refresh is best-effort
|
||||
time.sleep(delay)
|
||||
delay *= 2
|
||||
return None # unreachable, but satisfies type checker
|
||||
|
||||
def _create_table(self, vector_dim: int) -> lancedb.table.Table:
|
||||
"""Create a new table with the given vector dimension.
|
||||
|
||||
Caller must already hold the cross-process file lock.
|
||||
"""
|
||||
"""Create a new table with the given vector dimension."""
|
||||
placeholder = [
|
||||
{
|
||||
"id": "__schema_placeholder__",
|
||||
@@ -209,12 +200,8 @@ class LanceDBStorage:
|
||||
"vector": [0.0] * vector_dim,
|
||||
}
|
||||
]
|
||||
try:
|
||||
table = self._db.create_table(self._table_name, placeholder)
|
||||
except ValueError:
|
||||
table = self._db.open_table(self._table_name)
|
||||
else:
|
||||
table.delete("id = '__schema_placeholder__'")
|
||||
table = self._db.create_table(self._table_name, placeholder)
|
||||
table.delete("id = '__schema_placeholder__'")
|
||||
return table
|
||||
|
||||
def _ensure_scope_index(self) -> None:
|
||||
@@ -251,10 +238,8 @@ class LanceDBStorage:
|
||||
|
||||
def _compact_async(self) -> None:
|
||||
"""Fire-and-forget: compact the table in a daemon background thread."""
|
||||
ctx = contextvars.copy_context()
|
||||
threading.Thread(
|
||||
target=ctx.run,
|
||||
args=(self._compact_safe,),
|
||||
target=self._compact_safe,
|
||||
daemon=True,
|
||||
name="lancedb-compact",
|
||||
).start()
|
||||
@@ -263,9 +248,9 @@ class LanceDBStorage:
|
||||
"""Run ``table.optimize()`` in a background thread, absorbing errors."""
|
||||
try:
|
||||
if self._table is not None:
|
||||
with self._file_lock():
|
||||
self._table.optimize()
|
||||
self._ensure_scope_index()
|
||||
self._table.optimize()
|
||||
# Refresh the scope index so new fragments are covered.
|
||||
self._ensure_scope_index()
|
||||
except Exception:
|
||||
_logger.debug("LanceDB background compaction failed", exc_info=True)
|
||||
|
||||
@@ -295,9 +280,7 @@ class LanceDBStorage:
|
||||
"last_accessed": record.last_accessed.isoformat(),
|
||||
"source": record.source or "",
|
||||
"private": record.private,
|
||||
"vector": record.embedding
|
||||
if record.embedding
|
||||
else [0.0] * self._vector_dim,
|
||||
"vector": record.embedding if record.embedding else [0.0] * self._vector_dim,
|
||||
}
|
||||
|
||||
def _row_to_record(self, row: dict[str, Any]) -> MemoryRecord:
|
||||
@@ -313,9 +296,7 @@ class LanceDBStorage:
|
||||
id=str(row["id"]),
|
||||
content=str(row["content"]),
|
||||
scope=str(row["scope"]),
|
||||
categories=json.loads(row["categories_str"])
|
||||
if row.get("categories_str")
|
||||
else [],
|
||||
categories=json.loads(row["categories_str"]) if row.get("categories_str") else [],
|
||||
metadata=json.loads(row["metadata_str"]) if row.get("metadata_str") else {},
|
||||
importance=float(row.get("importance", 0.5)),
|
||||
created_at=_parse_dt(row.get("created_at")),
|
||||
@@ -335,15 +316,16 @@ class LanceDBStorage:
|
||||
dim = len(r.embedding)
|
||||
break
|
||||
is_new_table = self._table is None
|
||||
with self._write_lock, self._file_lock():
|
||||
with self._write_lock:
|
||||
self._ensure_table(vector_dim=dim)
|
||||
rows = [self._record_to_row(r) for r in records]
|
||||
for r in rows:
|
||||
if r["vector"] is None or len(r["vector"]) != self._vector_dim:
|
||||
r["vector"] = [0.0] * self._vector_dim
|
||||
self._do_write("add", rows)
|
||||
if is_new_table:
|
||||
self._ensure_scope_index()
|
||||
self._retry_write("add", rows)
|
||||
# Create the scope index on the first save so it covers the initial dataset.
|
||||
if is_new_table:
|
||||
self._ensure_scope_index()
|
||||
# Auto-compact every N saves so fragment files don't pile up.
|
||||
self._save_count += 1
|
||||
if self._compact_every > 0 and self._save_count % self._compact_every == 0:
|
||||
@@ -351,14 +333,14 @@ class LanceDBStorage:
|
||||
|
||||
def update(self, record: MemoryRecord) -> None:
|
||||
"""Update a record by ID. Preserves created_at, updates last_accessed."""
|
||||
with self._write_lock, self._file_lock():
|
||||
with self._write_lock:
|
||||
self._ensure_table()
|
||||
safe_id = str(record.id).replace("'", "''")
|
||||
self._do_write("delete", f"id = '{safe_id}'")
|
||||
self._retry_write("delete", f"id = '{safe_id}'")
|
||||
row = self._record_to_row(record)
|
||||
if row["vector"] is None or len(row["vector"]) != self._vector_dim:
|
||||
row["vector"] = [0.0] * self._vector_dim
|
||||
self._do_write("add", [row])
|
||||
self._retry_write("add", [row])
|
||||
|
||||
def touch_records(self, record_ids: list[str]) -> None:
|
||||
"""Update last_accessed to now for the given record IDs.
|
||||
@@ -372,11 +354,11 @@ class LanceDBStorage:
|
||||
"""
|
||||
if not record_ids or self._table is None:
|
||||
return
|
||||
with self._write_lock, self._file_lock():
|
||||
with self._write_lock:
|
||||
now = datetime.utcnow().isoformat()
|
||||
safe_ids = [str(rid).replace("'", "''") for rid in record_ids]
|
||||
ids_expr = ", ".join(f"'{rid}'" for rid in safe_ids)
|
||||
self._do_write(
|
||||
self._retry_write(
|
||||
"update",
|
||||
where=f"id IN ({ids_expr})",
|
||||
values={"last_accessed": now},
|
||||
@@ -408,17 +390,13 @@ class LanceDBStorage:
|
||||
prefix = scope_prefix.rstrip("/")
|
||||
like_val = prefix + "%"
|
||||
query = query.where(f"scope LIKE '{like_val}'")
|
||||
results = query.limit(
|
||||
limit * 3 if (categories or metadata_filter) else limit
|
||||
).to_list()
|
||||
results = query.limit(limit * 3 if (categories or metadata_filter) else limit).to_list()
|
||||
out: list[tuple[MemoryRecord, float]] = []
|
||||
for row in results:
|
||||
record = self._row_to_record(row)
|
||||
if categories and not any(c in record.categories for c in categories):
|
||||
continue
|
||||
if metadata_filter and not all(
|
||||
record.metadata.get(k) == v for k, v in metadata_filter.items()
|
||||
):
|
||||
if metadata_filter and not all(record.metadata.get(k) == v for k, v in metadata_filter.items()):
|
||||
continue
|
||||
distance = row.get("_distance", 0.0)
|
||||
score = 1.0 / (1.0 + float(distance)) if distance is not None else 1.0
|
||||
@@ -438,24 +416,20 @@ class LanceDBStorage:
|
||||
) -> int:
|
||||
if self._table is None:
|
||||
return 0
|
||||
with self._write_lock, self._file_lock():
|
||||
with self._write_lock:
|
||||
if record_ids and not (categories or metadata_filter):
|
||||
before = self._table.count_rows()
|
||||
ids_expr = ", ".join(f"'{rid}'" for rid in record_ids)
|
||||
self._do_write("delete", f"id IN ({ids_expr})")
|
||||
self._retry_write("delete", f"id IN ({ids_expr})")
|
||||
return before - self._table.count_rows()
|
||||
if categories or metadata_filter:
|
||||
rows = self._scan_rows(scope_prefix)
|
||||
to_delete: list[str] = []
|
||||
for row in rows:
|
||||
record = self._row_to_record(row)
|
||||
if categories and not any(
|
||||
c in record.categories for c in categories
|
||||
):
|
||||
if categories and not any(c in record.categories for c in categories):
|
||||
continue
|
||||
if metadata_filter and not all(
|
||||
record.metadata.get(k) == v for k, v in metadata_filter.items()
|
||||
):
|
||||
if metadata_filter and not all(record.metadata.get(k) == v for k, v in metadata_filter.items()):
|
||||
continue
|
||||
if older_than and record.created_at >= older_than:
|
||||
continue
|
||||
@@ -464,7 +438,7 @@ class LanceDBStorage:
|
||||
return 0
|
||||
before = self._table.count_rows()
|
||||
ids_expr = ", ".join(f"'{rid}'" for rid in to_delete)
|
||||
self._do_write("delete", f"id IN ({ids_expr})")
|
||||
self._retry_write("delete", f"id IN ({ids_expr})")
|
||||
return before - self._table.count_rows()
|
||||
conditions = []
|
||||
if scope_prefix is not None and scope_prefix.strip("/"):
|
||||
@@ -476,11 +450,11 @@ class LanceDBStorage:
|
||||
conditions.append(f"created_at < '{older_than.isoformat()}'")
|
||||
if not conditions:
|
||||
before = self._table.count_rows()
|
||||
self._do_write("delete", "id != ''")
|
||||
self._retry_write("delete", "id != ''")
|
||||
return before - self._table.count_rows()
|
||||
where_expr = " AND ".join(conditions)
|
||||
before = self._table.count_rows()
|
||||
self._do_write("delete", where_expr)
|
||||
self._retry_write("delete", where_expr)
|
||||
return before - self._table.count_rows()
|
||||
|
||||
def _scan_rows(
|
||||
@@ -554,7 +528,7 @@ class LanceDBStorage:
|
||||
for row in rows:
|
||||
sc = str(row.get("scope", ""))
|
||||
if child_prefix and sc.startswith(child_prefix):
|
||||
rest = sc[len(child_prefix) :]
|
||||
rest = sc[len(child_prefix):]
|
||||
first_component = rest.split("/", 1)[0]
|
||||
if first_component:
|
||||
children.add(child_prefix + first_component)
|
||||
@@ -565,11 +539,7 @@ class LanceDBStorage:
|
||||
pass
|
||||
created = row.get("created_at")
|
||||
if created:
|
||||
dt = (
|
||||
datetime.fromisoformat(str(created).replace("Z", "+00:00"))
|
||||
if isinstance(created, str)
|
||||
else created
|
||||
)
|
||||
dt = datetime.fromisoformat(str(created).replace("Z", "+00:00")) if isinstance(created, str) else created
|
||||
if isinstance(dt, datetime):
|
||||
if oldest is None or dt < oldest:
|
||||
oldest = dt
|
||||
@@ -592,7 +562,7 @@ class LanceDBStorage:
|
||||
for row in rows:
|
||||
sc = str(row.get("scope", ""))
|
||||
if sc.startswith(prefix) and sc != (prefix.rstrip("/") or "/"):
|
||||
rest = sc[len(prefix) :]
|
||||
rest = sc[len(prefix):]
|
||||
first_component = rest.split("/", 1)[0]
|
||||
if first_component:
|
||||
children.add(prefix + first_component)
|
||||
@@ -620,19 +590,17 @@ class LanceDBStorage:
|
||||
return info.record_count
|
||||
|
||||
def reset(self, scope_prefix: str | None = None) -> None:
|
||||
with self._write_lock, self._file_lock():
|
||||
if scope_prefix is None or scope_prefix.strip("/") == "":
|
||||
if self._table is not None:
|
||||
self._db.drop_table(self._table_name)
|
||||
self._table = None
|
||||
return
|
||||
if self._table is None:
|
||||
return
|
||||
prefix = scope_prefix.rstrip("/")
|
||||
if prefix:
|
||||
self._do_write(
|
||||
"delete", f"scope >= '{prefix}' AND scope < '{prefix}/\uffff'"
|
||||
)
|
||||
if scope_prefix is None or scope_prefix.strip("/") == "":
|
||||
if self._table is not None:
|
||||
self._db.drop_table(self._table_name)
|
||||
self._table = None
|
||||
# Dimension is preserved; table will be recreated on next save.
|
||||
return
|
||||
if self._table is None:
|
||||
return
|
||||
prefix = scope_prefix.rstrip("/")
|
||||
if prefix:
|
||||
self._table.delete(f"scope >= '{prefix}' AND scope < '{prefix}/\uFFFF'")
|
||||
|
||||
def optimize(self) -> None:
|
||||
"""Compact the table synchronously and refresh the scope index.
|
||||
@@ -646,9 +614,8 @@ class LanceDBStorage:
|
||||
"""
|
||||
if self._table is None:
|
||||
return
|
||||
with self._write_lock, self._file_lock():
|
||||
self._table.optimize()
|
||||
self._ensure_scope_index()
|
||||
self._table.optimize()
|
||||
self._ensure_scope_index()
|
||||
|
||||
async def asave(self, records: list[MemoryRecord]) -> None:
|
||||
self.save(records)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from concurrent.futures import Future, ThreadPoolExecutor
|
||||
import contextvars
|
||||
from datetime import datetime
|
||||
import threading
|
||||
import time
|
||||
@@ -230,9 +229,8 @@ class Memory(BaseModel):
|
||||
If the pool has been shut down (e.g. after ``close()``), the save
|
||||
runs synchronously as a fallback so late saves still succeed.
|
||||
"""
|
||||
ctx = contextvars.copy_context()
|
||||
try:
|
||||
future: Future[Any] = self._save_pool.submit(ctx.run, fn, *args, **kwargs)
|
||||
future: Future[Any] = self._save_pool.submit(fn, *args, **kwargs)
|
||||
except RuntimeError:
|
||||
# Pool shut down -- run synchronously as fallback
|
||||
future = Future()
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import contextvars
|
||||
from functools import wraps
|
||||
import inspect
|
||||
from typing import TYPE_CHECKING, Any, Concatenate, ParamSpec, TypeVar, overload
|
||||
@@ -170,9 +169,8 @@ def _call_method(method: Callable[..., Any], *args: Any, **kwargs: Any) -> Any:
|
||||
if loop and loop.is_running():
|
||||
import concurrent.futures
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
return pool.submit(ctx.run, asyncio.run, result).result()
|
||||
return pool.submit(asyncio.run, result).result()
|
||||
return asyncio.run(result)
|
||||
return result
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import contextvars
|
||||
from functools import partial
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
@@ -147,9 +146,8 @@ def _resolve_result(result: Any) -> Any:
|
||||
if loop and loop.is_running():
|
||||
import concurrent.futures
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor() as pool:
|
||||
return pool.submit(ctx.run, asyncio.run, result).result()
|
||||
return pool.submit(asyncio.run, result).result()
|
||||
return asyncio.run(result)
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"""Factory functions for creating ChromaDB clients."""
|
||||
|
||||
from hashlib import md5
|
||||
import os
|
||||
|
||||
from chromadb import PersistentClient
|
||||
import portalocker
|
||||
|
||||
from crewai.rag.chromadb.client import ChromaDBClient
|
||||
from crewai.rag.chromadb.config import ChromaDBConfig
|
||||
from crewai.utilities.lock_store import lock
|
||||
|
||||
|
||||
def create_client(config: ChromaDBConfig) -> ChromaDBClient:
|
||||
@@ -24,8 +25,10 @@ def create_client(config: ChromaDBConfig) -> ChromaDBClient:
|
||||
|
||||
persist_dir = config.settings.persist_directory
|
||||
os.makedirs(persist_dir, exist_ok=True)
|
||||
lock_id = md5(persist_dir.encode(), usedforsecurity=False).hexdigest()
|
||||
lockfile = os.path.join(persist_dir, f"chromadb-{lock_id}.lock")
|
||||
|
||||
with lock(f"chromadb:{persist_dir}"):
|
||||
with portalocker.Lock(lockfile):
|
||||
client = PersistentClient(
|
||||
path=persist_dir,
|
||||
settings=config.settings,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import contextvars
|
||||
from concurrent.futures import Future
|
||||
from copy import copy as shallow_copy
|
||||
import datetime
|
||||
@@ -525,11 +524,10 @@ class Task(BaseModel):
|
||||
) -> Future[TaskOutput]:
|
||||
"""Execute the task asynchronously."""
|
||||
future: Future[TaskOutput] = Future()
|
||||
ctx = contextvars.copy_context()
|
||||
threading.Thread(
|
||||
daemon=True,
|
||||
target=ctx.run,
|
||||
args=(self._execute_task_async, agent, context, tools, future),
|
||||
target=self._execute_task_async,
|
||||
args=(agent, context, tools, future),
|
||||
).start()
|
||||
return future
|
||||
|
||||
|
||||
@@ -1,31 +1,29 @@
|
||||
"""Native MCP tool wrapper for CrewAI agents.
|
||||
|
||||
This module provides a tool wrapper that creates a fresh MCP client for every
|
||||
invocation, ensuring safe parallel execution even when the same tool is called
|
||||
concurrently by the executor.
|
||||
This module provides a tool wrapper that reuses existing MCP client sessions
|
||||
for better performance and connection management.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
import contextvars
|
||||
from typing import Any
|
||||
|
||||
from crewai.tools import BaseTool
|
||||
|
||||
|
||||
class MCPNativeTool(BaseTool):
|
||||
"""Native MCP tool that creates a fresh client per invocation.
|
||||
"""Native MCP tool that reuses client sessions.
|
||||
|
||||
A ``client_factory`` callable produces an independent ``MCPClient`` +
|
||||
transport for every ``_run_async`` call. This guarantees that parallel
|
||||
invocations -- whether of the *same* tool or *different* tools from the
|
||||
same server -- never share mutable connection state (which would cause
|
||||
anyio cancel-scope errors).
|
||||
This tool wrapper is used when agents connect to MCP servers using
|
||||
structured configurations. It reuses existing client sessions for
|
||||
better performance and proper connection lifecycle management.
|
||||
|
||||
Unlike MCPToolWrapper which connects on-demand, this tool uses
|
||||
a shared MCP client instance that maintains a persistent connection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
client_factory: Callable[[], Any],
|
||||
mcp_client: Any,
|
||||
tool_name: str,
|
||||
tool_schema: dict[str, Any],
|
||||
server_name: str,
|
||||
@@ -34,16 +32,19 @@ class MCPNativeTool(BaseTool):
|
||||
"""Initialize native MCP tool.
|
||||
|
||||
Args:
|
||||
client_factory: Zero-arg callable that returns a new MCPClient.
|
||||
mcp_client: MCPClient instance with active session.
|
||||
tool_name: Name of the tool (may be prefixed).
|
||||
tool_schema: Schema information for the tool.
|
||||
server_name: Name of the MCP server for prefixing.
|
||||
original_tool_name: Original name of the tool on the MCP server.
|
||||
"""
|
||||
# Create tool name with server prefix to avoid conflicts
|
||||
prefixed_name = f"{server_name}_{tool_name}"
|
||||
|
||||
# Handle args_schema properly - BaseTool expects a BaseModel subclass
|
||||
args_schema = tool_schema.get("args_schema")
|
||||
|
||||
# Only pass args_schema if it's provided
|
||||
kwargs = {
|
||||
"name": prefixed_name,
|
||||
"description": tool_schema.get(
|
||||
@@ -56,9 +57,16 @@ class MCPNativeTool(BaseTool):
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
self._client_factory = client_factory
|
||||
# Set instance attributes after super().__init__
|
||||
self._mcp_client = mcp_client
|
||||
self._original_tool_name = original_tool_name or tool_name
|
||||
self._server_name = server_name
|
||||
# self._logger = logging.getLogger(__name__)
|
||||
|
||||
@property
|
||||
def mcp_client(self) -> Any:
|
||||
"""Get the MCP client instance."""
|
||||
return self._mcp_client
|
||||
|
||||
@property
|
||||
def original_tool_name(self) -> str:
|
||||
@@ -85,10 +93,9 @@ class MCPNativeTool(BaseTool):
|
||||
|
||||
import concurrent.futures
|
||||
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor() as executor:
|
||||
coro = self._run_async(**kwargs)
|
||||
future = executor.submit(ctx.run, asyncio.run, coro)
|
||||
future = executor.submit(asyncio.run, coro)
|
||||
return future.result()
|
||||
except RuntimeError:
|
||||
return asyncio.run(self._run_async(**kwargs))
|
||||
@@ -101,26 +108,51 @@ class MCPNativeTool(BaseTool):
|
||||
async def _run_async(self, **kwargs) -> str:
|
||||
"""Async implementation of tool execution.
|
||||
|
||||
A fresh ``MCPClient`` is created for every invocation so that
|
||||
concurrent calls never share transport or session state.
|
||||
|
||||
Args:
|
||||
**kwargs: Arguments to pass to the MCP tool.
|
||||
|
||||
Returns:
|
||||
Result from the MCP tool execution.
|
||||
"""
|
||||
client = self._client_factory()
|
||||
await client.connect()
|
||||
# Note: Since we use asyncio.run() which creates a new event loop each time,
|
||||
# Always reconnect on-demand because asyncio.run() creates new event loops per call
|
||||
# All MCP transport context managers (stdio, streamablehttp_client, sse_client)
|
||||
# use anyio.create_task_group() which can't span different event loops
|
||||
if self._mcp_client.connected:
|
||||
await self._mcp_client.disconnect()
|
||||
|
||||
await self._mcp_client.connect()
|
||||
|
||||
try:
|
||||
result = await client.call_tool(self.original_tool_name, kwargs)
|
||||
finally:
|
||||
await client.disconnect()
|
||||
result = await self._mcp_client.call_tool(self.original_tool_name, kwargs)
|
||||
|
||||
except Exception as e:
|
||||
error_str = str(e).lower()
|
||||
if (
|
||||
"not connected" in error_str
|
||||
or "connection" in error_str
|
||||
or "send" in error_str
|
||||
):
|
||||
await self._mcp_client.disconnect()
|
||||
await self._mcp_client.connect()
|
||||
# Retry the call
|
||||
result = await self._mcp_client.call_tool(
|
||||
self.original_tool_name, kwargs
|
||||
)
|
||||
else:
|
||||
raise
|
||||
|
||||
finally:
|
||||
# Always disconnect after tool call to ensure clean context manager lifecycle
|
||||
# This prevents "exit cancel scope in different task" errors
|
||||
# All transport context managers must be exited in the same event loop they were entered
|
||||
await self._mcp_client.disconnect()
|
||||
|
||||
# Extract result content
|
||||
if isinstance(result, str):
|
||||
return result
|
||||
|
||||
# Handle various result formats
|
||||
if hasattr(result, "content") and result.content:
|
||||
if isinstance(result.content, list) and len(result.content) > 0:
|
||||
content_item = result.content[0]
|
||||
|
||||
@@ -3,7 +3,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Callable, Sequence
|
||||
import concurrent.futures
|
||||
import contextvars
|
||||
import inspect
|
||||
import json
|
||||
import re
|
||||
@@ -908,9 +907,8 @@ def summarize_messages(
|
||||
chunks=chunks, llm=llm, callbacks=callbacks, i18n=i18n
|
||||
)
|
||||
if is_inside_event_loop():
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
summarized_contents = pool.submit(ctx.run, asyncio.run, coro).result()
|
||||
summarized_contents = pool.submit(asyncio.run, coro).result()
|
||||
else:
|
||||
summarized_contents = asyncio.run(coro)
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
from collections.abc import Coroutine
|
||||
import concurrent.futures
|
||||
import contextvars
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, TypeVar
|
||||
from uuid import UUID
|
||||
@@ -47,9 +46,8 @@ def _run_sync(coro: Coroutine[None, None, T]) -> T:
|
||||
"""
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
future = executor.submit(ctx.run, asyncio.run, coro)
|
||||
future = executor.submit(asyncio.run, coro)
|
||||
return future.result()
|
||||
except RuntimeError:
|
||||
return asyncio.run(coro)
|
||||
|
||||
@@ -1,61 +0,0 @@
|
||||
"""Centralised lock factory.
|
||||
|
||||
If ``REDIS_URL`` is set, locks are distributed via ``portalocker.RedisLock``. Otherwise, falls
|
||||
back to the standard ``portalocker.Lock``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterator
|
||||
from contextlib import contextmanager
|
||||
from functools import lru_cache
|
||||
from hashlib import md5
|
||||
import os
|
||||
import tempfile
|
||||
from typing import TYPE_CHECKING, Final
|
||||
|
||||
import portalocker
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import redis
|
||||
|
||||
|
||||
_REDIS_URL: str | None = os.environ.get("REDIS_URL")
|
||||
|
||||
_DEFAULT_TIMEOUT: Final[int] = 120
|
||||
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def _redis_connection() -> redis.Redis:
|
||||
"""Return a cached Redis connection, creating one on first call."""
|
||||
from redis import Redis
|
||||
|
||||
if _REDIS_URL is None:
|
||||
raise ValueError("REDIS_URL environment variable is not set")
|
||||
return Redis.from_url(_REDIS_URL)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def lock(name: str, *, timeout: float = _DEFAULT_TIMEOUT) -> Iterator[None]:
|
||||
"""Acquire a named lock, yielding while it is held.
|
||||
|
||||
Args:
|
||||
name: A human-readable lock name (e.g. ``"chromadb_init"``).
|
||||
Automatically namespaced to avoid collisions.
|
||||
timeout: Maximum seconds to wait for the lock before raising.
|
||||
"""
|
||||
channel = f"crewai:{md5(name.encode(), usedforsecurity=False).hexdigest()}"
|
||||
|
||||
if _REDIS_URL:
|
||||
with portalocker.RedisLock(
|
||||
channel=channel,
|
||||
connection=_redis_connection(),
|
||||
timeout=timeout,
|
||||
):
|
||||
yield
|
||||
else:
|
||||
lock_dir = tempfile.gettempdir()
|
||||
lock_path = os.path.join(lock_dir, f"{channel}.lock")
|
||||
with portalocker.Lock(lock_path, timeout=timeout):
|
||||
yield
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
import asyncio
|
||||
from collections.abc import AsyncIterator, Callable, Iterator
|
||||
import contextvars
|
||||
import queue
|
||||
import threading
|
||||
from typing import Any, NamedTuple
|
||||
@@ -241,8 +240,7 @@ def create_chunk_generator(
|
||||
Yields:
|
||||
StreamChunk objects as they arrive.
|
||||
"""
|
||||
ctx = contextvars.copy_context()
|
||||
thread = threading.Thread(target=ctx.run, args=(run_func,), daemon=True)
|
||||
thread = threading.Thread(target=run_func, daemon=True)
|
||||
thread.start()
|
||||
|
||||
try:
|
||||
|
||||
@@ -2353,68 +2353,3 @@ def test_agent_without_apps_no_platform_tools():
|
||||
|
||||
tools = crew._prepare_tools(agent, task, [])
|
||||
assert tools == []
|
||||
|
||||
|
||||
def test_agent_mcps_accepts_slug_with_specific_tool():
|
||||
"""Agent(mcps=["notion#get_page"]) must pass validation (_SLUG_RE)."""
|
||||
agent = Agent(
|
||||
role="MCP Agent",
|
||||
goal="Test MCP validation",
|
||||
backstory="Test agent",
|
||||
mcps=["notion#get_page"],
|
||||
)
|
||||
assert agent.mcps == ["notion#get_page"]
|
||||
|
||||
|
||||
def test_agent_mcps_accepts_slug_with_hyphenated_tool():
|
||||
agent = Agent(
|
||||
role="MCP Agent",
|
||||
goal="Test MCP validation",
|
||||
backstory="Test agent",
|
||||
mcps=["notion#get-page"],
|
||||
)
|
||||
assert agent.mcps == ["notion#get-page"]
|
||||
|
||||
|
||||
def test_agent_mcps_accepts_multiple_hash_refs():
|
||||
agent = Agent(
|
||||
role="MCP Agent",
|
||||
goal="Test MCP validation",
|
||||
backstory="Test agent",
|
||||
mcps=["notion#get_page", "notion#search", "github#list_repos"],
|
||||
)
|
||||
assert len(agent.mcps) == 3
|
||||
|
||||
|
||||
def test_agent_mcps_accepts_mixed_ref_types():
|
||||
agent = Agent(
|
||||
role="MCP Agent",
|
||||
goal="Test MCP validation",
|
||||
backstory="Test agent",
|
||||
mcps=[
|
||||
"notion#get_page",
|
||||
"notion",
|
||||
"https://mcp.example.com/api",
|
||||
],
|
||||
)
|
||||
assert len(agent.mcps) == 3
|
||||
|
||||
|
||||
def test_agent_mcps_rejects_hash_without_slug():
|
||||
with pytest.raises(ValueError, match="Invalid MCP reference"):
|
||||
Agent(
|
||||
role="MCP Agent",
|
||||
goal="Test MCP validation",
|
||||
backstory="Test agent",
|
||||
mcps=["#get_page"],
|
||||
)
|
||||
|
||||
|
||||
def test_agent_mcps_accepts_legacy_prefix_with_tool():
|
||||
agent = Agent(
|
||||
role="MCP Agent",
|
||||
goal="Test MCP validation",
|
||||
backstory="Test agent",
|
||||
mcps=["crewai-amp:notion#get_page"],
|
||||
)
|
||||
assert agent.mcps == ["crewai-amp:notion#get_page"]
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"What is the weather
|
||||
in Tokyo?"}],"model":"claude-sonnet-4-5","stream":false,"tools":[{"type":"tool_search_tool_bm25_20251119","name":"tool_search_tool_bm25"},{"name":"get_weather","description":"Get
|
||||
current weather conditions for a specified location","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_weather"}},"required":["input"]},"defer_loading":true},{"name":"search_files","description":"Search
|
||||
through files in the workspace by name or content","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for search_files"}},"required":["input"]},"defer_loading":true},{"name":"read_database","description":"Read
|
||||
records from a database table with optional filtering","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for read_database"}},"required":["input"]},"defer_loading":true},{"name":"write_database","description":"Write
|
||||
or update records in a database table","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for write_database"}},"required":["input"]},"defer_loading":true},{"name":"send_email","description":"Send
|
||||
an email message to one or more recipients","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for send_email"}},"required":["input"]},"defer_loading":true},{"name":"read_email","description":"Read
|
||||
emails from inbox with filtering options","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for read_email"}},"required":["input"]},"defer_loading":true},{"name":"create_ticket","description":"Create
|
||||
a new support ticket in the ticketing system","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for create_ticket"}},"required":["input"]},"defer_loading":true},{"name":"update_ticket","description":"Update
|
||||
an existing support ticket status or description","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for update_ticket"}},"required":["input"]},"defer_loading":true},{"name":"list_users","description":"List
|
||||
all users in the system with optional filters","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for list_users"}},"required":["input"]},"defer_loading":true},{"name":"get_user_profile","description":"Get
|
||||
detailed profile information for a specific user","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_user_profile"}},"required":["input"]},"defer_loading":true},{"name":"deploy_service","description":"Deploy
|
||||
a service to the specified environment","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for deploy_service"}},"required":["input"]},"defer_loading":true},{"name":"rollback_service","description":"Rollback
|
||||
a service deployment to a previous version","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for rollback_service"}},"required":["input"]},"defer_loading":true},{"name":"get_service_logs","description":"Get
|
||||
service logs filtered by time range and severity","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_service_logs"}},"required":["input"]},"defer_loading":true},{"name":"run_sql_query","description":"Run
|
||||
a read-only SQL query against the analytics database","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for run_sql_query"}},"required":["input"]},"defer_loading":true},{"name":"create_dashboard","description":"Create
|
||||
a new monitoring dashboard with widgets","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for create_dashboard"}},"required":["input"]},"defer_loading":true}]}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
anthropic-version:
|
||||
- '2023-06-01'
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '3952'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.anthropic.com
|
||||
x-api-key:
|
||||
- X-API-KEY-XXX
|
||||
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:
|
||||
- 0.73.0
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
x-stainless-timeout:
|
||||
- NOT_GIVEN
|
||||
method: POST
|
||||
uri: https://api.anthropic.com/v1/messages
|
||||
response:
|
||||
body:
|
||||
string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01DAGCoL6C12u6yAgR1UqNAs","type":"message","role":"assistant","content":[{"type":"text","text":"I''ll
|
||||
search for a weather-related tool to help you get the weather information
|
||||
for Tokyo."},{"type":"server_tool_use","id":"srvtoolu_0176qgHeeBpSygYAnUzKHCfh","name":"tool_search_tool_bm25","input":{"query":"weather
|
||||
Tokyo current conditions forecast"},"caller":{"type":"direct"}},{"type":"tool_search_tool_result","tool_use_id":"srvtoolu_0176qgHeeBpSygYAnUzKHCfh","content":{"type":"tool_search_tool_search_result","tool_references":[{"type":"tool_reference","tool_name":"get_weather"}]}},{"type":"text","text":"Great!
|
||||
I found a weather tool. Let me get the current weather conditions for Tokyo."},{"type":"tool_use","id":"toolu_01R3FavQLuTrwNvEk9gMaViK","name":"get_weather","input":{"input":"Tokyo"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1566,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"cache_creation":{"ephemeral_5m_input_tokens":0,"ephemeral_1h_input_tokens":0},"output_tokens":155,"service_tier":"standard","inference_geo":"not_available","server_tool_use":{"web_search_requests":0,"web_fetch_requests":0}}}'
|
||||
headers:
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Security-Policy:
|
||||
- CSP-FILTERED
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Sun, 08 Mar 2026 21:04:12 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Robots-Tag:
|
||||
- none
|
||||
anthropic-organization-id:
|
||||
- ANTHROPIC-ORGANIZATION-ID-XXX
|
||||
anthropic-ratelimit-input-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-input-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-input-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-INPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-output-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-output-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-output-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-OUTPUT-TOKENS-RESET-XXX
|
||||
anthropic-ratelimit-requests-limit:
|
||||
- '20000'
|
||||
anthropic-ratelimit-requests-remaining:
|
||||
- '19999'
|
||||
anthropic-ratelimit-requests-reset:
|
||||
- '2026-03-08T21:04:07Z'
|
||||
anthropic-ratelimit-tokens-limit:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-LIMIT-XXX
|
||||
anthropic-ratelimit-tokens-remaining:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-REMAINING-XXX
|
||||
anthropic-ratelimit-tokens-reset:
|
||||
- ANTHROPIC-RATELIMIT-TOKENS-RESET-XXX
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
request-id:
|
||||
- REQUEST-ID-XXX
|
||||
strict-transport-security:
|
||||
- STS-XXX
|
||||
vary:
|
||||
- Accept-Encoding
|
||||
x-envoy-upstream-service-time:
|
||||
- '4330'
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,112 +0,0 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"What is the weather
|
||||
in Tokyo?"}],"model":"claude-sonnet-4-5","stream":false,"tools":[{"name":"get_weather","description":"Get
|
||||
current weather conditions for a specified location","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_weather"}},"required":["input"]}},{"name":"search_files","description":"Search
|
||||
through files in the workspace by name or content","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for search_files"}},"required":["input"]}},{"name":"read_database","description":"Read
|
||||
records from a database table with optional filtering","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for read_database"}},"required":["input"]}},{"name":"write_database","description":"Write
|
||||
or update records in a database table","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for write_database"}},"required":["input"]}},{"name":"send_email","description":"Send
|
||||
an email message to one or more recipients","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for send_email"}},"required":["input"]}},{"name":"read_email","description":"Read
|
||||
emails from inbox with filtering options","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for read_email"}},"required":["input"]}},{"name":"create_ticket","description":"Create
|
||||
a new support ticket in the ticketing system","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for create_ticket"}},"required":["input"]}},{"name":"update_ticket","description":"Update
|
||||
an existing support ticket status or description","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for update_ticket"}},"required":["input"]}},{"name":"list_users","description":"List
|
||||
all users in the system with optional filters","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for list_users"}},"required":["input"]}},{"name":"get_user_profile","description":"Get
|
||||
detailed profile information for a specific user","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_user_profile"}},"required":["input"]}},{"name":"deploy_service","description":"Deploy
|
||||
a service to the specified environment","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for deploy_service"}},"required":["input"]}},{"name":"rollback_service","description":"Rollback
|
||||
a service deployment to a previous version","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for rollback_service"}},"required":["input"]}},{"name":"get_service_logs","description":"Get
|
||||
service logs filtered by time range and severity","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_service_logs"}},"required":["input"]}},{"name":"run_sql_query","description":"Run
|
||||
a read-only SQL query against the analytics database","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for run_sql_query"}},"required":["input"]}},{"name":"create_dashboard","description":"Create
|
||||
a new monitoring dashboard with widgets","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for create_dashboard"}},"required":["input"]}}]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
anthropic-version:
|
||||
- '2023-06-01'
|
||||
connection:
|
||||
- keep-alive
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.anthropic.com
|
||||
method: POST
|
||||
uri: https://api.anthropic.com/v1/messages
|
||||
response:
|
||||
body:
|
||||
string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01NoSearch001","type":"message","role":"assistant","content":[{"type":"tool_use","id":"toolu_01NoSearch001","name":"get_weather","input":{"input":"Tokyo"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1943,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":54,"service_tier":"standard"}}'
|
||||
headers:
|
||||
Content-Type:
|
||||
- application/json
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"max_tokens":4096,"messages":[{"role":"user","content":"What is the weather
|
||||
in Tokyo?"}],"model":"claude-sonnet-4-5","stream":false,"tools":[{"type":"tool_search_tool_bm25_20251119","name":"tool_search_tool_bm25"},{"name":"get_weather","description":"Get
|
||||
current weather conditions for a specified location","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_weather"}},"required":["input"]},"defer_loading":true},{"name":"search_files","description":"Search
|
||||
through files in the workspace by name or content","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for search_files"}},"required":["input"]},"defer_loading":true},{"name":"read_database","description":"Read
|
||||
records from a database table with optional filtering","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for read_database"}},"required":["input"]},"defer_loading":true},{"name":"write_database","description":"Write
|
||||
or update records in a database table","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for write_database"}},"required":["input"]},"defer_loading":true},{"name":"send_email","description":"Send
|
||||
an email message to one or more recipients","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for send_email"}},"required":["input"]},"defer_loading":true},{"name":"read_email","description":"Read
|
||||
emails from inbox with filtering options","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for read_email"}},"required":["input"]},"defer_loading":true},{"name":"create_ticket","description":"Create
|
||||
a new support ticket in the ticketing system","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for create_ticket"}},"required":["input"]},"defer_loading":true},{"name":"update_ticket","description":"Update
|
||||
an existing support ticket status or description","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for update_ticket"}},"required":["input"]},"defer_loading":true},{"name":"list_users","description":"List
|
||||
all users in the system with optional filters","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for list_users"}},"required":["input"]},"defer_loading":true},{"name":"get_user_profile","description":"Get
|
||||
detailed profile information for a specific user","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_user_profile"}},"required":["input"]},"defer_loading":true},{"name":"deploy_service","description":"Deploy
|
||||
a service to the specified environment","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for deploy_service"}},"required":["input"]},"defer_loading":true},{"name":"rollback_service","description":"Rollback
|
||||
a service deployment to a previous version","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for rollback_service"}},"required":["input"]},"defer_loading":true},{"name":"get_service_logs","description":"Get
|
||||
service logs filtered by time range and severity","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for get_service_logs"}},"required":["input"]},"defer_loading":true},{"name":"run_sql_query","description":"Run
|
||||
a read-only SQL query against the analytics database","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for run_sql_query"}},"required":["input"]},"defer_loading":true},{"name":"create_dashboard","description":"Create
|
||||
a new monitoring dashboard with widgets","input_schema":{"type":"object","properties":{"input":{"type":"string","description":"Input
|
||||
for create_dashboard"}},"required":["input"]},"defer_loading":true}]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
anthropic-version:
|
||||
- '2023-06-01'
|
||||
connection:
|
||||
- keep-alive
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.anthropic.com
|
||||
method: POST
|
||||
uri: https://api.anthropic.com/v1/messages
|
||||
response:
|
||||
body:
|
||||
string: '{"model":"claude-sonnet-4-5-20250929","id":"msg_01WithSearch001","type":"message","role":"assistant","content":[{"type":"text","text":"I''ll search for a weather tool."},{"type":"server_tool_use","id":"srvtoolu_01Search001","name":"tool_search_tool_bm25","input":{"query":"weather conditions"},"caller":{"type":"direct"}},{"type":"tool_search_tool_result","tool_use_id":"srvtoolu_01Search001","content":{"type":"tool_search_tool_search_result","tool_references":[{"type":"tool_reference","tool_name":"get_weather"}]}},{"type":"text","text":"Found it. Let me get the weather for Tokyo."},{"type":"tool_use","id":"toolu_01WithSearch001","name":"get_weather","input":{"input":"Tokyo"},"caller":{"type":"direct"}}],"stop_reason":"tool_use","stop_sequence":null,"usage":{"input_tokens":1566,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":155,"service_tier":"standard"}}'
|
||||
headers:
|
||||
Content-Type:
|
||||
- application/json
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
version: 1
|
||||
@@ -1,109 +1,828 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Test Agent. Test agent
|
||||
backstory\nYour personal goal is: Test agent goal"},{"role":"user","content":"\nCurrent
|
||||
Task: Test task description\n\nThis is the expected criteria for your final
|
||||
answer: Test expected output\nyou MUST return the actual complete content as
|
||||
the final answer, not a summary.\n\nProvide your complete response:"}],"model":"gpt-4.1-mini"}'
|
||||
body: !!binary |
|
||||
CvP7AQokCiIKDHNlcnZpY2UubmFtZRISChBjcmV3QUktdGVsZW1ldHJ5Esn7AQoSChBjcmV3YWku
|
||||
dGVsZW1ldHJ5Ep4HChBGdupVRwCZRqXxk3FnMwCbEghSR8rOc1qkfCoMQ3JldyBDcmVhdGVkMAE5
|
||||
8GzO7sagGhhBOAHe7sagGhhKGgoOY3Jld2FpX3ZlcnNpb24SCAoGMC45NS4wShoKDnB5dGhvbl92
|
||||
ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3X2tleRIiCiBjOTdiNWZlYjVkMWI2NmJiNTkwMDZhYWEw
|
||||
MWEyOWNkNkoxCgdjcmV3X2lkEiYKJDk1NGM2OTJmLTc5Y2ItNGZlZi05NjNkLWUyMGRkMjFhMjAw
|
||||
MUocCgxjcmV3X3Byb2Nlc3MSDAoKc2VxdWVudGlhbEoRCgtjcmV3X21lbW9yeRICEABKGgoUY3Jl
|
||||
d19udW1iZXJfb2ZfdGFza3MSAhgBShsKFWNyZXdfbnVtYmVyX29mX2FnZW50cxICGAFKzAIKC2Ny
|
||||
ZXdfYWdlbnRzErwCCrkCW3sia2V5IjogIjA3ZDk5YjYzMDQxMWQzNWZkOTA0N2E1MzJkNTNkZGE3
|
||||
IiwgImlkIjogImQ5ZjkyYTBlLTVlZTYtNGY0NS04NzZiLWIwOWMyZTcwZWZkZiIsICJyb2xlIjog
|
||||
IlJlc2VhcmNoZXIiLCAidmVyYm9zZT8iOiBmYWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBt
|
||||
IjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRl
|
||||
bGVnYXRpb25fZW5hYmxlZD8iOiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNl
|
||||
LCAibWF4X3JldHJ5X2xpbWl0IjogMiwgInRvb2xzX25hbWVzIjogW119XUr/AQoKY3Jld190YXNr
|
||||
cxLwAQrtAVt7ImtleSI6ICI2Mzk5NjUxN2YzZjNmMWM5NGQ2YmI2MTdhYTBiMWM0ZiIsICJpZCI6
|
||||
ICIzZDc0NDlkYi0wMzU3LTQ3NTMtOGNmNS03NGY2ZmMzMGEwYTkiLCAiYXN5bmNfZXhlY3V0aW9u
|
||||
PyI6IGZhbHNlLCAiaHVtYW5faW5wdXQ/IjogZmFsc2UsICJhZ2VudF9yb2xlIjogIlJlc2VhcmNo
|
||||
ZXIiLCAiYWdlbnRfa2V5IjogIjA3ZDk5YjYzMDQxMWQzNWZkOTA0N2E1MzJkNTNkZGE3IiwgInRv
|
||||
b2xzX25hbWVzIjogW119XXoCGAGFAQABAAASjgIKEP1sZDWz95ImNTj+qx9ckqUSCAmsHrq64Y/u
|
||||
KgxUYXNrIENyZWF0ZWQwATnQXu3uxqAaGEFgxO3uxqAaGEouCghjcmV3X2tleRIiCiBjOTdiNWZl
|
||||
YjVkMWI2NmJiNTkwMDZhYWEwMWEyOWNkNkoxCgdjcmV3X2lkEiYKJDk1NGM2OTJmLTc5Y2ItNGZl
|
||||
Zi05NjNkLWUyMGRkMjFhMjAwMUouCgh0YXNrX2tleRIiCiA2Mzk5NjUxN2YzZjNmMWM5NGQ2YmI2
|
||||
MTdhYTBiMWM0ZkoxCgd0YXNrX2lkEiYKJDNkNzQ0OWRiLTAzNTctNDc1My04Y2Y1LTc0ZjZmYzMw
|
||||
YTBhOXoCGAGFAQABAAASngcKEBNuju55KsgJoN1+Y7gEx24SCCoSNPvs01ScKgxDcmV3IENyZWF0
|
||||
ZWQwATlIpr3wxqAaGEHwVMbwxqAaGEoaCg5jcmV3YWlfdmVyc2lvbhIICgYwLjk1LjBKGgoOcHl0
|
||||
aG9uX3ZlcnNpb24SCAoGMy4xMi43Si4KCGNyZXdfa2V5EiIKIDhjMjc1MmY0OWU1YjlkMmI2OGNi
|
||||
MzVjYWM4ZmNjODZkSjEKB2NyZXdfaWQSJgokMTY2ODBmZjMtMjM1Yy00MzZlLTk2MWMtZGNhYWNh
|
||||
YTFiMjA4ShwKDGNyZXdfcHJvY2VzcxIMCgpzZXF1ZW50aWFsShEKC2NyZXdfbWVtb3J5EgIQAEoa
|
||||
ChRjcmV3X251bWJlcl9vZl90YXNrcxICGAFKGwoVY3Jld19udW1iZXJfb2ZfYWdlbnRzEgIYAUrM
|
||||
AgoLY3Jld19hZ2VudHMSvAIKuQJbeyJrZXkiOiAiOGJkMjEzOWI1OTc1MTgxNTA2ZTQxZmQ5YzQ1
|
||||
NjNkNzUiLCAiaWQiOiAiMzY5NmM3ZDktNjcyYS00NmIzLWJlMGMtMzNmNjI2YjEwMGU3IiwgInJv
|
||||
bGUiOiAiUmVzZWFyY2hlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIiOiAyMCwgIm1h
|
||||
eF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6ICJncHQtNG8i
|
||||
LCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29kZV9leGVjdXRpb24/Ijog
|
||||
ZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMiOiBbXX1dSv8BCgpjcmV3
|
||||
X3Rhc2tzEvABCu0BW3sia2V5IjogIjBkNjg1YTIxOTk0ZDk0OTA5N2JjNWE1NmQ3MzdlNmQxIiwg
|
||||
ImlkIjogIjIzYWM1MzA1LTg5YTUtNDM1NC1hODUyLTNmNGNlNDk4NjY4NCIsICJhc3luY19leGVj
|
||||
dXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFnZW50X3JvbGUiOiAiUmVz
|
||||
ZWFyY2hlciIsICJhZ2VudF9rZXkiOiAiOGJkMjEzOWI1OTc1MTgxNTA2ZTQxZmQ5YzQ1NjNkNzUi
|
||||
LCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUBAAEAABKOAgoQt0jLLt+z7mZzw/JaxaWi4xII/o7T
|
||||
QUAqVu8qDFRhc2sgQ3JlYXRlZDABOYg71PDGoBoYQZCN1PDGoBoYSi4KCGNyZXdfa2V5EiIKIDhj
|
||||
Mjc1MmY0OWU1YjlkMmI2OGNiMzVjYWM4ZmNjODZkSjEKB2NyZXdfaWQSJgokMTY2ODBmZjMtMjM1
|
||||
Yy00MzZlLTk2MWMtZGNhYWNhYTFiMjA4Si4KCHRhc2tfa2V5EiIKIDBkNjg1YTIxOTk0ZDk0OTA5
|
||||
N2JjNWE1NmQ3MzdlNmQxSjEKB3Rhc2tfaWQSJgokMjNhYzUzMDUtODlhNS00MzU0LWE4NTItM2Y0
|
||||
Y2U0OTg2Njg0egIYAYUBAAEAABKeBwoQAddeR+5jHI68iED9tmGToRIIqsyiA/tKs2QqDENyZXcg
|
||||
Q3JlYXRlZDABOcC+UPrGoBoYQchXWvrGoBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEoa
|
||||
Cg5weXRob25fdmVyc2lvbhIICgYzLjEyLjdKLgoIY3Jld19rZXkSIgogYjY3MzY4NmZjODIyYzIw
|
||||
M2M3ZTg3OWM2NzU0MjQ2OTlKMQoHY3Jld19pZBImCiRmYjJjNzYwZi00ZTdhLTQ0ZDctOWI4My1i
|
||||
NDA3MjY5YjVjZDRKHAoMY3Jld19wcm9jZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jld19tZW1vcnkS
|
||||
AhAAShoKFGNyZXdfbnVtYmVyX29mX3Rhc2tzEgIYAUobChVjcmV3X251bWJlcl9vZl9hZ2VudHMS
|
||||
AhgBSswCCgtjcmV3X2FnZW50cxK8Agq5Alt7ImtleSI6ICJiNTljZjc3YjZlNzY1ODQ4NzBlYjFj
|
||||
Mzg4MjNkN2UyOCIsICJpZCI6ICJhMTA3Y2M4My1jZjM0LTRhMDctYWFmNi1lNzA4MTU0MmNiOTUi
|
||||
LCAicm9sZSI6ICJSZXNlYXJjaGVyIiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6IDIw
|
||||
LCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjogImdw
|
||||
dC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1dGlv
|
||||
bj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfV1K/wEK
|
||||
CmNyZXdfdGFza3MS8AEK7QFbeyJrZXkiOiAiYTVlNWM1OGNlYTFiOWQwMDMzMmU2ODQ0MWQzMjdi
|
||||
ZGYiLCAiaWQiOiAiNTYzNjc0NmQtNmQ4YS00YzBjLTgyNmEtNDA2YzRlMzc0MTg5IiwgImFzeW5j
|
||||
X2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9sZSI6
|
||||
ICJSZXNlYXJjaGVyIiwgImFnZW50X2tleSI6ICJiNTljZjc3YjZlNzY1ODQ4NzBlYjFjMzg4MjNk
|
||||
N2UyOCIsICJ0b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEo4CChDxrID3kZmdkWC//z9+mfuy
|
||||
EgjUxsn2MojVPioMVGFzayBDcmVhdGVkMAE5IIRs+sagGhhB4OFs+sagGhhKLgoIY3Jld19rZXkS
|
||||
IgogYjY3MzY4NmZjODIyYzIwM2M3ZTg3OWM2NzU0MjQ2OTlKMQoHY3Jld19pZBImCiRmYjJjNzYw
|
||||
Zi00ZTdhLTQ0ZDctOWI4My1iNDA3MjY5YjVjZDRKLgoIdGFza19rZXkSIgogYTVlNWM1OGNlYTFi
|
||||
OWQwMDMzMmU2ODQ0MWQzMjdiZGZKMQoHdGFza19pZBImCiQ1NjM2NzQ2ZC02ZDhhLTRjMGMtODI2
|
||||
YS00MDZjNGUzNzQxODl6AhgBhQEAAQAAErgJChCvyf8lGSXM52eSUv8BPeh1EghI6rK/hduMWSoM
|
||||
Q3JldyBDcmVhdGVkMAE5mJtE/MagGhhB+NhM/MagGhhKGgoOY3Jld2FpX3ZlcnNpb24SCAoGMC45
|
||||
NS4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3X2tleRIiCiBlM2ZkYTBmMzEx
|
||||
MGZlODBiMTg5NDdjMDE0NzE0MzBhNEoxCgdjcmV3X2lkEiYKJDQ5ZWRjNGIwLWZlNzctNDc0Yy1i
|
||||
OGE0LTljMDlkNDUzMWIxY0oeCgxjcmV3X3Byb2Nlc3MSDgoMaGllcmFyY2hpY2FsShEKC2NyZXdf
|
||||
bWVtb3J5EgIQAEoaChRjcmV3X251bWJlcl9vZl90YXNrcxICGAFKGwoVY3Jld19udW1iZXJfb2Zf
|
||||
YWdlbnRzEgIYAkqIBQoLY3Jld19hZ2VudHMS+AQK9QRbeyJrZXkiOiAiOGJkMjEzOWI1OTc1MTgx
|
||||
NTA2ZTQxZmQ5YzQ1NjNkNzUiLCAiaWQiOiAiMzY5NmM3ZDktNjcyYS00NmIzLWJlMGMtMzNmNjI2
|
||||
YjEwMGU3IiwgInJvbGUiOiAiUmVzZWFyY2hlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0
|
||||
ZXIiOiAyMCwgIm1heF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxs
|
||||
bSI6ICJncHQtNG8iLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29kZV9l
|
||||
eGVjdXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMiOiBb
|
||||
XX0sIHsia2V5IjogIjlhNTAxNWVmNDg5NWRjNjI3OGQ1NDgxOGJhNDQ2YWY3IiwgImlkIjogImE5
|
||||
OTRlNjZlLWE5OTEtNDRhNi04OTIxLWE4OGQ0M2QyNjZiYyIsICJyb2xlIjogIlNlbmlvciBXcml0
|
||||
ZXIiLCAidmVyYm9zZT8iOiBmYWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwg
|
||||
ImZ1bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25f
|
||||
ZW5hYmxlZD8iOiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3Jl
|
||||
dHJ5X2xpbWl0IjogMiwgInRvb2xzX25hbWVzIjogW119XUrbAQoKY3Jld190YXNrcxLMAQrJAVt7
|
||||
ImtleSI6ICI1ZmE2NWMwNmE5ZTMxZjJjNjk1NDMyNjY4YWNkNjJkZCIsICJpZCI6ICJiOTY5MGI1
|
||||
OC1hYmNhLTRjYzktOGZlYS01ZTZmNDZjNmQ5ZDUiLCAiYXN5bmNfZXhlY3V0aW9uPyI6IGZhbHNl
|
||||
LCAiaHVtYW5faW5wdXQ/IjogZmFsc2UsICJhZ2VudF9yb2xlIjogIk5vbmUiLCAiYWdlbnRfa2V5
|
||||
IjogbnVsbCwgInRvb2xzX25hbWVzIjogW119XXoCGAGFAQABAAASuAkKECCrkzgLIi2bqMUA6kHF
|
||||
B1ESCFsUbfXKnCROKgxDcmV3IENyZWF0ZWQwATnAlbP8xqAaGEGwPrv8xqAaGEoaCg5jcmV3YWlf
|
||||
dmVyc2lvbhIICgYwLjk1LjBKGgoOcHl0aG9uX3ZlcnNpb24SCAoGMy4xMi43Si4KCGNyZXdfa2V5
|
||||
EiIKIGUzZmRhMGYzMTEwZmU4MGIxODk0N2MwMTQ3MTQzMGE0SjEKB2NyZXdfaWQSJgokNDJlMGQ1
|
||||
MmYtYWVjYS00MTMzLTlmMDItZDZiOGU0OTRkYjYxSh4KDGNyZXdfcHJvY2VzcxIOCgxoaWVyYXJj
|
||||
aGljYWxKEQoLY3Jld19tZW1vcnkSAhAAShoKFGNyZXdfbnVtYmVyX29mX3Rhc2tzEgIYAUobChVj
|
||||
cmV3X251bWJlcl9vZl9hZ2VudHMSAhgCSogFCgtjcmV3X2FnZW50cxL4BAr1BFt7ImtleSI6ICI4
|
||||
YmQyMTM5YjU5NzUxODE1MDZlNDFmZDljNDU2M2Q3NSIsICJpZCI6ICIzNjk2YzdkOS02NzJhLTQ2
|
||||
YjMtYmUwYy0zM2Y2MjZiMTAwZTciLCAicm9sZSI6ICJSZXNlYXJjaGVyIiwgInZlcmJvc2U/Ijog
|
||||
ZmFsc2UsICJtYXhfaXRlciI6IDIwLCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5n
|
||||
X2xsbSI6ICIiLCAibGxtIjogImdwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2Us
|
||||
ICJhbGxvd19jb2RlX2V4ZWN1dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0
|
||||
b29sc19uYW1lcyI6IFtdfSwgeyJrZXkiOiAiOWE1MDE1ZWY0ODk1ZGM2Mjc4ZDU0ODE4YmE0NDZh
|
||||
ZjciLCAiaWQiOiAiYTk5NGU2NmUtYTk5MS00NGE2LTg5MjEtYTg4ZDQzZDI2NmJjIiwgInJvbGUi
|
||||
OiAiU2VuaW9yIFdyaXRlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIiOiAyMCwgIm1h
|
||||
eF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6ICJncHQtNG8i
|
||||
LCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29kZV9leGVjdXRpb24/Ijog
|
||||
ZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMiOiBbXX1dStsBCgpjcmV3
|
||||
X3Rhc2tzEswBCskBW3sia2V5IjogIjVmYTY1YzA2YTllMzFmMmM2OTU0MzI2NjhhY2Q2MmRkIiwg
|
||||
ImlkIjogImM3MGNmMzliLTE2YzktNDNiOC1hN2VhLTY5MTgzZmZmZDg5ZiIsICJhc3luY19leGVj
|
||||
dXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFnZW50X3JvbGUiOiAiTm9u
|
||||
ZSIsICJhZ2VudF9rZXkiOiBudWxsLCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUBAAEAABLKCwoQ
|
||||
Nu3FGKmDx1jRbaca6HH3TRIIb9vd1api6NYqDENyZXcgQ3JlYXRlZDABOaiMR/3GoBoYQRjxT/3G
|
||||
oBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEoaCg5weXRob25fdmVyc2lvbhIICgYzLjEy
|
||||
LjdKLgoIY3Jld19rZXkSIgogZDM4NDZjOWQyNzZlOGU2ZTQzZTMxZjYxNzYzNTdiNGZKMQoHY3Jl
|
||||
d19pZBImCiQ2MDE5NzNhNy04NDlmLTQ4ZWQtOGM4MS04YzY5N2QyY2ViNGRKHAoMY3Jld19wcm9j
|
||||
ZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jld19tZW1vcnkSAhAAShoKFGNyZXdfbnVtYmVyX29mX3Rh
|
||||
c2tzEgIYAkobChVjcmV3X251bWJlcl9vZl9hZ2VudHMSAhgCSogFCgtjcmV3X2FnZW50cxL4BAr1
|
||||
BFt7ImtleSI6ICI4YmQyMTM5YjU5NzUxODE1MDZlNDFmZDljNDU2M2Q3NSIsICJpZCI6ICIzNjk2
|
||||
YzdkOS02NzJhLTQ2YjMtYmUwYy0zM2Y2MjZiMTAwZTciLCAicm9sZSI6ICJSZXNlYXJjaGVyIiwg
|
||||
InZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6IDIwLCAibWF4X3JwbSI6IG51bGwsICJmdW5j
|
||||
dGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjogImdwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJs
|
||||
ZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9s
|
||||
aW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfSwgeyJrZXkiOiAiOWE1MDE1ZWY0ODk1ZGM2Mjc4
|
||||
ZDU0ODE4YmE0NDZhZjciLCAiaWQiOiAiYTk5NGU2NmUtYTk5MS00NGE2LTg5MjEtYTg4ZDQzZDI2
|
||||
NmJjIiwgInJvbGUiOiAiU2VuaW9yIFdyaXRlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0
|
||||
ZXIiOiAyMCwgIm1heF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxs
|
||||
bSI6ICJncHQtNG8iLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29kZV9l
|
||||
eGVjdXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMiOiBb
|
||||
XX1dSu8DCgpjcmV3X3Rhc2tzEuADCt0DW3sia2V5IjogImU5ZTZiNzJhYWMzMjY0NTlkZDcwNjhm
|
||||
MGIxNzE3YzFjIiwgImlkIjogImYzNGM5ZGZjLWU4NzYtNDkzNS04NTNmLTMyM2EwYzhhZGViMiIs
|
||||
ICJhc3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFnZW50
|
||||
X3JvbGUiOiAiUmVzZWFyY2hlciIsICJhZ2VudF9rZXkiOiAiOGJkMjEzOWI1OTc1MTgxNTA2ZTQx
|
||||
ZmQ5YzQ1NjNkNzUiLCAidG9vbHNfbmFtZXMiOiBbXX0sIHsia2V5IjogImVlZWU3ZTczZDVkZjY2
|
||||
ZDQ4ZDJkODA3YmFmZjg3NGYzIiwgImlkIjogImNjOGMxZGQ0LTUxNzktNDdlMC1iMTk0LTU3NmNh
|
||||
MjFkZjllOCIsICJhc3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxz
|
||||
ZSwgImFnZW50X3JvbGUiOiAiU2VuaW9yIFdyaXRlciIsICJhZ2VudF9rZXkiOiAiOWE1MDE1ZWY0
|
||||
ODk1ZGM2Mjc4ZDU0ODE4YmE0NDZhZjciLCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUBAAEAABKm
|
||||
BwoQYZWMzWnoYys7S/fnI87iGRIIla+Vilm2/HgqDENyZXcgQ3JlYXRlZDABOaDT6f3GoBoYQZB8
|
||||
8f3GoBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEoaCg5weXRob25fdmVyc2lvbhIICgYz
|
||||
LjEyLjdKLgoIY3Jld19rZXkSIgogNjczOGFkNWI4Y2IzZTZmMWMxYzkzNTBiOTZjMmU2NzhKMQoH
|
||||
Y3Jld19pZBImCiRjYjJmYWQ2NS1jZmVlLTQ5MjMtYmE4ZS1jYzllYTM4YmRlZDVKHAoMY3Jld19w
|
||||
cm9jZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jld19tZW1vcnkSAhAAShoKFGNyZXdfbnVtYmVyX29m
|
||||
X3Rhc2tzEgIYAUobChVjcmV3X251bWJlcl9vZl9hZ2VudHMSAhgBStACCgtjcmV3X2FnZW50cxLA
|
||||
Agq9Alt7ImtleSI6ICI1MTJhNmRjMzc5ZjY2YjIxZWVhYjI0ZTYzNDgzNmY3MiIsICJpZCI6ICJl
|
||||
ZmM1ZmYyNC1lNGRlLTQwMDctOTE0Ni03MzQ2ODkyMzMxNmEiLCAicm9sZSI6ICJDb250ZW50IFdy
|
||||
aXRlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIiOiAyMCwgIm1heF9ycG0iOiBudWxs
|
||||
LCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6ICJncHQtNG8iLCAiZGVsZWdhdGlv
|
||||
bl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29kZV9leGVjdXRpb24/IjogZmFsc2UsICJtYXhf
|
||||
cmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMiOiBbXX1dSoMCCgpjcmV3X3Rhc2tzEvQBCvEB
|
||||
W3sia2V5IjogIjM0NzcwNzZiZTNhZjcxMzA0NjJlZGFhMmViOGEwNDhlIiwgImlkIjogImI1YTU1
|
||||
ZDIxLWM0YWQtNGY3MS1hNzlmLTc5MmI3MzcwZDM0MSIsICJhc3luY19leGVjdXRpb24/IjogZmFs
|
||||
c2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFnZW50X3JvbGUiOiAiQ29udGVudCBXcml0ZXIi
|
||||
LCAiYWdlbnRfa2V5IjogIjUxMmE2ZGMzNzlmNjZiMjFlZWFiMjRlNjM0ODM2ZjcyIiwgInRvb2xz
|
||||
X25hbWVzIjogW119XXoCGAGFAQABAAASjg8KEPffWTWZFpn8wcrgD+eyhrMSCHU6W3vsK6dIKgxD
|
||||
cmV3IENyZWF0ZWQwATmAXFj+xqAaGEHQ72D+xqAaGEoaCg5jcmV3YWlfdmVyc2lvbhIICgYwLjk1
|
||||
LjBKGgoOcHl0aG9uX3ZlcnNpb24SCAoGMy4xMi43Si4KCGNyZXdfa2V5EiIKIDRhY2I5MzNmZThk
|
||||
ZTRjZDU3NzJlZGIwZTgyMDZlMjhmSjEKB2NyZXdfaWQSJgokZjQ4NDAzYjUtZjRjMi00NjA4LWE1
|
||||
YzYtMjc4NGU5ZTY0MDNlShwKDGNyZXdfcHJvY2VzcxIMCgpzZXF1ZW50aWFsShEKC2NyZXdfbWVt
|
||||
b3J5EgIQAEoaChRjcmV3X251bWJlcl9vZl90YXNrcxICGARKGwoVY3Jld19udW1iZXJfb2ZfYWdl
|
||||
bnRzEgIYAkqBBQoLY3Jld19hZ2VudHMS8QQK7gRbeyJrZXkiOiAiMmJlZmZkY2FjNjVjY2VhYTY1
|
||||
Mzk2ZjJjN2Y1NjhlNmEiLCAiaWQiOiAiNzlkY2E1NjgtOTUxNy00ZWM0LThkODctMDMxZWFlM2Ji
|
||||
OTk1IiwgInJvbGUiOiAiUmVzZWFyY2hlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIi
|
||||
OiAyMCwgIm1heF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6
|
||||
ICJncHQtNG8iLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29kZV9leGVj
|
||||
dXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMiOiBbXX0s
|
||||
IHsia2V5IjogIjFjZGNhOGRlMDdiMjhkMDc0ZDc4NjQ3NDhiZGIxNzY3IiwgImlkIjogIjgzZWI3
|
||||
MGNkLWIzODEtNDYwMy05Nzg5LTkyN2IxYmNlYTU2ZCIsICJyb2xlIjogIldyaXRlciIsICJ2ZXJi
|
||||
b3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIiOiAyMCwgIm1heF9ycG0iOiBudWxsLCAiZnVuY3Rpb25f
|
||||
Y2FsbGluZ19sbG0iOiAiIiwgImxsbSI6ICJncHQtNG8iLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6
|
||||
IGZhbHNlLCAiYWxsb3dfY29kZV9leGVjdXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQi
|
||||
OiAyLCAidG9vbHNfbmFtZXMiOiBbXX1dSroHCgpjcmV3X3Rhc2tzEqsHCqgHW3sia2V5IjogImVi
|
||||
YWVhYTk2ZThjODU1N2YwNDYxNzM2ZDRiZWY5MzE3IiwgImlkIjogImRkMGVkMzgxLTZhNzUtNDVh
|
||||
My1iZGUyLTRlNzdiOTU0YmI2OCIsICJhc3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1hbl9p
|
||||
bnB1dD8iOiBmYWxzZSwgImFnZW50X3JvbGUiOiAiUmVzZWFyY2hlciIsICJhZ2VudF9rZXkiOiAi
|
||||
MmJlZmZkY2FjNjVjY2VhYTY1Mzk2ZjJjN2Y1NjhlNmEiLCAidG9vbHNfbmFtZXMiOiBbXX0sIHsi
|
||||
a2V5IjogIjYwZjM1MjI4ZWMxY2I3M2ZlZDM1ZDk5MTBhNmQ3OWYzIiwgImlkIjogImE0OGZmMzgx
|
||||
LTI2ZDEtNDVjNy04MGVkLWJlODM0NTkxYWIzYyIsICJhc3luY19leGVjdXRpb24/IjogZmFsc2Us
|
||||
ICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFnZW50X3JvbGUiOiAiV3JpdGVyIiwgImFnZW50X2tl
|
||||
eSI6ICIxY2RjYThkZTA3YjI4ZDA3NGQ3ODY0NzQ4YmRiMTc2NyIsICJ0b29sc19uYW1lcyI6IFtd
|
||||
fSwgeyJrZXkiOiAiYmUyYTcxNGFjMzVlM2E2YjBhYmJhMjRjZWMyZTA0Y2MiLCAiaWQiOiAiMDkx
|
||||
YWE2YjMtZGYyMC00YTMzLTk1MzUtOGJiNDllMzlhMGQyIiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBm
|
||||
YWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9sZSI6ICJXcml0ZXIiLCAiYWdl
|
||||
bnRfa2V5IjogIjFjZGNhOGRlMDdiMjhkMDc0ZDc4NjQ3NDhiZGIxNzY3IiwgInRvb2xzX25hbWVz
|
||||
IjogW119LCB7ImtleSI6ICI0YTU2YTYyNzk4ODZhNmZlNThkNjc1NzgxZDFmNWFkOSIsICJpZCI6
|
||||
ICIxMDFlOGNhNC04MTk1LTQyNDYtYjg2Ny05ZjYxYzM1NWJjOGIiLCAiYXN5bmNfZXhlY3V0aW9u
|
||||
PyI6IGZhbHNlLCAiaHVtYW5faW5wdXQ/IjogZmFsc2UsICJhZ2VudF9yb2xlIjogIldyaXRlciIs
|
||||
ICJhZ2VudF9rZXkiOiAiMWNkY2E4ZGUwN2IyOGQwNzRkNzg2NDc0OGJkYjE3NjciLCAidG9vbHNf
|
||||
bmFtZXMiOiBbXX1degIYAYUBAAEAABKLCQoQgHmumMETjYmEZpveDu3dwBIIByVlUIAMTMEqDENy
|
||||
ZXcgQ3JlYXRlZDABOfgtEgDHoBoYQTC/GwDHoBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUu
|
||||
MEoaCg5weXRob25fdmVyc2lvbhIICgYzLjEyLjdKLgoIY3Jld19rZXkSIgogODBjNzk4ZjYyMjhm
|
||||
MzJhNzQ4M2Y3MmFmZTM2NmVkY2FKMQoHY3Jld19pZBImCiQ0YzM3YTFhNS1lMzA5LTQ2N2EtYWJk
|
||||
ZC0zZDY1YThlNjY5ZjBKHAoMY3Jld19wcm9jZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jld19tZW1v
|
||||
cnkSAhAAShoKFGNyZXdfbnVtYmVyX29mX3Rhc2tzEgIYAkobChVjcmV3X251bWJlcl9vZl9hZ2Vu
|
||||
dHMSAhgBSswCCgtjcmV3X2FnZW50cxK8Agq5Alt7ImtleSI6ICIzN2Q3MTNkM2RjZmFlMWRlNTNi
|
||||
NGUyZGFjNzU1M2ZkNyIsICJpZCI6ICJmNGY2NmQxMi01M2Q0LTQ2NTQtODRiZC1lMjJmYzk2ZDU0
|
||||
NTEiLCAicm9sZSI6ICJ0ZXN0X2FnZW50IiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6
|
||||
IDIwLCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjog
|
||||
ImdwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1
|
||||
dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfV1K
|
||||
7AMKCmNyZXdfdGFza3MS3QMK2gNbeyJrZXkiOiAiY2M0YTQyYzE4NmVlMWEyZTY2YjAyOGVjNWI3
|
||||
MmJkNGUiLCAiaWQiOiAiMmUyMmZiMDMtMzIxMS00NTgxLTkzN2EtZjY1Zjk5MjY3ZmIyIiwgImFz
|
||||
eW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9s
|
||||
ZSI6ICJ0ZXN0X2FnZW50IiwgImFnZW50X2tleSI6ICIzN2Q3MTNkM2RjZmFlMWRlNTNiNGUyZGFj
|
||||
NzU1M2ZkNyIsICJ0b29sc19uYW1lcyI6IFtdfSwgeyJrZXkiOiAiNzRlNmIyNDQ5YzQ1NzRhY2Jj
|
||||
MmJmNDk3MjczYTVjYzEiLCAiaWQiOiAiODIzYmRlYzUtMTRkMS00ZDdjLWJkYWMtODkzNTY1YmFi
|
||||
YmM1IiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAi
|
||||
YWdlbnRfcm9sZSI6ICJ0ZXN0X2FnZW50IiwgImFnZW50X2tleSI6ICIzN2Q3MTNkM2RjZmFlMWRl
|
||||
NTNiNGUyZGFjNzU1M2ZkNyIsICJ0b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEo4CChDXwUEa
|
||||
LzdRrsWweePQjNzuEgjgSUXh0IH0OyoMVGFzayBDcmVhdGVkMAE5aKkrAMegGhhBaCYsAMegGhhK
|
||||
LgoIY3Jld19rZXkSIgogODBjNzk4ZjYyMjhmMzJhNzQ4M2Y3MmFmZTM2NmVkY2FKMQoHY3Jld19p
|
||||
ZBImCiQ0YzM3YTFhNS1lMzA5LTQ2N2EtYWJkZC0zZDY1YThlNjY5ZjBKLgoIdGFza19rZXkSIgog
|
||||
Y2M0YTQyYzE4NmVlMWEyZTY2YjAyOGVjNWI3MmJkNGVKMQoHdGFza19pZBImCiQyZTIyZmIwMy0z
|
||||
MjExLTQ1ODEtOTM3YS1mNjVmOTkyNjdmYjJ6AhgBhQEAAQAAEo4CChDxJ8ZFykKBgfaipCQ/ggPb
|
||||
EgguzV65sDQE1yoMVGFzayBDcmVhdGVkMAE5OBNvAMegGhhBgIRvAMegGhhKLgoIY3Jld19rZXkS
|
||||
IgogODBjNzk4ZjYyMjhmMzJhNzQ4M2Y3MmFmZTM2NmVkY2FKMQoHY3Jld19pZBImCiQ0YzM3YTFh
|
||||
NS1lMzA5LTQ2N2EtYWJkZC0zZDY1YThlNjY5ZjBKLgoIdGFza19rZXkSIgogNzRlNmIyNDQ5YzQ1
|
||||
NzRhY2JjMmJmNDk3MjczYTVjYzFKMQoHdGFza19pZBImCiQ4MjNiZGVjNS0xNGQxLTRkN2MtYmRh
|
||||
Yy04OTM1NjViYWJiYzV6AhgBhQEAAQAAEo4CChC0QeqqmE8Dp/Ee9DEhuLMuEggOnt12q4mouioM
|
||||
VGFzayBDcmVhdGVkMAE5eBbHAMegGhhB2IPHAMegGhhKLgoIY3Jld19rZXkSIgogODBjNzk4ZjYy
|
||||
MjhmMzJhNzQ4M2Y3MmFmZTM2NmVkY2FKMQoHY3Jld19pZBImCiQ0YzM3YTFhNS1lMzA5LTQ2N2Et
|
||||
YWJkZC0zZDY1YThlNjY5ZjBKLgoIdGFza19rZXkSIgogNzRlNmIyNDQ5YzQ1NzRhY2JjMmJmNDk3
|
||||
MjczYTVjYzFKMQoHdGFza19pZBImCiQ4MjNiZGVjNS0xNGQxLTRkN2MtYmRhYy04OTM1NjViYWJi
|
||||
YzV6AhgBhQEAAQAAEsoLChAQHimti07LsJEmR4M5P2iQEgjeCnwCLR02XyoMQ3JldyBDcmVhdGVk
|
||||
MAE5IOlAAsegGhhBAGVJAsegGhhKGgoOY3Jld2FpX3ZlcnNpb24SCAoGMC45NS4wShoKDnB5dGhv
|
||||
bl92ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3X2tleRIiCiBhYzdlNzQ1OTA3MmM3ZWMwNmRlYWY5
|
||||
ZDMyZWNlYzE1YUoxCgdjcmV3X2lkEiYKJGI1NTdkNDliLTkxZTktNDllMy1iNjA4LTUyZTdiMGE1
|
||||
YzZjM0ocCgxjcmV3X3Byb2Nlc3MSDAoKc2VxdWVudGlhbEoRCgtjcmV3X21lbW9yeRICEABKGgoU
|
||||
Y3Jld19udW1iZXJfb2ZfdGFza3MSAhgCShsKFWNyZXdfbnVtYmVyX29mX2FnZW50cxICGAJKiAUK
|
||||
C2NyZXdfYWdlbnRzEvgECvUEW3sia2V5IjogIjhiZDIxMzliNTk3NTE4MTUwNmU0MWZkOWM0NTYz
|
||||
ZDc1IiwgImlkIjogIjM2OTZjN2Q5LTY3MmEtNDZiMy1iZTBjLTMzZjYyNmIxMDBlNyIsICJyb2xl
|
||||
IjogIlJlc2VhcmNoZXIiLCAidmVyYm9zZT8iOiBmYWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhf
|
||||
cnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwg
|
||||
ImRlbGVnYXRpb25fZW5hYmxlZD8iOiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZh
|
||||
bHNlLCAibWF4X3JldHJ5X2xpbWl0IjogMiwgInRvb2xzX25hbWVzIjogW119LCB7ImtleSI6ICI5
|
||||
YTUwMTVlZjQ4OTVkYzYyNzhkNTQ4MThiYTQ0NmFmNyIsICJpZCI6ICJhOTk0ZTY2ZS1hOTkxLTQ0
|
||||
YTYtODkyMS1hODhkNDNkMjY2YmMiLCAicm9sZSI6ICJTZW5pb3IgV3JpdGVyIiwgInZlcmJvc2U/
|
||||
IjogZmFsc2UsICJtYXhfaXRlciI6IDIwLCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxs
|
||||
aW5nX2xsbSI6ICIiLCAibGxtIjogImdwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFs
|
||||
c2UsICJhbGxvd19jb2RlX2V4ZWN1dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIs
|
||||
ICJ0b29sc19uYW1lcyI6IFtdfV1K7wMKCmNyZXdfdGFza3MS4AMK3QNbeyJrZXkiOiAiYTgwNjE3
|
||||
MTcyZmZjYjkwZjg5N2MxYThjMzJjMzEwMmEiLCAiaWQiOiAiZjNmMDYxNWItMDg3NS00NWM0LWFm
|
||||
YmMtYWI1OGQxMGQyZDA0IiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0
|
||||
PyI6IGZhbHNlLCAiYWdlbnRfcm9sZSI6ICJSZXNlYXJjaGVyIiwgImFnZW50X2tleSI6ICI4YmQy
|
||||
MTM5YjU5NzUxODE1MDZlNDFmZDljNDU2M2Q3NSIsICJ0b29sc19uYW1lcyI6IFtdfSwgeyJrZXki
|
||||
OiAiNWZhNjVjMDZhOWUzMWYyYzY5NTQzMjY2OGFjZDYyZGQiLCAiaWQiOiAiNGUwZTEyOTQtZjdi
|
||||
ZS00OTBhLThiYmUtNjliYjQ5ODc1YTUzIiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1
|
||||
bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9sZSI6ICJTZW5pb3IgV3JpdGVyIiwgImFnZW50
|
||||
X2tleSI6ICI5YTUwMTVlZjQ4OTVkYzYyNzhkNTQ4MThiYTQ0NmFmNyIsICJ0b29sc19uYW1lcyI6
|
||||
IFtdfV16AhgBhQEAAQAAEo4CChBu6pl3tRo8XQcOz1dOfEiREgi+aKvpuUNN/ioMVGFzayBDcmVh
|
||||
dGVkMAE5QCRZAsegGhhBKKVZAsegGhhKLgoIY3Jld19rZXkSIgogYWM3ZTc0NTkwNzJjN2VjMDZk
|
||||
ZWFmOWQzMmVjZWMxNWFKMQoHY3Jld19pZBImCiRiNTU3ZDQ5Yi05MWU5LTQ5ZTMtYjYwOC01MmU3
|
||||
YjBhNWM2YzNKLgoIdGFza19rZXkSIgogYTgwNjE3MTcyZmZjYjkwZjg5N2MxYThjMzJjMzEwMmFK
|
||||
MQoHdGFza19pZBImCiRmM2YwNjE1Yi0wODc1LTQ1YzQtYWZiYy1hYjU4ZDEwZDJkMDR6AhgBhQEA
|
||||
AQAAEo4CChBNL9q8o7PtXvaR6poXIlx6EggIBAybRwvpyCoMVGFzayBDcmVhdGVkMAE5qP2oAseg
|
||||
GhhB6JmpAsegGhhKLgoIY3Jld19rZXkSIgogYWM3ZTc0NTkwNzJjN2VjMDZkZWFmOWQzMmVjZWMx
|
||||
NWFKMQoHY3Jld19pZBImCiRiNTU3ZDQ5Yi05MWU5LTQ5ZTMtYjYwOC01MmU3YjBhNWM2YzNKLgoI
|
||||
dGFza19rZXkSIgogNWZhNjVjMDZhOWUzMWYyYzY5NTQzMjY2OGFjZDYyZGRKMQoHdGFza19pZBIm
|
||||
CiQ0ZTBlMTI5NC1mN2JlLTQ5MGEtOGJiZS02OWJiNDk4NzVhNTN6AhgBhQEAAQAAEsoLChAxUBRb
|
||||
Q0xWxbf9ef52QMDSEgihBkurLl3qiSoMQ3JldyBDcmVhdGVkMAE5eE9hBcegGhhBCIVpBcegGhhK
|
||||
GgoOY3Jld2FpX3ZlcnNpb24SCAoGMC45NS4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMuMTIuN0ou
|
||||
CghjcmV3X2tleRIiCiBhYzdlNzQ1OTA3MmM3ZWMwNmRlYWY5ZDMyZWNlYzE1YUoxCgdjcmV3X2lk
|
||||
EiYKJGU1YmYwYTFjLTg2YjctNDhkZC04YzJlLTdjMThhZTZhODJhZUocCgxjcmV3X3Byb2Nlc3MS
|
||||
DAoKc2VxdWVudGlhbEoRCgtjcmV3X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2ZfdGFza3MS
|
||||
AhgCShsKFWNyZXdfbnVtYmVyX29mX2FnZW50cxICGAJKiAUKC2NyZXdfYWdlbnRzEvgECvUEW3si
|
||||
a2V5IjogIjhiZDIxMzliNTk3NTE4MTUwNmU0MWZkOWM0NTYzZDc1IiwgImlkIjogIjM2OTZjN2Q5
|
||||
LTY3MmEtNDZiMy1iZTBjLTMzZjYyNmIxMDBlNyIsICJyb2xlIjogIlJlc2VhcmNoZXIiLCAidmVy
|
||||
Ym9zZT8iOiBmYWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9u
|
||||
X2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5hYmxlZD8i
|
||||
OiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3JldHJ5X2xpbWl0
|
||||
IjogMiwgInRvb2xzX25hbWVzIjogW119LCB7ImtleSI6ICI5YTUwMTVlZjQ4OTVkYzYyNzhkNTQ4
|
||||
MThiYTQ0NmFmNyIsICJpZCI6ICJhOTk0ZTY2ZS1hOTkxLTQ0YTYtODkyMS1hODhkNDNkMjY2YmMi
|
||||
LCAicm9sZSI6ICJTZW5pb3IgV3JpdGVyIiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6
|
||||
IDIwLCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjog
|
||||
ImdwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1
|
||||
dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfV1K
|
||||
7wMKCmNyZXdfdGFza3MS4AMK3QNbeyJrZXkiOiAiYTgwNjE3MTcyZmZjYjkwZjg5N2MxYThjMzJj
|
||||
MzEwMmEiLCAiaWQiOiAiMDJlMTk1ODMtZmY3OS00N2YzLThkNDMtNWJhMGY4NmYxOTllIiwgImFz
|
||||
eW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9s
|
||||
ZSI6ICJSZXNlYXJjaGVyIiwgImFnZW50X2tleSI6ICI4YmQyMTM5YjU5NzUxODE1MDZlNDFmZDlj
|
||||
NDU2M2Q3NSIsICJ0b29sc19uYW1lcyI6IFtdfSwgeyJrZXkiOiAiNWZhNjVjMDZhOWUzMWYyYzY5
|
||||
NTQzMjY2OGFjZDYyZGQiLCAiaWQiOiAiY2ViMjZhOTUtODc5ZS00OGFmLTg2MmItNzAyZmIyODA3
|
||||
MzM5IiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAi
|
||||
YWdlbnRfcm9sZSI6ICJTZW5pb3IgV3JpdGVyIiwgImFnZW50X2tleSI6ICI5YTUwMTVlZjQ4OTVk
|
||||
YzYyNzhkNTQ4MThiYTQ0NmFmNyIsICJ0b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEo4CChD9
|
||||
XNrHzMkqfERO3pxva7qVEgi+KDMFQWeCXioMVGFzayBDcmVhdGVkMAE5KHl4BcegGhhBKPZ4Bceg
|
||||
GhhKLgoIY3Jld19rZXkSIgogYWM3ZTc0NTkwNzJjN2VjMDZkZWFmOWQzMmVjZWMxNWFKMQoHY3Jl
|
||||
d19pZBImCiRlNWJmMGExYy04NmI3LTQ4ZGQtOGMyZS03YzE4YWU2YTgyYWVKLgoIdGFza19rZXkS
|
||||
IgogYTgwNjE3MTcyZmZjYjkwZjg5N2MxYThjMzJjMzEwMmFKMQoHdGFza19pZBImCiQwMmUxOTU4
|
||||
My1mZjc5LTQ3ZjMtOGQ0My01YmEwZjg2ZjE5OWV6AhgBhQEAAQAAEsoLChBy2/tEpjdjZeT9McCa
|
||||
zn1ZEghPIBt/a/+PUyoMQ3JldyBDcmVhdGVkMAE5ABE/BsegGhhB+PlJBsegGhhKGgoOY3Jld2Fp
|
||||
X3ZlcnNpb24SCAoGMC45NS4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3X2tl
|
||||
eRIiCiBkMjdkNDVhZDlkYTE1ODU0MzI1YjBhZjNiMGZiYzMyYkoxCgdjcmV3X2lkEiYKJGM4OGMx
|
||||
ZDc1LWZlN2QtNDQwMi04N2QwLWFkYzQ3MWFiMWI3YUocCgxjcmV3X3Byb2Nlc3MSDAoKc2VxdWVu
|
||||
dGlhbEoRCgtjcmV3X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2ZfdGFza3MSAhgCShsKFWNy
|
||||
ZXdfbnVtYmVyX29mX2FnZW50cxICGAJKiAUKC2NyZXdfYWdlbnRzEvgECvUEW3sia2V5IjogIjhi
|
||||
ZDIxMzliNTk3NTE4MTUwNmU0MWZkOWM0NTYzZDc1IiwgImlkIjogIjM2OTZjN2Q5LTY3MmEtNDZi
|
||||
My1iZTBjLTMzZjYyNmIxMDBlNyIsICJyb2xlIjogIlJlc2VhcmNoZXIiLCAidmVyYm9zZT8iOiBm
|
||||
YWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdf
|
||||
bGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5hYmxlZD8iOiBmYWxzZSwg
|
||||
ImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3JldHJ5X2xpbWl0IjogMiwgInRv
|
||||
b2xzX25hbWVzIjogW119LCB7ImtleSI6ICI5YTUwMTVlZjQ4OTVkYzYyNzhkNTQ4MThiYTQ0NmFm
|
||||
NyIsICJpZCI6ICJhOTk0ZTY2ZS1hOTkxLTQ0YTYtODkyMS1hODhkNDNkMjY2YmMiLCAicm9sZSI6
|
||||
ICJTZW5pb3IgV3JpdGVyIiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6IDIwLCAibWF4
|
||||
X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjogImdwdC00byIs
|
||||
ICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1dGlvbj8iOiBm
|
||||
YWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfV1K7wMKCmNyZXdf
|
||||
dGFza3MS4AMK3QNbeyJrZXkiOiAiODE2ZTllYmM2OWRiNjdjNjhiYjRmM2VhNjVjY2RhNTgiLCAi
|
||||
aWQiOiAiZDM1YjllMjUtODE1MC00ODQ0LWFhMTctYzk0MTRhMDE2NjcyIiwgImFzeW5jX2V4ZWN1
|
||||
dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9sZSI6ICJSZXNl
|
||||
YXJjaGVyIiwgImFnZW50X2tleSI6ICI4YmQyMTM5YjU5NzUxODE1MDZlNDFmZDljNDU2M2Q3NSIs
|
||||
ICJ0b29sc19uYW1lcyI6IFtdfSwgeyJrZXkiOiAiNWZhNjVjMDZhOWUzMWYyYzY5NTQzMjY2OGFj
|
||||
ZDYyZGQiLCAiaWQiOiAiYjIwMjdlZWUtYjNjYi00MGMxLWI1NDEtNmY0ZTA5ZGRhNTU5IiwgImFz
|
||||
eW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9s
|
||||
ZSI6ICJTZW5pb3IgV3JpdGVyIiwgImFnZW50X2tleSI6ICI5YTUwMTVlZjQ4OTVkYzYyNzhkNTQ4
|
||||
MThiYTQ0NmFmNyIsICJ0b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEsoLChD//jBA0L4Z7qgQ
|
||||
5xomV5+TEgjd+k4M+YdqbCoMQ3JldyBDcmVhdGVkMAE5uAq/BsegGhhB6EPJBsegGhhKGgoOY3Jl
|
||||
d2FpX3ZlcnNpb24SCAoGMC45NS4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3
|
||||
X2tleRIiCiBkMjdkNDVhZDlkYTE1ODU0MzI1YjBhZjNiMGZiYzMyYkoxCgdjcmV3X2lkEiYKJGY3
|
||||
OTg0ZWVlLWZjMGItNGFjYy1iNWE3LWExYjgwMWU0NGM1MEocCgxjcmV3X3Byb2Nlc3MSDAoKc2Vx
|
||||
dWVudGlhbEoRCgtjcmV3X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2ZfdGFza3MSAhgCShsK
|
||||
FWNyZXdfbnVtYmVyX29mX2FnZW50cxICGAJKiAUKC2NyZXdfYWdlbnRzEvgECvUEW3sia2V5Ijog
|
||||
IjhiZDIxMzliNTk3NTE4MTUwNmU0MWZkOWM0NTYzZDc1IiwgImlkIjogIjM2OTZjN2Q5LTY3MmEt
|
||||
NDZiMy1iZTBjLTMzZjYyNmIxMDBlNyIsICJyb2xlIjogIlJlc2VhcmNoZXIiLCAidmVyYm9zZT8i
|
||||
OiBmYWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxp
|
||||
bmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5hYmxlZD8iOiBmYWxz
|
||||
ZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3JldHJ5X2xpbWl0IjogMiwg
|
||||
InRvb2xzX25hbWVzIjogW119LCB7ImtleSI6ICI5YTUwMTVlZjQ4OTVkYzYyNzhkNTQ4MThiYTQ0
|
||||
NmFmNyIsICJpZCI6ICJhOTk0ZTY2ZS1hOTkxLTQ0YTYtODkyMS1hODhkNDNkMjY2YmMiLCAicm9s
|
||||
ZSI6ICJTZW5pb3IgV3JpdGVyIiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6IDIwLCAi
|
||||
bWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjogImdwdC00
|
||||
byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1dGlvbj8i
|
||||
OiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfV1K7wMKCmNy
|
||||
ZXdfdGFza3MS4AMK3QNbeyJrZXkiOiAiODE2ZTllYmM2OWRiNjdjNjhiYjRmM2VhNjVjY2RhNTgi
|
||||
LCAiaWQiOiAiOTcxMDdmNTUtY2U2Yi00NWI4LWI4Y2QtZjhjNmIyOGI1YjI5IiwgImFzeW5jX2V4
|
||||
ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9sZSI6ICJS
|
||||
ZXNlYXJjaGVyIiwgImFnZW50X2tleSI6ICI4YmQyMTM5YjU5NzUxODE1MDZlNDFmZDljNDU2M2Q3
|
||||
NSIsICJ0b29sc19uYW1lcyI6IFtdfSwgeyJrZXkiOiAiNWZhNjVjMDZhOWUzMWYyYzY5NTQzMjY2
|
||||
OGFjZDYyZGQiLCAiaWQiOiAiNzZlMTYxMDEtNTY3ZC00YmVlLTg3MGQtNjlkNjUzNWUxM2Y0Iiwg
|
||||
ImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRf
|
||||
cm9sZSI6ICJTZW5pb3IgV3JpdGVyIiwgImFnZW50X2tleSI6ICI5YTUwMTVlZjQ4OTVkYzYyNzhk
|
||||
NTQ4MThiYTQ0NmFmNyIsICJ0b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEv4BChBUyY/ccsE1
|
||||
R24CGyVtHLqZEgiwrBqbcxAHeCoTQ3JldyBUZXN0IEV4ZWN1dGlvbjABOSiyJAfHoBoYQZiNLgfH
|
||||
oBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEouCghjcmV3X2tleRIiCiAzOTQ5M2UxNjE2
|
||||
MzRhOWVjNGRjNGUzOTdhOTc2OTU3MkoxCgdjcmV3X2lkEiYKJGUwZWJlYWE2LTFjMmItNGMxZi1i
|
||||
MzY1LTE4YmNmMjZhOGIwNkoRCgppdGVyYXRpb25zEgMKATJKGwoKbW9kZWxfbmFtZRINCgtncHQt
|
||||
NG8tbWluaXoCGAGFAQABAAASuAkKEPPNALYHa18lwaRtQDvBnDESCJJZx6P/4qPDKgxDcmV3IENy
|
||||
ZWF0ZWQwATnIzZ8Hx6AaGEFIWagHx6AaGEoaCg5jcmV3YWlfdmVyc2lvbhIICgYwLjk1LjBKGgoO
|
||||
cHl0aG9uX3ZlcnNpb24SCAoGMy4xMi43Si4KCGNyZXdfa2V5EiIKIGUzZmRhMGYzMTEwZmU4MGIx
|
||||
ODk0N2MwMTQ3MTQzMGE0SjEKB2NyZXdfaWQSJgokMTBhYzc4ODQtOTA2ZC00YTg0LWIxMTYtMWMx
|
||||
MTg5NDg3OTc3Sh4KDGNyZXdfcHJvY2VzcxIOCgxoaWVyYXJjaGljYWxKEQoLY3Jld19tZW1vcnkS
|
||||
AhAAShoKFGNyZXdfbnVtYmVyX29mX3Rhc2tzEgIYAUobChVjcmV3X251bWJlcl9vZl9hZ2VudHMS
|
||||
AhgCSogFCgtjcmV3X2FnZW50cxL4BAr1BFt7ImtleSI6ICI4YmQyMTM5YjU5NzUxODE1MDZlNDFm
|
||||
ZDljNDU2M2Q3NSIsICJpZCI6ICIzNjk2YzdkOS02NzJhLTQ2YjMtYmUwYy0zM2Y2MjZiMTAwZTci
|
||||
LCAicm9sZSI6ICJSZXNlYXJjaGVyIiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6IDIw
|
||||
LCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjogImdw
|
||||
dC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1dGlv
|
||||
bj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfSwgeyJr
|
||||
ZXkiOiAiOWE1MDE1ZWY0ODk1ZGM2Mjc4ZDU0ODE4YmE0NDZhZjciLCAiaWQiOiAiYTk5NGU2NmUt
|
||||
YTk5MS00NGE2LTg5MjEtYTg4ZDQzZDI2NmJjIiwgInJvbGUiOiAiU2VuaW9yIFdyaXRlciIsICJ2
|
||||
ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIiOiAyMCwgIm1heF9ycG0iOiBudWxsLCAiZnVuY3Rp
|
||||
b25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6ICJncHQtNG8iLCAiZGVsZWdhdGlvbl9lbmFibGVk
|
||||
PyI6IGZhbHNlLCAiYWxsb3dfY29kZV9leGVjdXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGlt
|
||||
aXQiOiAyLCAidG9vbHNfbmFtZXMiOiBbXX1dStsBCgpjcmV3X3Rhc2tzEswBCskBW3sia2V5Ijog
|
||||
IjVmYTY1YzA2YTllMzFmMmM2OTU0MzI2NjhhY2Q2MmRkIiwgImlkIjogIjYzYmEzZTVmLWNlOWIt
|
||||
NDE4Zi04NGNmLWJjOWNlYjUwYTMwNyIsICJhc3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1h
|
||||
bl9pbnB1dD8iOiBmYWxzZSwgImFnZW50X3JvbGUiOiAiTm9uZSIsICJhZ2VudF9rZXkiOiBudWxs
|
||||
LCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUBAAEAABKOAgoQlnr9jeEDn0IZusmEkE/xBxIIbyk0
|
||||
sNkOWxwqDFRhc2sgQ3JlYXRlZDABOdAdygfHoBoYQQCTygfHoBoYSi4KCGNyZXdfa2V5EiIKIGUz
|
||||
ZmRhMGYzMTEwZmU4MGIxODk0N2MwMTQ3MTQzMGE0SjEKB2NyZXdfaWQSJgokMTBhYzc4ODQtOTA2
|
||||
ZC00YTg0LWIxMTYtMWMxMTg5NDg3OTc3Si4KCHRhc2tfa2V5EiIKIDVmYTY1YzA2YTllMzFmMmM2
|
||||
OTU0MzI2NjhhY2Q2MmRkSjEKB3Rhc2tfaWQSJgokNjNiYTNlNWYtY2U5Yi00MThmLTg0Y2YtYmM5
|
||||
Y2ViNTBhMzA3egIYAYUBAAEAABKcAQoQbJPP7Nx3r3ewgPHdeJybDBIIlUb3D4pi3dkqClRvb2wg
|
||||
VXNhZ2UwATmonCAKx6AaGEEgUykKx6AaGEoaCg5jcmV3YWlfdmVyc2lvbhIICgYwLjk1LjBKKAoJ
|
||||
dG9vbF9uYW1lEhsKGURlbGVnYXRlIHdvcmsgdG8gY293b3JrZXJKDgoIYXR0ZW1wdHMSAhgBegIY
|
||||
AYUBAAEAABKcAQoQ1SSOOcoVWGrQIs6azsmxmBIIGSOj86a7GPsqClRvb2wgVXNhZ2UwATmA8e4O
|
||||
x6AaGEGo3vcOx6AaGEoaCg5jcmV3YWlfdmVyc2lvbhIICgYwLjk1LjBKKAoJdG9vbF9uYW1lEhsK
|
||||
GURlbGVnYXRlIHdvcmsgdG8gY293b3JrZXJKDgoIYXR0ZW1wdHMSAhgBegIYAYUBAAEAABK4CQoQ
|
||||
EQHO/mvzkyYWgZwwn+Rc5BIIv4Hy3+pCFpYqDENyZXcgQ3JlYXRlZDABOTgFvg/HoBoYQfi1xQ/H
|
||||
oBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEoaCg5weXRob25fdmVyc2lvbhIICgYzLjEy
|
||||
LjdKLgoIY3Jld19rZXkSIgogZTNmZGEwZjMxMTBmZTgwYjE4OTQ3YzAxNDcxNDMwYTRKMQoHY3Jl
|
||||
d19pZBImCiQxYTNiYWYyMi04ZDA3LTRiOTctOGM4Ni1kMmM0NDNlYTZkZjdKHgoMY3Jld19wcm9j
|
||||
ZXNzEg4KDGhpZXJhcmNoaWNhbEoRCgtjcmV3X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2Zf
|
||||
dGFza3MSAhgBShsKFWNyZXdfbnVtYmVyX29mX2FnZW50cxICGAJKiAUKC2NyZXdfYWdlbnRzEvgE
|
||||
CvUEW3sia2V5IjogIjhiZDIxMzliNTk3NTE4MTUwNmU0MWZkOWM0NTYzZDc1IiwgImlkIjogIjM2
|
||||
OTZjN2Q5LTY3MmEtNDZiMy1iZTBjLTMzZjYyNmIxMDBlNyIsICJyb2xlIjogIlJlc2VhcmNoZXIi
|
||||
LCAidmVyYm9zZT8iOiBmYWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1
|
||||
bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5h
|
||||
YmxlZD8iOiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3JldHJ5
|
||||
X2xpbWl0IjogMiwgInRvb2xzX25hbWVzIjogW119LCB7ImtleSI6ICI5YTUwMTVlZjQ4OTVkYzYy
|
||||
NzhkNTQ4MThiYTQ0NmFmNyIsICJpZCI6ICJhOTk0ZTY2ZS1hOTkxLTQ0YTYtODkyMS1hODhkNDNk
|
||||
MjY2YmMiLCAicm9sZSI6ICJTZW5pb3IgV3JpdGVyIiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhf
|
||||
aXRlciI6IDIwLCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAi
|
||||
bGxtIjogImdwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2Rl
|
||||
X2V4ZWN1dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6
|
||||
IFtdfV1K2wEKCmNyZXdfdGFza3MSzAEKyQFbeyJrZXkiOiAiNWZhNjVjMDZhOWUzMWYyYzY5NTQz
|
||||
MjY2OGFjZDYyZGQiLCAiaWQiOiAiZWYxYjNhN2MtOTMxYi00MjRjLTkxMzQtZDY1OTM1N2I3ODNi
|
||||
IiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdl
|
||||
bnRfcm9sZSI6ICJOb25lIiwgImFnZW50X2tleSI6IG51bGwsICJ0b29sc19uYW1lcyI6IFtdfV16
|
||||
AhgBhQEAAQAAEo4CChBZkLAu5xnAQh/ILJnU7h1REggAGIt5Pa4D3ioMVGFzayBDcmVhdGVkMAE5
|
||||
AMXlD8egGhhBwCLmD8egGhhKLgoIY3Jld19rZXkSIgogZTNmZGEwZjMxMTBmZTgwYjE4OTQ3YzAx
|
||||
NDcxNDMwYTRKMQoHY3Jld19pZBImCiQxYTNiYWYyMi04ZDA3LTRiOTctOGM4Ni1kMmM0NDNlYTZk
|
||||
ZjdKLgoIdGFza19rZXkSIgogNWZhNjVjMDZhOWUzMWYyYzY5NTQzMjY2OGFjZDYyZGRKMQoHdGFz
|
||||
a19pZBImCiRlZjFiM2E3Yy05MzFiLTQyNGMtOTEzNC1kNjU5MzU3Yjc4M2J6AhgBhQEAAQAAEpwB
|
||||
ChBl/QzggjWFEfDigYrgsKMhEgjIhVTOpOyNnioKVG9vbCBVc2FnZTABOWi8pxHHoBoYQYhdrxHH
|
||||
oBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEooCgl0b29sX25hbWUSGwoZRGVsZWdhdGUg
|
||||
d29yayB0byBjb3dvcmtlckoOCghhdHRlbXB0cxICGAF6AhgBhQEAAQAAEpwBChC1Cxzix7ErLK5V
|
||||
rNWRMj7jEgjEMld4I2kVXCoKVG9vbCBVc2FnZTABOSh2whjHoBoYQSi9yxjHoBoYShoKDmNyZXdh
|
||||
aV92ZXJzaW9uEggKBjAuOTUuMEooCgl0b29sX25hbWUSGwoZRGVsZWdhdGUgd29yayB0byBjb3dv
|
||||
cmtlckoOCghhdHRlbXB0cxICGAF6AhgBhQEAAQAAEuEJChCh/OOje68hh/B1dkfbmjf/Egje+GUm
|
||||
CUGqZCoMQ3JldyBDcmVhdGVkMAE5cBtkV8egGhhBcD5zV8egGhhKGgoOY3Jld2FpX3ZlcnNpb24S
|
||||
CAoGMC45NS4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3X2tleRIiCiBjYWEx
|
||||
YWViM2RkNDM2Mzg2NTY4YTVjM2ZlMjEwMWFmNUoxCgdjcmV3X2lkEiYKJDdlZWUxNTA4LWQwNGIt
|
||||
NDczYy1iZjhmLTJkODgxNGU1MjNhN0ocCgxjcmV3X3Byb2Nlc3MSDAoKc2VxdWVudGlhbEoRCgtj
|
||||
cmV3X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2ZfdGFza3MSAhgBShsKFWNyZXdfbnVtYmVy
|
||||
X29mX2FnZW50cxICGAJKhAUKC2NyZXdfYWdlbnRzEvQECvEEW3sia2V5IjogIjk3ZjQxN2YzZTFl
|
||||
MzFjZjBjMTA5Zjc1MjlhYzhmNmJjIiwgImlkIjogIjQwM2ZkM2Q2LTAxNTYtNDIwMS04OGFmLTU0
|
||||
MjU5YjczNzJkYSIsICJyb2xlIjogIlByb2dyYW1tZXIiLCAidmVyYm9zZT8iOiBmYWxzZSwgIm1h
|
||||
eF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIs
|
||||
ICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5hYmxlZD8iOiB0cnVlLCAiYWxsb3dfY29k
|
||||
ZV9leGVjdXRpb24/IjogdHJ1ZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6
|
||||
IFtdfSwgeyJrZXkiOiAiOTJhMjRiMGJjY2ZiMGRjMGU0MzlkN2Q1OWJhOWY2ZjMiLCAiaWQiOiAi
|
||||
YzIxMTQ4ZmQtOGU3NS00NDlhLTg2MmMtNWRiNjQ5Yzc0OTYzIiwgInJvbGUiOiAiQ29kZSBSZXZp
|
||||
ZXdlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIiOiAyMCwgIm1heF9ycG0iOiBudWxs
|
||||
LCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6ICJncHQtNG8iLCAiZGVsZWdhdGlv
|
||||
bl9lbmFibGVkPyI6IHRydWUsICJhbGxvd19jb2RlX2V4ZWN1dGlvbj8iOiB0cnVlLCAibWF4X3Jl
|
||||
dHJ5X2xpbWl0IjogMiwgInRvb2xzX25hbWVzIjogW119XUqKAgoKY3Jld190YXNrcxL7AQr4AVt7
|
||||
ImtleSI6ICI3OWFhMjdkZjc0ZTYyNzllMzRhODg4ODE3NDgxYzQwZiIsICJpZCI6ICI0ZWYzZWEy
|
||||
OS0xMzNjLTQxNjktODgyMS1jZDI4ZTgxMTYxYmIiLCAiYXN5bmNfZXhlY3V0aW9uPyI6IGZhbHNl
|
||||
LCAiaHVtYW5faW5wdXQ/IjogZmFsc2UsICJhZ2VudF9yb2xlIjogIlByb2dyYW1tZXIiLCAiYWdl
|
||||
bnRfa2V5IjogIjk3ZjQxN2YzZTFlMzFjZjBjMTA5Zjc1MjlhYzhmNmJjIiwgInRvb2xzX25hbWVz
|
||||
IjogWyJ0ZXN0IHRvb2wiXX1degIYAYUBAAEAABKuBwoQjpMoNMb5Vz8kFm796AmokxIIPavlOS8Y
|
||||
ZJ0qDENyZXcgQ3JlYXRlZDABOZg1IVjHoBoYQXBfKVjHoBoYShoKDmNyZXdhaV92ZXJzaW9uEggK
|
||||
BjAuOTUuMEoaCg5weXRob25fdmVyc2lvbhIICgYzLjEyLjdKLgoIY3Jld19rZXkSIgogNzczYTg3
|
||||
NmI1NzkyZGI2OTU1OWZlODJjM2FkMjM1OWZKMQoHY3Jld19pZBImCiQwNDQzNzU1MS0yN2RmLTQ3
|
||||
YTQtOTliNS1iOWNkYmYxMDFhNjZKHAoMY3Jld19wcm9jZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jl
|
||||
d19tZW1vcnkSAhAAShoKFGNyZXdfbnVtYmVyX29mX3Rhc2tzEgIYAUobChVjcmV3X251bWJlcl9v
|
||||
Zl9hZ2VudHMSAhgBStQCCgtjcmV3X2FnZW50cxLEAgrBAlt7ImtleSI6ICIwNzdjN2E4NjdlMjBk
|
||||
MGE2OGI5NzRlNDc2MDcxMDlmMyIsICJpZCI6ICIzMDMzZmZkYy03YjI0LTRmMDgtYmNmZS1iYzQz
|
||||
NzhkM2U5NjAiLCAicm9sZSI6ICJNdWx0aW1vZGFsIEFuYWx5c3QiLCAidmVyYm9zZT8iOiBmYWxz
|
||||
ZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdfbGxt
|
||||
IjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5hYmxlZD8iOiBmYWxzZSwgImFs
|
||||
bG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3JldHJ5X2xpbWl0IjogMiwgInRvb2xz
|
||||
X25hbWVzIjogW119XUqHAgoKY3Jld190YXNrcxL4AQr1AVt7ImtleSI6ICJjNzUzYzY4MDYzNTk0
|
||||
MzZhNTg5NmZlYzA5YmFhMTI1ZSIsICJpZCI6ICI3Y2YxYTRkNC0xMmRjLTRjOWUtOWY1Ny0xZjhk
|
||||
MTc5YmNlZGEiLCAiYXN5bmNfZXhlY3V0aW9uPyI6IGZhbHNlLCAiaHVtYW5faW5wdXQ/IjogZmFs
|
||||
c2UsICJhZ2VudF9yb2xlIjogIk11bHRpbW9kYWwgQW5hbHlzdCIsICJhZ2VudF9rZXkiOiAiMDc3
|
||||
YzdhODY3ZTIwZDBhNjhiOTc0ZTQ3NjA3MTA5ZjMiLCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUB
|
||||
AAEAABKkBwoQ7zp57STyOlOLCoDVAFh15hIInYYk7J+gZ94qDENyZXcgQ3JlYXRlZDABOYjOfljH
|
||||
oBoYQZhIhljHoBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEoaCg5weXRob25fdmVyc2lv
|
||||
bhIICgYzLjEyLjdKLgoIY3Jld19rZXkSIgogY2Q0ZGE2NGU2ZGMzYjllYmRjYTI0NDRjMWQ3MzAy
|
||||
ODFKMQoHY3Jld19pZBImCiQ1OTlmMjViNS0xMTgzLTQ2OTctODNjMy03OWUzZmQ3MmQ0NDlKHAoM
|
||||
Y3Jld19wcm9jZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jld19tZW1vcnkSAhAAShoKFGNyZXdfbnVt
|
||||
YmVyX29mX3Rhc2tzEgIYAUobChVjcmV3X251bWJlcl9vZl9hZ2VudHMSAhgBSs8CCgtjcmV3X2Fn
|
||||
ZW50cxK/Agq8Alt7ImtleSI6ICJkODUxMDY0YjliNDg0MThhYzI1ZjhkMzdjN2UzMmJiNiIsICJp
|
||||
ZCI6ICJiY2I5ZjA4Ny1iMzI2LTRmYTQtOWJlZS0wMGVjODlmZTEwMzEiLCAicm9sZSI6ICJJbWFn
|
||||
ZSBBbmFseXN0IiwgInZlcmJvc2U/IjogZmFsc2UsICJtYXhfaXRlciI6IDIwLCAibWF4X3JwbSI6
|
||||
IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xsbSI6ICIiLCAibGxtIjogImdwdC00byIsICJkZWxl
|
||||
Z2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2RlX2V4ZWN1dGlvbj8iOiBmYWxzZSwg
|
||||
Im1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6IFtdfV1KggIKCmNyZXdfdGFza3MS
|
||||
8wEK8AFbeyJrZXkiOiAiZWU4NzI5Njk0MTBjOTRjMzM0ZjljZmZhMGE0MTVmZWMiLCAiaWQiOiAi
|
||||
NmFlMDcxYmItMjU4ZS00ZWRkLThhOGItODIxNzU4ZTFhNmRkIiwgImFzeW5jX2V4ZWN1dGlvbj8i
|
||||
OiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdlbnRfcm9sZSI6ICJJbWFnZSBBbmFs
|
||||
eXN0IiwgImFnZW50X2tleSI6ICJkODUxMDY0YjliNDg0MThhYzI1ZjhkMzdjN2UzMmJiNiIsICJ0
|
||||
b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEqMHChBetHqqjbX/OlqTuIZkVppxEgirl8FuUewu
|
||||
TSoMQ3JldyBDcmVhdGVkMAE5aGwoWcegGhhBOCw0WcegGhhKGgoOY3Jld2FpX3ZlcnNpb24SCAoG
|
||||
MC45NS4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMuMTIuN0ouCghjcmV3X2tleRIiCiBlMzk1Njdi
|
||||
NTA1MjkwOWNhMzM0MDk4NGI4Mzg5ODBlYUoxCgdjcmV3X2lkEiYKJDA2ZTljN2FjLTEzZDItNGU4
|
||||
MS1hNzI2LTBlYjIyYzdlNWQ3MEocCgxjcmV3X3Byb2Nlc3MSDAoKc2VxdWVudGlhbEoRCgtjcmV3
|
||||
X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2ZfdGFza3MSAhgBShsKFWNyZXdfbnVtYmVyX29m
|
||||
X2FnZW50cxICGAFKzgIKC2NyZXdfYWdlbnRzEr4CCrsCW3sia2V5IjogIjlkYzhjY2UwMzA0Njgx
|
||||
OTYwNDFiNGMzODBiNjE3Y2IwIiwgImlkIjogImI1ZGZkNmEyLTA1ZWYtNDIzNS1iZDVjLTI3ZTAy
|
||||
MGExYzk4ZiIsICJyb2xlIjogIkltYWdlIEFuYWx5c3QiLCAidmVyYm9zZT8iOiB0cnVlLCAibWF4
|
||||
X2l0ZXIiOiAyMCwgIm1heF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwg
|
||||
ImxsbSI6ICJncHQtNG8iLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29k
|
||||
ZV9leGVjdXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMi
|
||||
OiBbXX1dSoICCgpjcmV3X3Rhc2tzEvMBCvABW3sia2V5IjogImE5YTc2Y2E2OTU3ZDBiZmZhNjll
|
||||
YWIyMGI2NjQ4MjJiIiwgImlkIjogIjJhMmQ4MDYzLTBkMmQtNDhmZi04NjJhLWNiOGM1NGEyMDYx
|
||||
NiIsICJhc3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFn
|
||||
ZW50X3JvbGUiOiAiSW1hZ2UgQW5hbHlzdCIsICJhZ2VudF9rZXkiOiAiOWRjOGNjZTAzMDQ2ODE5
|
||||
NjA0MWI0YzM4MGI2MTdjYjAiLCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUBAAEAABKOAgoQj49w
|
||||
ugM/XFoNkMEnAmaPnRIIcFM/RoDbVhcqDFRhc2sgQ3JlYXRlZDABOViFR1nHoBoYQfgRSFnHoBoY
|
||||
Si4KCGNyZXdfa2V5EiIKIGUzOTU2N2I1MDUyOTA5Y2EzMzQwOTg0YjgzODk4MGVhSjEKB2NyZXdf
|
||||
aWQSJgokMDZlOWM3YWMtMTNkMi00ZTgxLWE3MjYtMGViMjJjN2U1ZDcwSi4KCHRhc2tfa2V5EiIK
|
||||
IGE5YTc2Y2E2OTU3ZDBiZmZhNjllYWIyMGI2NjQ4MjJiSjEKB3Rhc2tfaWQSJgokMmEyZDgwNjMt
|
||||
MGQyZC00OGZmLTg2MmEtY2I4YzU0YTIwNjE2egIYAYUBAAEAABKXAQoQQgYNvHzrhiz04CrSnkG0
|
||||
KBII9UsJM/96oEoqClRvb2wgVXNhZ2UwATkQPOFax6AaGEGAmupax6AaGEoaCg5jcmV3YWlfdmVy
|
||||
c2lvbhIICgYwLjk1LjBKIwoJdG9vbF9uYW1lEhYKFEFkZCBpbWFnZSB0byBjb250ZW50Sg4KCGF0
|
||||
dGVtcHRzEgIYAXoCGAGFAQABAAASpAcKEL8pSiN4H/umQhWexA4UYzoSCC+JqZKUlDffKgxDcmV3
|
||||
IENyZWF0ZWQwATnA9r9cx6AaGEGAJMhcx6AaGEoaCg5jcmV3YWlfdmVyc2lvbhIICgYwLjk1LjBK
|
||||
GgoOcHl0aG9uX3ZlcnNpb24SCAoGMy4xMi43Si4KCGNyZXdfa2V5EiIKIDAwYjk0NmJlNDQzNzE0
|
||||
YjNhNDdjMjAxMDFlYjAyZDY2SjEKB2NyZXdfaWQSJgokZDRhZDMyZTUtM2I1NS00OGQ0LTlmYjMt
|
||||
ZTVkOTY0ZGI5NzJhShwKDGNyZXdfcHJvY2VzcxIMCgpzZXF1ZW50aWFsShEKC2NyZXdfbWVtb3J5
|
||||
EgIQAEoaChRjcmV3X251bWJlcl9vZl90YXNrcxICGAFKGwoVY3Jld19udW1iZXJfb2ZfYWdlbnRz
|
||||
EgIYAUrPAgoLY3Jld19hZ2VudHMSvwIKvAJbeyJrZXkiOiAiNGI4YTdiODQwZjk0YmY3ODE4YjVk
|
||||
NTNmNjg5MjdmZDUiLCAiaWQiOiAiNjdlMDhiZDMtMzA5MS00ZTdhLWE4NjQtYTUyOGQ4ZDZlN2Y4
|
||||
IiwgInJvbGUiOiAiUmVwb3J0IFdyaXRlciIsICJ2ZXJib3NlPyI6IGZhbHNlLCAibWF4X2l0ZXIi
|
||||
OiAyMCwgIm1heF9ycG0iOiBudWxsLCAiZnVuY3Rpb25fY2FsbGluZ19sbG0iOiAiIiwgImxsbSI6
|
||||
ICJncHQtNG8iLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29kZV9leGVj
|
||||
dXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMiOiBbXX1d
|
||||
SoICCgpjcmV3X3Rhc2tzEvMBCvABW3sia2V5IjogImI3MTNjODJmZWI5MmM5ZjVjNThiNDBhOTc1
|
||||
NTZiN2FjIiwgImlkIjogIjUyZGMwN2ZjLWJjY2ItNDI4Mi1hZjllLWUyYTkxY2ViMzI0MCIsICJh
|
||||
c3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFnZW50X3Jv
|
||||
bGUiOiAiUmVwb3J0IFdyaXRlciIsICJhZ2VudF9rZXkiOiAiNGI4YTdiODQwZjk0YmY3ODE4YjVk
|
||||
NTNmNjg5MjdmZDUiLCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUBAAEAABKOAgoQFiOJNSnPbaBo
|
||||
fje7Tx2DdBIIwjGhGgyR5BkqDFRhc2sgQ3JlYXRlZDABOaAq1FzHoBoYQah81FzHoBoYSi4KCGNy
|
||||
ZXdfa2V5EiIKIDAwYjk0NmJlNDQzNzE0YjNhNDdjMjAxMDFlYjAyZDY2SjEKB2NyZXdfaWQSJgok
|
||||
ZDRhZDMyZTUtM2I1NS00OGQ0LTlmYjMtZTVkOTY0ZGI5NzJhSi4KCHRhc2tfa2V5EiIKIGI3MTNj
|
||||
ODJmZWI5MmM5ZjVjNThiNDBhOTc1NTZiN2FjSjEKB3Rhc2tfaWQSJgokNTJkYzA3ZmMtYmNjYi00
|
||||
MjgyLWFmOWUtZTJhOTFjZWIzMjQwegIYAYUBAAEAABKOAgoQt0X92psFBaT0eyn1IxJl0RIIpDY4
|
||||
j2AlTioqDFRhc2sgQ3JlYXRlZDABOdgnPV/HoBoYQXi0PV/HoBoYSi4KCGNyZXdfa2V5EiIKIDAw
|
||||
Yjk0NmJlNDQzNzE0YjNhNDdjMjAxMDFlYjAyZDY2SjEKB2NyZXdfaWQSJgokZDRhZDMyZTUtM2I1
|
||||
NS00OGQ0LTlmYjMtZTVkOTY0ZGI5NzJhSi4KCHRhc2tfa2V5EiIKIGI3MTNjODJmZWI5MmM5ZjVj
|
||||
NThiNDBhOTc1NTZiN2FjSjEKB3Rhc2tfaWQSJgokNTJkYzA3ZmMtYmNjYi00MjgyLWFmOWUtZTJh
|
||||
OTFjZWIzMjQwegIYAYUBAAEAABKOAgoQZyIwBbsHH+6dumgTUJNVzxIIMAEwlT69bAwqDFRhc2sg
|
||||
Q3JlYXRlZDABOeh9u2HHoBoYQfghvGHHoBoYSi4KCGNyZXdfa2V5EiIKIDAwYjk0NmJlNDQzNzE0
|
||||
YjNhNDdjMjAxMDFlYjAyZDY2SjEKB2NyZXdfaWQSJgokZDRhZDMyZTUtM2I1NS00OGQ0LTlmYjMt
|
||||
ZTVkOTY0ZGI5NzJhSi4KCHRhc2tfa2V5EiIKIGI3MTNjODJmZWI5MmM5ZjVjNThiNDBhOTc1NTZi
|
||||
N2FjSjEKB3Rhc2tfaWQSJgokNTJkYzA3ZmMtYmNjYi00MjgyLWFmOWUtZTJhOTFjZWIzMjQwegIY
|
||||
AYUBAAEAABKOAgoQNmx90haqHtL8tj3Y948aIhIIaiFn4f7x7RAqDFRhc2sgQ3JlYXRlZDABOTgM
|
||||
nmTHoBoYQZCknmTHoBoYSi4KCGNyZXdfa2V5EiIKIDAwYjk0NmJlNDQzNzE0YjNhNDdjMjAxMDFl
|
||||
YjAyZDY2SjEKB2NyZXdfaWQSJgokZDRhZDMyZTUtM2I1NS00OGQ0LTlmYjMtZTVkOTY0ZGI5NzJh
|
||||
Si4KCHRhc2tfa2V5EiIKIGI3MTNjODJmZWI5MmM5ZjVjNThiNDBhOTc1NTZiN2FjSjEKB3Rhc2tf
|
||||
aWQSJgokNTJkYzA3ZmMtYmNjYi00MjgyLWFmOWUtZTJhOTFjZWIzMjQwegIYAYUBAAEAABKWBwoQ
|
||||
vt1TslFugf+idjOWhVfl9BIIGjt6tt0AKKkqDENyZXcgQ3JlYXRlZDABOWiz12fHoBoYQZj432fH
|
||||
oBoYShoKDmNyZXdhaV92ZXJzaW9uEggKBjAuOTUuMEoaCg5weXRob25fdmVyc2lvbhIICgYzLjEy
|
||||
LjdKLgoIY3Jld19rZXkSIgogZjVkZTY3ZTk5ODUwNTA3NmEyOTM3YjNmZGFhNzc1ZjFKMQoHY3Jl
|
||||
d19pZBImCiQ2MzJjYTc0MC1mNjg2LTRlNGQtOTBmYy00YjZkYmE5ZjViMGRKHAoMY3Jld19wcm9j
|
||||
ZXNzEgwKCnNlcXVlbnRpYWxKEQoLY3Jld19tZW1vcnkSAhAAShoKFGNyZXdfbnVtYmVyX29mX3Rh
|
||||
c2tzEgIYAUobChVjcmV3X251bWJlcl9vZl9hZ2VudHMSAhgBSsgCCgtjcmV3X2FnZW50cxK4Agq1
|
||||
Alt7ImtleSI6ICI2ZjYzZjNlMzU4M2E0NjJmZjNlNzY2MDcxYzgyMTJhZiIsICJpZCI6ICI1ZTZl
|
||||
NTMzNy1iZmMzLTRjZmYtODBlZi1hM2U5NDQ4YjBlYTMiLCAicm9sZSI6ICJXcml0ZXIiLCAidmVy
|
||||
Ym9zZT8iOiBmYWxzZSwgIm1heF9pdGVyIjogMjAsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9u
|
||||
X2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5hYmxlZD8i
|
||||
OiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3JldHJ5X2xpbWl0
|
||||
IjogMiwgInRvb2xzX25hbWVzIjogW119XUr7AQoKY3Jld190YXNrcxLsAQrpAVt7ImtleSI6ICIz
|
||||
ZjMyNzEyMDk2ZmFjYjliNGI2ZWE1NWI3OGViN2M4MCIsICJpZCI6ICI5NDRiZWRmNS0xZjZiLTQw
|
||||
OWEtOTE4Mi04YzMyZTM0MGZmMzQiLCAiYXN5bmNfZXhlY3V0aW9uPyI6IGZhbHNlLCAiaHVtYW5f
|
||||
aW5wdXQ/IjogZmFsc2UsICJhZ2VudF9yb2xlIjogIldyaXRlciIsICJhZ2VudF9rZXkiOiAiNmY2
|
||||
M2YzZTM1ODNhNDYyZmYzZTc2NjA3MWM4MjEyYWYiLCAidG9vbHNfbmFtZXMiOiBbXX1degIYAYUB
|
||||
AAEAABKOAgoQ4leDd4+yGvuAxat0Z7g/uhIInjgmW2jrDBIqDFRhc2sgQ3JlYXRlZDABOXCN62fH
|
||||
oBoYQXjf62fHoBoYSi4KCGNyZXdfa2V5EiIKIGY1ZGU2N2U5OTg1MDUwNzZhMjkzN2IzZmRhYTc3
|
||||
NWYxSjEKB2NyZXdfaWQSJgokNjMyY2E3NDAtZjY4Ni00ZTRkLTkwZmMtNGI2ZGJhOWY1YjBkSi4K
|
||||
CHRhc2tfa2V5EiIKIDNmMzI3MTIwOTZmYWNiOWI0YjZlYTU1Yjc4ZWI3YzgwSjEKB3Rhc2tfaWQS
|
||||
JgokOTQ0YmVkZjUtMWY2Yi00MDlhLTkxODItOGMzMmUzNDBmZjM0egIYAYUBAAEAABKOAgoQ/K3x
|
||||
az8rHR8RbOPAn3/V0xIIkOxMowIIFUoqDFRhc2sgQ3JlYXRlZDABOUCJ7WfHoBoYQcDH7WfHoBoY
|
||||
Si4KCGNyZXdfa2V5EiIKIGY1ZGU2N2U5OTg1MDUwNzZhMjkzN2IzZmRhYTc3NWYxSjEKB2NyZXdf
|
||||
aWQSJgokNjMyY2E3NDAtZjY4Ni00ZTRkLTkwZmMtNGI2ZGJhOWY1YjBkSi4KCHRhc2tfa2V5EiIK
|
||||
IDNmMzI3MTIwOTZmYWNiOWI0YjZlYTU1Yjc4ZWI3YzgwSjEKB3Rhc2tfaWQSJgokOTQ0YmVkZjUt
|
||||
MWY2Yi00MDlhLTkxODItOGMzMmUzNDBmZjM0egIYAYUBAAEAABKeBwoQ/q45KvZiCrfu5bu1k3u9
|
||||
PBII3yPQFsZi+ywqDENyZXcgQ3JlYXRlZDABObA3PWjHoBoYQUDYSGjHoBoYShoKDmNyZXdhaV92
|
||||
ZXJzaW9uEggKBjAuOTUuMEoaCg5weXRob25fdmVyc2lvbhIICgYzLjEyLjdKLgoIY3Jld19rZXkS
|
||||
IgogNzc2NTcyNTMwMGY2NjAwYjI5NjExYmI3ZTAyZDU2ZTZKMQoHY3Jld19pZBImCiQ3NDcwMDVh
|
||||
Yi1lODE0LTQ0YzItOWFlMy1lZTZkYWEzYmMxYjZKHAoMY3Jld19wcm9jZXNzEgwKCnNlcXVlbnRp
|
||||
YWxKEQoLY3Jld19tZW1vcnkSAhAAShoKFGNyZXdfbnVtYmVyX29mX3Rhc2tzEgIYAUobChVjcmV3
|
||||
X251bWJlcl9vZl9hZ2VudHMSAhgBSswCCgtjcmV3X2FnZW50cxK8Agq5Alt7ImtleSI6ICI3YjMz
|
||||
ZjY0ZGQwYjFiYTc4NWUwYmE4YmI1YjUyZjI0NiIsICJpZCI6ICI1ZTA0MzczNC02MGU1LTQwZWQt
|
||||
OGNlNS0wNjQ1MTNmMTkxMzciLCAicm9sZSI6ICJUZXN0IEFnZW50IiwgInZlcmJvc2U/IjogZmFs
|
||||
c2UsICJtYXhfaXRlciI6IDIwLCAibWF4X3JwbSI6IG51bGwsICJmdW5jdGlvbl9jYWxsaW5nX2xs
|
||||
bSI6ICIiLCAibGxtIjogImdwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJh
|
||||
bGxvd19jb2RlX2V4ZWN1dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29s
|
||||
c19uYW1lcyI6IFtdfV1K/wEKCmNyZXdfdGFza3MS8AEK7QFbeyJrZXkiOiAiZDg3OTA0ZWU4MmNh
|
||||
NzVmZWQ1ODY4MTM3ZDRkYzEzNmYiLCAiaWQiOiAiNjdlZmEyZWEtZTQ0Ni00ZWI2LTg5YWMtMzA1
|
||||
ZDUwZjFkODMwIiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZh
|
||||
bHNlLCAiYWdlbnRfcm9sZSI6ICJUZXN0IEFnZW50IiwgImFnZW50X2tleSI6ICI3YjMzZjY0ZGQw
|
||||
YjFiYTc4NWUwYmE4YmI1YjUyZjI0NiIsICJ0b29sc19uYW1lcyI6IFtdfV16AhgBhQEAAQAAEo4C
|
||||
ChAWSoeQUP+DNRqnwCDlpo82Egg4jJLBn5Yi2ioMVGFzayBDcmVhdGVkMAE5+I9WaMegGhhBAOJW
|
||||
aMegGhhKLgoIY3Jld19rZXkSIgogNzc2NTcyNTMwMGY2NjAwYjI5NjExYmI3ZTAyZDU2ZTZKMQoH
|
||||
Y3Jld19pZBImCiQ3NDcwMDVhYi1lODE0LTQ0YzItOWFlMy1lZTZkYWEzYmMxYjZKLgoIdGFza19r
|
||||
ZXkSIgogZDg3OTA0ZWU4MmNhNzVmZWQ1ODY4MTM3ZDRkYzEzNmZKMQoHdGFza19pZBImCiQ2N2Vm
|
||||
YTJlYS1lNDQ2LTRlYjYtODlhYy0zMDVkNTBmMWQ4MzB6AhgBhQEAAQAA
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '32247'
|
||||
Content-Type:
|
||||
- application/x-protobuf
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
- OTel-OTLP-Exporter-Python/1.27.0
|
||||
method: POST
|
||||
uri: https://telemetry.crewai.com:4319/v1/traces
|
||||
response:
|
||||
body:
|
||||
string: "\n\0"
|
||||
headers:
|
||||
Content-Length:
|
||||
- '2'
|
||||
Content-Type:
|
||||
- application/x-protobuf
|
||||
Date:
|
||||
- Tue, 14 Jan 2025 17:56:25 GMT
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test agent
|
||||
backstory\nYour personal goal is: Test agent goal\nTo give my best complete
|
||||
final answer to the task respond using the exact following format:\n\nThought:
|
||||
I now can give a great answer\nFinal Answer: Your final answer must be the great
|
||||
and the most complete as possible, it must be outcome described.\n\nI MUST use
|
||||
these formats, my job depends on it!"}, {"role": "user", "content": "\nCurrent
|
||||
Task: Test task description\n\nThis is the expect criteria for your final answer:
|
||||
Test expected output\nyou MUST return the actual complete content as the final
|
||||
answer, not a summary.\n\nBegin! This is VERY important to you, use the tools
|
||||
available and give your best Final Answer, your job depends on it!\n\nThought:"}],
|
||||
"model": "gpt-4o", "stop": ["\nObservation:"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '407'
|
||||
- '838'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- _cfuvid=SlnUP7AT9jJlQiN.Fm1c7MDyo78_hBRAz8PoabvHVSU-1736018539826-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.59.6
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
- 1.59.6
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
- 3.12.7
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DIjv3LqL0QS4iw3OM5b28B4VOMZPA\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1773358789,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"Test expected output\",\n \"refusal\":
|
||||
null,\n \"annotations\": []\n },\n \"logprobs\": null,\n
|
||||
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
72,\n \"completion_tokens\": 3,\n \"total_tokens\": 75,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_5e793402c9\"\n}\n"
|
||||
string: "{\n \"id\": \"chatcmpl-ApfRLkycSd0vwuTw50dfB5bgIoWiC\",\n \"object\"\
|
||||
: \"chat.completion\",\n \"created\": 1736877387,\n \"model\": \"gpt-4o-2024-08-06\"\
|
||||
,\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \
|
||||
\ \"role\": \"assistant\",\n \"content\": \"I now can give a great\
|
||||
\ answer \\nFinal Answer: The final answer must be the great and the most\
|
||||
\ complete as possible, it must be outcome described.\",\n \"refusal\"\
|
||||
: null\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\
|
||||
\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 158,\n \"completion_tokens\"\
|
||||
: 31,\n \"total_tokens\": 189,\n \"prompt_tokens_details\": {\n \
|
||||
\ \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\"\
|
||||
: {\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"\
|
||||
accepted_prediction_tokens\": 0,\n \"rejected_prediction_tokens\": 0\n\
|
||||
\ }\n },\n \"service_tier\": \"default\",\n \"system_fingerprint\":\
|
||||
\ \"fp_50cad350e4\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9db6a3f31e087b0e-EWR
|
||||
CF-RAY:
|
||||
- 901f80a64cc6bd25-ATL
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 12 Mar 2026 23:39:50 GMT
|
||||
- Tue, 14 Jan 2025 17:56:28 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Set-Cookie:
|
||||
- __cf_bm=A.PJUaUHPGyIr2pwNz44ei0seKXMH7czqXc5dA_MzD0-1736877388-1.0.1.1-jC2Lo7dl92z6qdY8mxRekSqg68TqMNsvyjPoNVXBfKNO6hHwL5BKWSBeA2i9hYWN2DBBLvHWeFXq1nXCKNcnlQ;
|
||||
path=/; expires=Tue, 14-Jan-25 18:26:28 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=kERLxnulwhkdPi_RxnQLZV8G2Zbub8n_KYkKSL6uke8-1736877388108-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '360'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
- '1020'
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
- '30000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
- '29999807'
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
- 0s
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
- req_4ceac9bc8ae57f631959b91d2ab63c4d
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test agent
|
||||
backstory\nYour personal goal is: Test agent goal\nTo give my best complete
|
||||
final answer to the task respond using the exact following format:\n\nThought:
|
||||
I now can give a great answer\nFinal Answer: Your final answer must be the great
|
||||
and the most complete as possible, it must be outcome described.\n\nI MUST use
|
||||
these formats, my job depends on it!"}, {"role": "user", "content": "\nCurrent
|
||||
Task: Test task description\n\nThis is the expected criteria for your final
|
||||
answer: Test expected output\nyou MUST return the actual complete content as
|
||||
the final answer, not a summary.\n\nBegin! This is VERY important to you, use
|
||||
the tools available and give your best Final Answer, your job depends on it!\n\nThought:"}],
|
||||
"model": "gpt-4o", "stop": ["\nObservation:"]}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '840'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.61.0
|
||||
x-stainless-arch:
|
||||
- x64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.61.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.12.9
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-BExKOliqPgvHyozZaBu5oN50CHtsa\",\n \"object\"\
|
||||
: \"chat.completion\",\n \"created\": 1742904348,\n \"model\": \"gpt-4o-2024-08-06\"\
|
||||
,\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \
|
||||
\ \"role\": \"assistant\",\n \"content\": \"I now can give a great\
|
||||
\ answer \\nFinal Answer: Test expected output\",\n \"refusal\": null,\n\
|
||||
\ \"annotations\": []\n },\n \"logprobs\": null,\n \"\
|
||||
finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\"\
|
||||
: 158,\n \"completion_tokens\": 15,\n \"total_tokens\": 173,\n \"\
|
||||
prompt_tokens_details\": {\n \"cached_tokens\": 0,\n \"audio_tokens\"\
|
||||
: 0\n },\n \"completion_tokens_details\": {\n \"reasoning_tokens\"\
|
||||
: 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": 0,\n\
|
||||
\ \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\"\
|
||||
: \"default\",\n \"system_fingerprint\": \"fp_90d33c15d4\"\n}\n"
|
||||
headers:
|
||||
CF-RAY:
|
||||
- 925e4749af02f227-GRU
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Tue, 25 Mar 2025 12:05:48 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Set-Cookie:
|
||||
- __cf_bm=VHa7Z7dJYptxXpaMxgldvK6HqIM.m74xpi.80N_EBDc-1742904348-1.0.1.1-VthD2riCSnAprFYhOZxfIrTjT33tybJHpHWB25Q_Hx4vuACCyF00tix6e6eorDReGcW3jb5cUzbGqYi47TrMsS4LYjxBv5eCo7cU9OuFajs;
|
||||
path=/; expires=Tue, 25-Mar-25 12:35:48 GMT; domain=.api.openai.com; HttpOnly;
|
||||
Secure; SameSite=None
|
||||
- _cfuvid=Is8fSaH3lU8yHyT3fI7cRZiDqIYSI6sPpzfzvEV8HMc-1742904348760-0.0.1.1-604800000;
|
||||
path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- X-Request-ID
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '377'
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-ratelimit-limit-requests:
|
||||
- '50000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- '150000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- '49999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- '149999822'
|
||||
x-ratelimit-reset-requests:
|
||||
- 1ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- 0s
|
||||
x-request-id:
|
||||
- req_fd6b93e3b1a30868482c72306e7f63c2
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
|
||||
@@ -45,89 +45,78 @@ interactions:
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
- 3.13.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DIjlpMNPWid0bFT3tJ0wlsOZelKz7\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1773358217,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
string: "{\n \"id\": \"chatcmpl-DDFzCiMzYEJMnv9oV3KbMUwH6TGRO\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1772052086,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"- **The Future of Autonomous AI Agents:
|
||||
From Task Automation to Decision Making** \\nExploring the evolving landscape
|
||||
of autonomous AI agents reveals a fascinating journey from simple task automation
|
||||
to complex decision-making systems capable of self-learning and adaptation.
|
||||
An article diving into this topic could unravel how cutting-edge advancements
|
||||
in reinforcement learning, natural language processing, and multi-agent systems
|
||||
are propelling AI agents beyond rigid scripts into dynamic collaborators and
|
||||
problem solvers. It would offer readers insights into real-world applications\u2014such
|
||||
as autonomous drones, financial trading bots, and personalized digital assistants\u2014and
|
||||
speculate on ethical considerations and regulatory frameworks shaping their
|
||||
future. This exploration emphasizes the transformative potential and the challenges
|
||||
that autonomous AI agents pose to industries and society at large.\\n\\n-
|
||||
**Bridging the Gap Between AI Agents and Human Collaboration** \\nAI agents
|
||||
are no longer isolated tools but increasingly integral collaborators in creative
|
||||
and professional workflows. An article tackling this theme could examine the
|
||||
latest progress in human-AI interaction models, including explainability,
|
||||
adaptability, and collaborative problem-solving. It would highlight how AI
|
||||
agents are augmenting human capabilities in fields like healthcare diagnostics,
|
||||
content creation, customer service, and software development. The narrative
|
||||
could also include case studies demonstrating successful AI-human partnerships
|
||||
along with the psychological and ergonomic aspects critical to designing AI
|
||||
agents that work harmoniously with humans. Such a piece would resonate deeply
|
||||
with readers interested in the symbiosis between artificial intelligence and
|
||||
human ingenuity.\\n\\n- **The Rise of AI Agents in Cybersecurity: Defense
|
||||
and Offense** \\nIn cybersecurity, AI agents are becoming indispensable,
|
||||
not only in defensive roles but also on the offensive front. An article focused
|
||||
on this area could deliver a comprehensive analysis of how AI agents detect
|
||||
and respond to threats in real time, employing techniques like anomaly detection,
|
||||
behavioral analysis, and automated incident response. Additionally, it would
|
||||
delve into the darker side: the use of AI agents by malicious actors for sophisticated
|
||||
cyber-attacks, including adaptive malware and social engineering bots. This
|
||||
dual perspective could provide a thrilling and nuanced investigation of the
|
||||
cybersecurity landscape dominated by AI, shedding light on strategic innovations,
|
||||
emerging threats, and the ongoing arms race between attackers and defenders.\\n\\n-
|
||||
**AI Agents in Startups: Revolutionizing Business Models and Customer Experience**
|
||||
\ \\nStartups are leveraging AI agents as a catalyst for innovation, scalability,
|
||||
and personalization, fundamentally transforming traditional business models.
|
||||
An article on this topic could survey real examples where AI agents streamline
|
||||
operations, enable hyper-personalized marketing, automate customer support,
|
||||
and generate actionable business insights. It would analyze how the integration
|
||||
of AI agents accelerates product-market fit through rapid iteration and data-driven
|
||||
decision-making. Moreover, the article could explore challenges unique to
|
||||
startup environments, such as resource constraints, technology adoption hurdles,
|
||||
and ethical considerations around AI deployment. This comprehensive view would
|
||||
inspire entrepreneurs and investors alike, spotlighting AI agents as game
|
||||
changers in the startup ecosystem.\\n\\n- **Ethical and Societal Implications
|
||||
of Delegating Decisions to AI Agents** \\nAs AI agents increasingly take
|
||||
on decision-making roles with significant real-world impact, ethical and societal
|
||||
questions come sharply into focus. An article on this theme could dissect
|
||||
challenges concerning accountability, transparency, bias, and human autonomy.
|
||||
It would provide an in-depth treatment of case studies where AI agents\u2019
|
||||
decisions led to unintended consequences or public backlash, and the regulatory
|
||||
and design frameworks proposed to mitigate these risks. Furthermore, the article
|
||||
could explore philosophical questions about trust, control, and the future
|
||||
relationship between humans and AI decision-makers. By unpacking the complex
|
||||
moral landscape surrounding AI agents, this piece would offer critical insight
|
||||
for policymakers, developers, and society at large.\",\n \"refusal\":
|
||||
null,\n \"annotations\": []\n },\n \"logprobs\": null,\n
|
||||
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
164,\n \"completion_tokens\": 720,\n \"total_tokens\": 884,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
\"assistant\",\n \"content\": \"- **The Rise of Autonomous AI Agents:
|
||||
Redefining Productivity and Creativity** \\n This article would dive into
|
||||
how autonomous AI agents\u2014intelligent software systems capable of independently
|
||||
performing complex tasks\u2014are transforming industries by augmenting human
|
||||
productivity and creativity. It would explore real-world use cases from automated
|
||||
content generation and customer support bots to AI-driven design and research
|
||||
assistants, illustrating how these agents reduce repetitive workload and open
|
||||
new avenues for innovation. The article could also analyze challenges such
|
||||
as ethical considerations, decision-making transparency, and integration with
|
||||
existing workflows, offering readers a comprehensive view of how autonomous
|
||||
AI agents are reshaping the future of work.\\n\\n- **Bridging Human-AI Collaboration:
|
||||
Designing AI Agents for Intuitive Interaction** \\n This piece would investigate
|
||||
the critical design principles behind successful human-AI collaboration, focusing
|
||||
on building AI agents that communicate and interact naturally with users.
|
||||
From natural language processing nuances to adaptive learning from user behavior,
|
||||
the article would examine how these technological advancements create seamless
|
||||
partnerships between humans and machines. Highlighting case studies in healthcare,
|
||||
finance, and creative industries, it would demonstrate the importance of trust,
|
||||
interpretability, and empathy in AI agent interfaces, emphasizing how better-designed
|
||||
interactions can dramatically improve adoption and effectiveness.\\n\\n- **The
|
||||
Ethical Frontier: Navigating Bias and Accountability in AI Agents** \\n Exploring
|
||||
the ethical implications of deploying AI agents at scale, this article would
|
||||
address pressing issues like algorithmic bias, privacy concerns, and accountability
|
||||
in autonomous decision-making. It would analyze how biases embedded in training
|
||||
data can propagate through AI agents, impacting critical outcomes in hiring,
|
||||
lending, and law enforcement. The article would also discuss emerging regulatory
|
||||
frameworks, best practices for auditing AI agents, and the role of interdisciplinary
|
||||
ethics teams in ensuring these technologies are fair, transparent, and responsible,
|
||||
helping readers grasp the societal responsibilities accompanying AI advancement.\\n\\n-
|
||||
**AI Agents in Startups: Driving Innovation and Competitive Advantage** \\n
|
||||
\ Focused on the startup ecosystem, this article would explore how emerging
|
||||
companies leverage AI agents to disrupt markets and scale rapidly with limited
|
||||
resources. It would profile startups using AI agents for customer acquisition,
|
||||
personalized marketing, operational automation, and product development, illustrating
|
||||
how these tools enable lean teams to achieve much more. The narrative would
|
||||
consider investment trends, challenges faced by startups incorporating AI
|
||||
agents, and strategies for balancing innovation with reliability, providing
|
||||
entrepreneurs and investors with valuable insights into harnessing AI agents
|
||||
for meaningful growth.\\n\\n- **From Data to Decision: How AI Agents Transform
|
||||
Business Intelligence** \\n This article would delve into the role of AI
|
||||
agents as intelligent intermediaries in business intelligence (BI) systems,
|
||||
automating data analysis and delivering actionable insights in real-time.
|
||||
It would explain how AI agents can parse vast datasets, identify trends, generate
|
||||
forecasts, and even suggest strategic decisions without constant human oversight.
|
||||
Highlighting innovations like conversational BI interfaces and predictive
|
||||
analytics agents, the article would underscore how businesses of all sizes
|
||||
can democratize data-driven decision-making, driving agility and competitive
|
||||
advantage in increasingly complex markets.\",\n \"refusal\": null,\n
|
||||
\ \"annotations\": []\n },\n \"logprobs\": null,\n \"finish_reason\":
|
||||
\"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\": 164,\n \"completion_tokens\":
|
||||
597,\n \"total_tokens\": 761,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_ae0f8c9a7b\"\n}\n"
|
||||
\"default\",\n \"system_fingerprint\": \"fp_417e90869b\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9db695fa4bf9b911-EWR
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 12 Mar 2026 23:30:27 GMT
|
||||
- Wed, 25 Feb 2026 20:41:39 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
@@ -140,10 +129,12 @@ interactions:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '9053'
|
||||
- '13437'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
@@ -185,75 +176,68 @@ interactions:
|
||||
list of ideas with their paragraph and your notes. task_expected_output: 5 bullet
|
||||
points with a paragraph for each idea. agent: Researcher agent_goal: Make the
|
||||
best research and analysis on content about AI and AI agents Task Output: -
|
||||
**The Future of Autonomous AI Agents: From Task Automation to Decision Making**
|
||||
\ \\nExploring the evolving landscape of autonomous AI agents reveals a fascinating
|
||||
journey from simple task automation to complex decision-making systems capable
|
||||
of self-learning and adaptation. An article diving into this topic could unravel
|
||||
how cutting-edge advancements in reinforcement learning, natural language processing,
|
||||
and multi-agent systems are propelling AI agents beyond rigid scripts into dynamic
|
||||
collaborators and problem solvers. It would offer readers insights into real-world
|
||||
applications\u2014such as autonomous drones, financial trading bots, and personalized
|
||||
digital assistants\u2014and speculate on ethical considerations and regulatory
|
||||
frameworks shaping their future. This exploration emphasizes the transformative
|
||||
potential and the challenges that autonomous AI agents pose to industries and
|
||||
society at large.\\n\\n- **Bridging the Gap Between AI Agents and Human Collaboration**
|
||||
\ \\nAI agents are no longer isolated tools but increasingly integral collaborators
|
||||
in creative and professional workflows. An article tackling this theme could
|
||||
examine the latest progress in human-AI interaction models, including explainability,
|
||||
adaptability, and collaborative problem-solving. It would highlight how AI agents
|
||||
are augmenting human capabilities in fields like healthcare diagnostics, content
|
||||
creation, customer service, and software development. The narrative could also
|
||||
include case studies demonstrating successful AI-human partnerships along with
|
||||
the psychological and ergonomic aspects critical to designing AI agents that
|
||||
work harmoniously with humans. Such a piece would resonate deeply with readers
|
||||
interested in the symbiosis between artificial intelligence and human ingenuity.\\n\\n-
|
||||
**The Rise of AI Agents in Cybersecurity: Defense and Offense** \\nIn cybersecurity,
|
||||
AI agents are becoming indispensable, not only in defensive roles but also on
|
||||
the offensive front. An article focused on this area could deliver a comprehensive
|
||||
analysis of how AI agents detect and respond to threats in real time, employing
|
||||
techniques like anomaly detection, behavioral analysis, and automated incident
|
||||
response. Additionally, it would delve into the darker side: the use of AI agents
|
||||
by malicious actors for sophisticated cyber-attacks, including adaptive malware
|
||||
and social engineering bots. This dual perspective could provide a thrilling
|
||||
and nuanced investigation of the cybersecurity landscape dominated by AI, shedding
|
||||
light on strategic innovations, emerging threats, and the ongoing arms race
|
||||
between attackers and defenders.\\n\\n- **AI Agents in Startups: Revolutionizing
|
||||
Business Models and Customer Experience** \\nStartups are leveraging AI agents
|
||||
as a catalyst for innovation, scalability, and personalization, fundamentally
|
||||
transforming traditional business models. An article on this topic could survey
|
||||
real examples where AI agents streamline operations, enable hyper-personalized
|
||||
marketing, automate customer support, and generate actionable business insights.
|
||||
It would analyze how the integration of AI agents accelerates product-market
|
||||
fit through rapid iteration and data-driven decision-making. Moreover, the article
|
||||
could explore challenges unique to startup environments, such as resource constraints,
|
||||
technology adoption hurdles, and ethical considerations around AI deployment.
|
||||
This comprehensive view would inspire entrepreneurs and investors alike, spotlighting
|
||||
AI agents as game changers in the startup ecosystem.\\n\\n- **Ethical and Societal
|
||||
Implications of Delegating Decisions to AI Agents** \\nAs AI agents increasingly
|
||||
take on decision-making roles with significant real-world impact, ethical and
|
||||
societal questions come sharply into focus. An article on this theme could dissect
|
||||
challenges concerning accountability, transparency, bias, and human autonomy.
|
||||
It would provide an in-depth treatment of case studies where AI agents\u2019
|
||||
decisions led to unintended consequences or public backlash, and the regulatory
|
||||
and design frameworks proposed to mitigate these risks. Furthermore, the article
|
||||
could explore philosophical questions about trust, control, and the future relationship
|
||||
between humans and AI decision-makers. By unpacking the complex moral landscape
|
||||
surrounding AI agents, this piece would offer critical insight for policymakers,
|
||||
developers, and society at large.\\n\\nThis is the expected criteria for your
|
||||
final answer: Evaluation Score from 1 to 10 based on the performance of the
|
||||
agents on the tasks\\nyou MUST return the actual complete content as the final
|
||||
answer, not a summary.\\nFormat your final answer according to the following
|
||||
OpenAPI schema: {\\n \\\"properties\\\": {\\n \\\"quality\\\": {\\n \\\"description\\\":
|
||||
\\\"A score from 1 to 10 evaluating on completion, quality, and overall performance
|
||||
from the task_description and task_expected_output to the actual Task Output.\\\",\\n
|
||||
\ \\\"title\\\": \\\"Quality\\\",\\n \\\"type\\\": \\\"number\\\"\\n
|
||||
\ }\\n },\\n \\\"required\\\": [\\n \\\"quality\\\"\\n ],\\n \\\"title\\\":
|
||||
\\\"TaskEvaluationPydanticOutput\\\",\\n \\\"type\\\": \\\"object\\\",\\n \\\"additionalProperties\\\":
|
||||
false\\n}\\n\\nIMPORTANT: Preserve the original content exactly as-is. Do NOT
|
||||
rewrite, paraphrase, or modify the meaning of the content. Only structure it
|
||||
to match the schema format.\\n\\nDo not include the OpenAPI schema in the final
|
||||
output. Ensure the final output does not include any code block markers like
|
||||
```json or ```python.\\n\\nProvide your complete response:\"}],\"model\":\"gpt-4o-mini\",\"response_format\":{\"type\":\"json_schema\",\"json_schema\":{\"schema\":{\"properties\":{\"quality\":{\"description\":\"A
|
||||
**The Rise of Autonomous AI Agents: Redefining Productivity and Creativity**
|
||||
\ \\n This article would dive into how autonomous AI agents\u2014intelligent
|
||||
software systems capable of independently performing complex tasks\u2014are
|
||||
transforming industries by augmenting human productivity and creativity. It
|
||||
would explore real-world use cases from automated content generation and customer
|
||||
support bots to AI-driven design and research assistants, illustrating how these
|
||||
agents reduce repetitive workload and open new avenues for innovation. The article
|
||||
could also analyze challenges such as ethical considerations, decision-making
|
||||
transparency, and integration with existing workflows, offering readers a comprehensive
|
||||
view of how autonomous AI agents are reshaping the future of work.\\n\\n- **Bridging
|
||||
Human-AI Collaboration: Designing AI Agents for Intuitive Interaction** \\n
|
||||
\ This piece would investigate the critical design principles behind successful
|
||||
human-AI collaboration, focusing on building AI agents that communicate and
|
||||
interact naturally with users. From natural language processing nuances to adaptive
|
||||
learning from user behavior, the article would examine how these technological
|
||||
advancements create seamless partnerships between humans and machines. Highlighting
|
||||
case studies in healthcare, finance, and creative industries, it would demonstrate
|
||||
the importance of trust, interpretability, and empathy in AI agent interfaces,
|
||||
emphasizing how better-designed interactions can dramatically improve adoption
|
||||
and effectiveness.\\n\\n- **The Ethical Frontier: Navigating Bias and Accountability
|
||||
in AI Agents** \\n Exploring the ethical implications of deploying AI agents
|
||||
at scale, this article would address pressing issues like algorithmic bias,
|
||||
privacy concerns, and accountability in autonomous decision-making. It would
|
||||
analyze how biases embedded in training data can propagate through AI agents,
|
||||
impacting critical outcomes in hiring, lending, and law enforcement. The article
|
||||
would also discuss emerging regulatory frameworks, best practices for auditing
|
||||
AI agents, and the role of interdisciplinary ethics teams in ensuring these
|
||||
technologies are fair, transparent, and responsible, helping readers grasp the
|
||||
societal responsibilities accompanying AI advancement.\\n\\n- **AI Agents in
|
||||
Startups: Driving Innovation and Competitive Advantage** \\n Focused on the
|
||||
startup ecosystem, this article would explore how emerging companies leverage
|
||||
AI agents to disrupt markets and scale rapidly with limited resources. It would
|
||||
profile startups using AI agents for customer acquisition, personalized marketing,
|
||||
operational automation, and product development, illustrating how these tools
|
||||
enable lean teams to achieve much more. The narrative would consider investment
|
||||
trends, challenges faced by startups incorporating AI agents, and strategies
|
||||
for balancing innovation with reliability, providing entrepreneurs and investors
|
||||
with valuable insights into harnessing AI agents for meaningful growth.\\n\\n-
|
||||
**From Data to Decision: How AI Agents Transform Business Intelligence** \\n
|
||||
\ This article would delve into the role of AI agents as intelligent intermediaries
|
||||
in business intelligence (BI) systems, automating data analysis and delivering
|
||||
actionable insights in real-time. It would explain how AI agents can parse vast
|
||||
datasets, identify trends, generate forecasts, and even suggest strategic decisions
|
||||
without constant human oversight. Highlighting innovations like conversational
|
||||
BI interfaces and predictive analytics agents, the article would underscore
|
||||
how businesses of all sizes can democratize data-driven decision-making, driving
|
||||
agility and competitive advantage in increasingly complex markets.\\n\\nThis
|
||||
is the expected criteria for your final answer: Evaluation Score from 1 to 10
|
||||
based on the performance of the agents on the tasks\\nyou MUST return the actual
|
||||
complete content as the final answer, not a summary.\\nFormat your final answer
|
||||
according to the following OpenAPI schema: {\\n \\\"properties\\\": {\\n \\\"quality\\\":
|
||||
{\\n \\\"description\\\": \\\"A score from 1 to 10 evaluating on completion,
|
||||
quality, and overall performance from the task_description and task_expected_output
|
||||
to the actual Task Output.\\\",\\n \\\"title\\\": \\\"Quality\\\",\\n \\\"type\\\":
|
||||
\\\"number\\\"\\n }\\n },\\n \\\"required\\\": [\\n \\\"quality\\\"\\n
|
||||
\ ],\\n \\\"title\\\": \\\"TaskEvaluationPydanticOutput\\\",\\n \\\"type\\\":
|
||||
\\\"object\\\",\\n \\\"additionalProperties\\\": false\\n}\\n\\nIMPORTANT:
|
||||
Preserve the original content exactly as-is. Do NOT rewrite, paraphrase, or
|
||||
modify the meaning of the content. Only structure it to match the schema format.\\n\\nDo
|
||||
not include the OpenAPI schema in the final output. Ensure the final output
|
||||
does not include any code block markers like ```json or ```python.\\n\\nProvide
|
||||
your complete response:\"}],\"model\":\"gpt-4o-mini\",\"response_format\":{\"type\":\"json_schema\",\"json_schema\":{\"schema\":{\"properties\":{\"quality\":{\"description\":\"A
|
||||
score from 1 to 10 evaluating on completion, quality, and overall performance
|
||||
from the task_description and task_expected_output to the actual Task Output.\",\"title\":\"Quality\",\"type\":\"number\"}},\"required\":[\"quality\"],\"title\":\"TaskEvaluationPydanticOutput\",\"type\":\"object\",\"additionalProperties\":false},\"name\":\"TaskEvaluationPydanticOutput\",\"strict\":true}},\"stream\":false}"
|
||||
headers:
|
||||
@@ -268,7 +252,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '7184'
|
||||
- '6502'
|
||||
content-type:
|
||||
- application/json
|
||||
host:
|
||||
@@ -292,33 +276,31 @@ interactions:
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
- 3.13.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DIjm0WxDVIL9NNzw98XHHh3cA4Yeh\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1773358228,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
|
||||
string: "{\n \"id\": \"chatcmpl-DDFzQTBe214rOuf82URXmgkuNj5u4\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1772052100,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"{\\\"quality\\\":9}\",\n \"refusal\":
|
||||
null,\n \"annotations\": []\n },\n \"logprobs\": null,\n
|
||||
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
1257,\n \"completion_tokens\": 5,\n \"total_tokens\": 1262,\n \"prompt_tokens_details\":
|
||||
1134,\n \"completion_tokens\": 5,\n \"total_tokens\": 1139,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_e609550549\"\n}\n"
|
||||
\"default\",\n \"system_fingerprint\": \"fp_bd4be55b21\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9db696379bb48095-EWR
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 12 Mar 2026 23:30:28 GMT
|
||||
- Wed, 25 Feb 2026 20:41:40 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
@@ -331,10 +313,12 @@ interactions:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '380'
|
||||
- '241'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
@@ -372,62 +356,54 @@ interactions:
|
||||
paragraph and your notes.\\n\\nThis is the expected criteria for your final
|
||||
answer: 5 bullet points with a paragraph for each idea.\\nyou MUST return the
|
||||
actual complete content as the final answer, not a summary.\\n\\nProvide your
|
||||
complete response:\"},{\"role\":\"assistant\",\"content\":\"- **The Future of
|
||||
Autonomous AI Agents: From Task Automation to Decision Making** \\nExploring
|
||||
the evolving landscape of autonomous AI agents reveals a fascinating journey
|
||||
from simple task automation to complex decision-making systems capable of self-learning
|
||||
and adaptation. An article diving into this topic could unravel how cutting-edge
|
||||
advancements in reinforcement learning, natural language processing, and multi-agent
|
||||
systems are propelling AI agents beyond rigid scripts into dynamic collaborators
|
||||
and problem solvers. It would offer readers insights into real-world applications\u2014such
|
||||
as autonomous drones, financial trading bots, and personalized digital assistants\u2014and
|
||||
speculate on ethical considerations and regulatory frameworks shaping their
|
||||
future. This exploration emphasizes the transformative potential and the challenges
|
||||
that autonomous AI agents pose to industries and society at large.\\n\\n- **Bridging
|
||||
the Gap Between AI Agents and Human Collaboration** \\nAI agents are no longer
|
||||
isolated tools but increasingly integral collaborators in creative and professional
|
||||
workflows. An article tackling this theme could examine the latest progress
|
||||
in human-AI interaction models, including explainability, adaptability, and
|
||||
collaborative problem-solving. It would highlight how AI agents are augmenting
|
||||
human capabilities in fields like healthcare diagnostics, content creation,
|
||||
customer service, and software development. The narrative could also include
|
||||
case studies demonstrating successful AI-human partnerships along with the psychological
|
||||
and ergonomic aspects critical to designing AI agents that work harmoniously
|
||||
with humans. Such a piece would resonate deeply with readers interested in the
|
||||
symbiosis between artificial intelligence and human ingenuity.\\n\\n- **The
|
||||
Rise of AI Agents in Cybersecurity: Defense and Offense** \\nIn cybersecurity,
|
||||
AI agents are becoming indispensable, not only in defensive roles but also on
|
||||
the offensive front. An article focused on this area could deliver a comprehensive
|
||||
analysis of how AI agents detect and respond to threats in real time, employing
|
||||
techniques like anomaly detection, behavioral analysis, and automated incident
|
||||
response. Additionally, it would delve into the darker side: the use of AI agents
|
||||
by malicious actors for sophisticated cyber-attacks, including adaptive malware
|
||||
and social engineering bots. This dual perspective could provide a thrilling
|
||||
and nuanced investigation of the cybersecurity landscape dominated by AI, shedding
|
||||
light on strategic innovations, emerging threats, and the ongoing arms race
|
||||
between attackers and defenders.\\n\\n- **AI Agents in Startups: Revolutionizing
|
||||
Business Models and Customer Experience** \\nStartups are leveraging AI agents
|
||||
as a catalyst for innovation, scalability, and personalization, fundamentally
|
||||
transforming traditional business models. An article on this topic could survey
|
||||
real examples where AI agents streamline operations, enable hyper-personalized
|
||||
marketing, automate customer support, and generate actionable business insights.
|
||||
It would analyze how the integration of AI agents accelerates product-market
|
||||
fit through rapid iteration and data-driven decision-making. Moreover, the article
|
||||
could explore challenges unique to startup environments, such as resource constraints,
|
||||
technology adoption hurdles, and ethical considerations around AI deployment.
|
||||
This comprehensive view would inspire entrepreneurs and investors alike, spotlighting
|
||||
AI agents as game changers in the startup ecosystem.\\n\\n- **Ethical and Societal
|
||||
Implications of Delegating Decisions to AI Agents** \\nAs AI agents increasingly
|
||||
take on decision-making roles with significant real-world impact, ethical and
|
||||
societal questions come sharply into focus. An article on this theme could dissect
|
||||
challenges concerning accountability, transparency, bias, and human autonomy.
|
||||
It would provide an in-depth treatment of case studies where AI agents\u2019
|
||||
decisions led to unintended consequences or public backlash, and the regulatory
|
||||
and design frameworks proposed to mitigate these risks. Furthermore, the article
|
||||
could explore philosophical questions about trust, control, and the future relationship
|
||||
between humans and AI decision-makers. By unpacking the complex moral landscape
|
||||
surrounding AI agents, this piece would offer critical insight for policymakers,
|
||||
developers, and society at large.\"},{\"role\":\"system\",\"content\":\"You
|
||||
complete response:\"},{\"role\":\"assistant\",\"content\":\"- **The Rise of
|
||||
Autonomous AI Agents: Redefining Productivity and Creativity** \\n This article
|
||||
would dive into how autonomous AI agents\u2014intelligent software systems capable
|
||||
of independently performing complex tasks\u2014are transforming industries by
|
||||
augmenting human productivity and creativity. It would explore real-world use
|
||||
cases from automated content generation and customer support bots to AI-driven
|
||||
design and research assistants, illustrating how these agents reduce repetitive
|
||||
workload and open new avenues for innovation. The article could also analyze
|
||||
challenges such as ethical considerations, decision-making transparency, and
|
||||
integration with existing workflows, offering readers a comprehensive view of
|
||||
how autonomous AI agents are reshaping the future of work.\\n\\n- **Bridging
|
||||
Human-AI Collaboration: Designing AI Agents for Intuitive Interaction** \\n
|
||||
\ This piece would investigate the critical design principles behind successful
|
||||
human-AI collaboration, focusing on building AI agents that communicate and
|
||||
interact naturally with users. From natural language processing nuances to adaptive
|
||||
learning from user behavior, the article would examine how these technological
|
||||
advancements create seamless partnerships between humans and machines. Highlighting
|
||||
case studies in healthcare, finance, and creative industries, it would demonstrate
|
||||
the importance of trust, interpretability, and empathy in AI agent interfaces,
|
||||
emphasizing how better-designed interactions can dramatically improve adoption
|
||||
and effectiveness.\\n\\n- **The Ethical Frontier: Navigating Bias and Accountability
|
||||
in AI Agents** \\n Exploring the ethical implications of deploying AI agents
|
||||
at scale, this article would address pressing issues like algorithmic bias,
|
||||
privacy concerns, and accountability in autonomous decision-making. It would
|
||||
analyze how biases embedded in training data can propagate through AI agents,
|
||||
impacting critical outcomes in hiring, lending, and law enforcement. The article
|
||||
would also discuss emerging regulatory frameworks, best practices for auditing
|
||||
AI agents, and the role of interdisciplinary ethics teams in ensuring these
|
||||
technologies are fair, transparent, and responsible, helping readers grasp the
|
||||
societal responsibilities accompanying AI advancement.\\n\\n- **AI Agents in
|
||||
Startups: Driving Innovation and Competitive Advantage** \\n Focused on the
|
||||
startup ecosystem, this article would explore how emerging companies leverage
|
||||
AI agents to disrupt markets and scale rapidly with limited resources. It would
|
||||
profile startups using AI agents for customer acquisition, personalized marketing,
|
||||
operational automation, and product development, illustrating how these tools
|
||||
enable lean teams to achieve much more. The narrative would consider investment
|
||||
trends, challenges faced by startups incorporating AI agents, and strategies
|
||||
for balancing innovation with reliability, providing entrepreneurs and investors
|
||||
with valuable insights into harnessing AI agents for meaningful growth.\\n\\n-
|
||||
**From Data to Decision: How AI Agents Transform Business Intelligence** \\n
|
||||
\ This article would delve into the role of AI agents as intelligent intermediaries
|
||||
in business intelligence (BI) systems, automating data analysis and delivering
|
||||
actionable insights in real-time. It would explain how AI agents can parse vast
|
||||
datasets, identify trends, generate forecasts, and even suggest strategic decisions
|
||||
without constant human oversight. Highlighting innovations like conversational
|
||||
BI interfaces and predictive analytics agents, the article would underscore
|
||||
how businesses of all sizes can democratize data-driven decision-making, driving
|
||||
agility and competitive advantage in increasingly complex markets.\"},{\"role\":\"system\",\"content\":\"You
|
||||
are Researcher. You're an expert researcher, specialized in technology, software
|
||||
engineering, AI and startups. You work as a freelancer and is now working on
|
||||
doing research and analysis for a new customer.\\nYour personal goal is: Make
|
||||
@@ -451,7 +427,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '6164'
|
||||
- '5482'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
@@ -475,88 +451,88 @@ interactions:
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
- 3.13.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DIjm02rlgGyxm8CuBCxW7VcLsZ2UH\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1773358228,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
string: "{\n \"id\": \"chatcmpl-DDFzR5eiizUskHHVDIqVHcFSGrZxy\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1772052101,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"- **How AI Agents are Shaping the Future
|
||||
of Personalized Learning** \\nAn article exploring how AI agents are revolutionizing
|
||||
personalized learning could captivate readers by detailing how these intelligent
|
||||
systems adapt educational content in real time to meet individual learner
|
||||
needs. By combining adaptive learning algorithms, natural language understanding,
|
||||
and behavioral analytics, AI agents are transforming classrooms and online
|
||||
platforms alike, providing personalized feedback, pacing, and even motivation
|
||||
strategies. This piece would dive into current implementations, such as AI
|
||||
tutors and automated grading, and forecast the potential for lifelong learning
|
||||
companions that evolve alongside the learner\u2019s growth. Readers would
|
||||
gain a comprehensive view of how AI agents can democratize access to education
|
||||
while enhancing efficacy across diverse learning environments.\\n\\n- **The
|
||||
Role of AI Agents in Accelerating Drug Discovery and Healthcare Innovation**
|
||||
\ \\nThis article would provide a compelling exploration of how AI agents
|
||||
are accelerating drug discovery by automating complex data analysis, predicting
|
||||
molecular interactions, and optimizing clinical trial design. It would highlight
|
||||
cutting-edge examples where AI agents collaborate with humans to speed up
|
||||
identifying promising compounds, reducing costs, and shortening development
|
||||
cycles. Beyond pharmaceuticals, the article could explore AI agents\u2019
|
||||
expanding roles in personalized medicine, patient monitoring, and diagnostic
|
||||
support. With healthcare challenges mounting globally, this topic offers high
|
||||
relevance and excitement by showcasing how AI agents act as catalysts for
|
||||
breakthroughs that can save lives and transform medical care.\\n\\n- **Collaborative
|
||||
AI Agents: The Next Frontier in Software Development and DevOps** \\nAn in-depth
|
||||
article on collaborative AI agents in software engineering would showcase
|
||||
how these tools enhance productivity and code quality by automating routine
|
||||
tasks, catching bugs, and assisting in code reviews. It could examine emerging
|
||||
AI agents designed for pair programming, continuous integration, deployment
|
||||
automation, and intelligent testing. By integrating seamlessly into DevOps
|
||||
pipelines, these AI collaborators reduce human error, speed up delivery cycles,
|
||||
and enable developers to focus on innovation. The piece would also discuss
|
||||
challenges like trust, explainability, and maintaining human oversight, appealing
|
||||
to software engineers and technology leaders eager to understand the practical
|
||||
implications of AI agents in development workflows.\\n\\n- **AI Agents and
|
||||
the Evolution of Customer Experience in Digital Businesses** \\nThis article
|
||||
would explore the transformative role AI agents play in reshaping customer
|
||||
experience for digital-first businesses. It could cover AI-powered chatbots,
|
||||
recommendation engines, sentiment analysis tools, and personalized marketing
|
||||
agents, illustrating how these intelligent systems enhance engagement and
|
||||
satisfaction while reducing operational costs. By weaving in real-world case
|
||||
studies and data, the article would demonstrate how AI agents help companies
|
||||
anticipate customer needs, resolve issues proactively, and create seamless
|
||||
omnichannel interactions. The narrative could also touch on the balance between
|
||||
automation and human touch, offering strategic insights for businesses aiming
|
||||
to harness AI agents without compromising brand loyalty and trust.\\n\\n-
|
||||
**Ethical Frameworks for the Deployment of Autonomous AI Agents in Society**
|
||||
\ \\nThis article would address the critical and timely topic of ethical considerations
|
||||
surrounding the deployment of autonomous AI agents in public and private spheres.
|
||||
It would systematically analyze issues such as accountability, transparency,
|
||||
fairness, privacy, and unintended consequences when AI agents make decisions
|
||||
or act autonomously. The article could feature interviews with ethicists,
|
||||
policymakers, and researchers, and review current regulatory efforts and standards
|
||||
shaping AI governance. By unpacking this complex terrain, the article would
|
||||
provide a thoughtful, multidisciplinary perspective crucial for stakeholders
|
||||
aiming to responsibly develop and deploy AI agents in ways that align with
|
||||
societal values and legal frameworks.\",\n \"refusal\": null,\n \"annotations\":
|
||||
\"assistant\",\n \"content\": \"- **The Evolution of AI Agents: From
|
||||
Simple Automation to Context-Aware Decision Makers** \\n This article would
|
||||
chart the remarkable progression of AI agents, tracing their journey from
|
||||
rule-based automation systems to sophisticated, context-aware entities capable
|
||||
of reasoning and adapting within complex environments. By exploring advances
|
||||
in reinforcement learning, natural language understanding, and multi-agent
|
||||
collaboration, readers would gain insight into how modern AI agents operate
|
||||
with increasing autonomy. The piece could include cutting-edge research examples
|
||||
and practical applications, illustrating how this evolution unlocks new possibilities
|
||||
across industries such as healthcare, logistics, and customer service, truly
|
||||
showcasing the expanding potential and impact of AI agents in everyday life.\\n\\n-
|
||||
**Designing Trustworthy AI Agents: Balancing Transparency and Performance**
|
||||
\ \\n Focusing on the critical issue of trust, this article would explore
|
||||
the tension between creating AI agents that offer high performance and those
|
||||
designed to be transparent and explainable to users. It would delve into techniques
|
||||
like explainable AI (XAI), confidence scoring, and user-centric design principles
|
||||
that foster trust and accountability. With a mix of theoretical insights and
|
||||
real-world implementations, the article would highlight how companies tackle
|
||||
challenges in deploying AI agents responsibly\u2014especially in sensitive
|
||||
domains like finance, law enforcement, and healthcare\u2014demonstrating how
|
||||
trustworthiness can become a competitive advantage in AI-driven services.\\n\\n-
|
||||
**AI Agents as Personal Productivity Assistants: Beyond Scheduling and Reminders**
|
||||
\ \\n This topic examines how AI agents are evolving from basic virtual assistants
|
||||
to powerful personal productivity coaches that understand context, anticipate
|
||||
needs, and proactively manage tasks. The article would investigate advances
|
||||
in multi-modal understanding, emotional intelligence, and continuous learning
|
||||
that enable AI agents to provide nuanced support in time management, email
|
||||
triage, project coordination, and even creative brainstorming. Case studies
|
||||
from popular platforms and startups would showcase how this new generation
|
||||
of AI agents is revolutionizing daily workflows for professionals across sectors,
|
||||
offering readers a forward-looking perspective on the future of personal digital
|
||||
assistance.\\n\\n- **Collaborative AI Agents in Multi-Agent Systems: Driving
|
||||
Complex Problem Solving** \\n This article would focus on the growing field
|
||||
of multi-agent AI systems, where multiple AI agents communicate, negotiate,
|
||||
and collaborate to solve problems that are too complex for a single agent.
|
||||
It would highlight research advances in swarm intelligence, decentralized
|
||||
decision-making, and cooperative game theory, and demonstrate practical applications
|
||||
ranging from autonomous vehicle fleets to smart grid management and disaster
|
||||
response coordination. By unpacking these complex interactions, the article
|
||||
would engage readers with the fascinating dynamics of AI ecosystems and the
|
||||
promise of collaborative agents to address society\u2019s grand challenges.\\n\\n-
|
||||
**Startups Building Next-Gen AI Agents: Innovating at the Intersection of
|
||||
AI and User Experience** \\n Highlighting startups at the forefront of AI
|
||||
agent technology, this article would provide an in-depth look at how these
|
||||
ventures blend cutting-edge artificial intelligence with seamless user experiences
|
||||
to disrupt traditional markets. It would examine how startups harness advances
|
||||
in natural language processing, reinforcement learning, and personalized modeling
|
||||
to create AI agents that feel intuitive and human-like, powering applications
|
||||
in healthcare, education, finance, and customer engagement. The article would
|
||||
also discuss funding trends, go-to-market strategies, and technological challenges,
|
||||
offering entrepreneurs, investors, and technologists valuable insights into
|
||||
what it takes to succeed in the burgeoning AI agent landscape.\\n\\n**Notes:**
|
||||
\ \\nThese ideas are crafted to cover a broad spectrum of AI agent-related
|
||||
topics, combining technical depth with real-world relevance. Each paragraph
|
||||
aims to showcase the potential richness, relevance, and appeal of a full article,
|
||||
ensuring the content would engage a diverse readership, from AI researchers
|
||||
and software engineers to startup founders and business leaders interested
|
||||
in AI innovation.\",\n \"refusal\": null,\n \"annotations\":
|
||||
[]\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n
|
||||
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 1049,\n \"completion_tokens\":
|
||||
677,\n \"total_tokens\": 1726,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 926,\n \"completion_tokens\":
|
||||
727,\n \"total_tokens\": 1653,\n \"prompt_tokens_details\": {\n \"cached_tokens\":
|
||||
0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_ae0f8c9a7b\"\n}\n"
|
||||
\"default\",\n \"system_fingerprint\": \"fp_417e90869b\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9db696410a2db911-EWR
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 12 Mar 2026 23:30:38 GMT
|
||||
- Wed, 25 Feb 2026 20:41:50 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
@@ -569,10 +545,12 @@ interactions:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '9221'
|
||||
- '9082'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
@@ -612,74 +590,76 @@ interactions:
|
||||
list of ideas with their paragraph and your notes. task_expected_output: 5 bullet
|
||||
points with a paragraph for each idea. agent: Researcher agent_goal: Make the
|
||||
best research and analysis on content about AI and AI agents Task Output: -
|
||||
**How AI Agents are Shaping the Future of Personalized Learning** \\nAn article
|
||||
exploring how AI agents are revolutionizing personalized learning could captivate
|
||||
readers by detailing how these intelligent systems adapt educational content
|
||||
in real time to meet individual learner needs. By combining adaptive learning
|
||||
algorithms, natural language understanding, and behavioral analytics, AI agents
|
||||
are transforming classrooms and online platforms alike, providing personalized
|
||||
feedback, pacing, and even motivation strategies. This piece would dive into
|
||||
current implementations, such as AI tutors and automated grading, and forecast
|
||||
the potential for lifelong learning companions that evolve alongside the learner\u2019s
|
||||
growth. Readers would gain a comprehensive view of how AI agents can democratize
|
||||
access to education while enhancing efficacy across diverse learning environments.\\n\\n-
|
||||
**The Role of AI Agents in Accelerating Drug Discovery and Healthcare Innovation**
|
||||
\ \\nThis article would provide a compelling exploration of how AI agents are
|
||||
accelerating drug discovery by automating complex data analysis, predicting
|
||||
molecular interactions, and optimizing clinical trial design. It would highlight
|
||||
cutting-edge examples where AI agents collaborate with humans to speed up identifying
|
||||
promising compounds, reducing costs, and shortening development cycles. Beyond
|
||||
pharmaceuticals, the article could explore AI agents\u2019 expanding roles in
|
||||
personalized medicine, patient monitoring, and diagnostic support. With healthcare
|
||||
challenges mounting globally, this topic offers high relevance and excitement
|
||||
by showcasing how AI agents act as catalysts for breakthroughs that can save
|
||||
lives and transform medical care.\\n\\n- **Collaborative AI Agents: The Next
|
||||
Frontier in Software Development and DevOps** \\nAn in-depth article on collaborative
|
||||
AI agents in software engineering would showcase how these tools enhance productivity
|
||||
and code quality by automating routine tasks, catching bugs, and assisting in
|
||||
code reviews. It could examine emerging AI agents designed for pair programming,
|
||||
continuous integration, deployment automation, and intelligent testing. By integrating
|
||||
seamlessly into DevOps pipelines, these AI collaborators reduce human error,
|
||||
speed up delivery cycles, and enable developers to focus on innovation. The
|
||||
piece would also discuss challenges like trust, explainability, and maintaining
|
||||
human oversight, appealing to software engineers and technology leaders eager
|
||||
to understand the practical implications of AI agents in development workflows.\\n\\n-
|
||||
**AI Agents and the Evolution of Customer Experience in Digital Businesses**
|
||||
\ \\nThis article would explore the transformative role AI agents play in reshaping
|
||||
customer experience for digital-first businesses. It could cover AI-powered
|
||||
chatbots, recommendation engines, sentiment analysis tools, and personalized
|
||||
marketing agents, illustrating how these intelligent systems enhance engagement
|
||||
and satisfaction while reducing operational costs. By weaving in real-world
|
||||
case studies and data, the article would demonstrate how AI agents help companies
|
||||
anticipate customer needs, resolve issues proactively, and create seamless omnichannel
|
||||
interactions. The narrative could also touch on the balance between automation
|
||||
and human touch, offering strategic insights for businesses aiming to harness
|
||||
AI agents without compromising brand loyalty and trust.\\n\\n- **Ethical Frameworks
|
||||
for the Deployment of Autonomous AI Agents in Society** \\nThis article would
|
||||
address the critical and timely topic of ethical considerations surrounding
|
||||
the deployment of autonomous AI agents in public and private spheres. It would
|
||||
systematically analyze issues such as accountability, transparency, fairness,
|
||||
privacy, and unintended consequences when AI agents make decisions or act autonomously.
|
||||
The article could feature interviews with ethicists, policymakers, and researchers,
|
||||
and review current regulatory efforts and standards shaping AI governance. By
|
||||
unpacking this complex terrain, the article would provide a thoughtful, multidisciplinary
|
||||
perspective crucial for stakeholders aiming to responsibly develop and deploy
|
||||
AI agents in ways that align with societal values and legal frameworks.\\n\\nThis
|
||||
is the expected criteria for your final answer: Evaluation Score from 1 to 10
|
||||
based on the performance of the agents on the tasks\\nyou MUST return the actual
|
||||
complete content as the final answer, not a summary.\\nFormat your final answer
|
||||
according to the following OpenAPI schema: {\\n \\\"properties\\\": {\\n \\\"quality\\\":
|
||||
{\\n \\\"description\\\": \\\"A score from 1 to 10 evaluating on completion,
|
||||
quality, and overall performance from the task_description and task_expected_output
|
||||
to the actual Task Output.\\\",\\n \\\"title\\\": \\\"Quality\\\",\\n \\\"type\\\":
|
||||
\\\"number\\\"\\n }\\n },\\n \\\"required\\\": [\\n \\\"quality\\\"\\n
|
||||
\ ],\\n \\\"title\\\": \\\"TaskEvaluationPydanticOutput\\\",\\n \\\"type\\\":
|
||||
\\\"object\\\",\\n \\\"additionalProperties\\\": false\\n}\\n\\nIMPORTANT:
|
||||
Preserve the original content exactly as-is. Do NOT rewrite, paraphrase, or
|
||||
modify the meaning of the content. Only structure it to match the schema format.\\n\\nDo
|
||||
not include the OpenAPI schema in the final output. Ensure the final output
|
||||
does not include any code block markers like ```json or ```python.\\n\\nProvide
|
||||
your complete response:\"}],\"model\":\"gpt-4o-mini\",\"response_format\":{\"type\":\"json_schema\",\"json_schema\":{\"schema\":{\"properties\":{\"quality\":{\"description\":\"A
|
||||
**The Evolution of AI Agents: From Simple Automation to Context-Aware Decision
|
||||
Makers** \\n This article would chart the remarkable progression of AI agents,
|
||||
tracing their journey from rule-based automation systems to sophisticated, context-aware
|
||||
entities capable of reasoning and adapting within complex environments. By exploring
|
||||
advances in reinforcement learning, natural language understanding, and multi-agent
|
||||
collaboration, readers would gain insight into how modern AI agents operate
|
||||
with increasing autonomy. The piece could include cutting-edge research examples
|
||||
and practical applications, illustrating how this evolution unlocks new possibilities
|
||||
across industries such as healthcare, logistics, and customer service, truly
|
||||
showcasing the expanding potential and impact of AI agents in everyday life.\\n\\n-
|
||||
**Designing Trustworthy AI Agents: Balancing Transparency and Performance**
|
||||
\ \\n Focusing on the critical issue of trust, this article would explore the
|
||||
tension between creating AI agents that offer high performance and those designed
|
||||
to be transparent and explainable to users. It would delve into techniques like
|
||||
explainable AI (XAI), confidence scoring, and user-centric design principles
|
||||
that foster trust and accountability. With a mix of theoretical insights and
|
||||
real-world implementations, the article would highlight how companies tackle
|
||||
challenges in deploying AI agents responsibly\u2014especially in sensitive domains
|
||||
like finance, law enforcement, and healthcare\u2014demonstrating how trustworthiness
|
||||
can become a competitive advantage in AI-driven services.\\n\\n- **AI Agents
|
||||
as Personal Productivity Assistants: Beyond Scheduling and Reminders** \\n
|
||||
\ This topic examines how AI agents are evolving from basic virtual assistants
|
||||
to powerful personal productivity coaches that understand context, anticipate
|
||||
needs, and proactively manage tasks. The article would investigate advances
|
||||
in multi-modal understanding, emotional intelligence, and continuous learning
|
||||
that enable AI agents to provide nuanced support in time management, email triage,
|
||||
project coordination, and even creative brainstorming. Case studies from popular
|
||||
platforms and startups would showcase how this new generation of AI agents is
|
||||
revolutionizing daily workflows for professionals across sectors, offering readers
|
||||
a forward-looking perspective on the future of personal digital assistance.\\n\\n-
|
||||
**Collaborative AI Agents in Multi-Agent Systems: Driving Complex Problem Solving**
|
||||
\ \\n This article would focus on the growing field of multi-agent AI systems,
|
||||
where multiple AI agents communicate, negotiate, and collaborate to solve problems
|
||||
that are too complex for a single agent. It would highlight research advances
|
||||
in swarm intelligence, decentralized decision-making, and cooperative game theory,
|
||||
and demonstrate practical applications ranging from autonomous vehicle fleets
|
||||
to smart grid management and disaster response coordination. By unpacking these
|
||||
complex interactions, the article would engage readers with the fascinating
|
||||
dynamics of AI ecosystems and the promise of collaborative agents to address
|
||||
society\u2019s grand challenges.\\n\\n- **Startups Building Next-Gen AI Agents:
|
||||
Innovating at the Intersection of AI and User Experience** \\n Highlighting
|
||||
startups at the forefront of AI agent technology, this article would provide
|
||||
an in-depth look at how these ventures blend cutting-edge artificial intelligence
|
||||
with seamless user experiences to disrupt traditional markets. It would examine
|
||||
how startups harness advances in natural language processing, reinforcement
|
||||
learning, and personalized modeling to create AI agents that feel intuitive
|
||||
and human-like, powering applications in healthcare, education, finance, and
|
||||
customer engagement. The article would also discuss funding trends, go-to-market
|
||||
strategies, and technological challenges, offering entrepreneurs, investors,
|
||||
and technologists valuable insights into what it takes to succeed in the burgeoning
|
||||
AI agent landscape.\\n\\n**Notes:** \\nThese ideas are crafted to cover a broad
|
||||
spectrum of AI agent-related topics, combining technical depth with real-world
|
||||
relevance. Each paragraph aims to showcase the potential richness, relevance,
|
||||
and appeal of a full article, ensuring the content would engage a diverse readership,
|
||||
from AI researchers and software engineers to startup founders and business
|
||||
leaders interested in AI innovation.\\n\\nThis is the expected criteria for
|
||||
your final answer: Evaluation Score from 1 to 10 based on the performance of
|
||||
the agents on the tasks\\nyou MUST return the actual complete content as the
|
||||
final answer, not a summary.\\nFormat your final answer according to the following
|
||||
OpenAPI schema: {\\n \\\"properties\\\": {\\n \\\"quality\\\": {\\n \\\"description\\\":
|
||||
\\\"A score from 1 to 10 evaluating on completion, quality, and overall performance
|
||||
from the task_description and task_expected_output to the actual Task Output.\\\",\\n
|
||||
\ \\\"title\\\": \\\"Quality\\\",\\n \\\"type\\\": \\\"number\\\"\\n
|
||||
\ }\\n },\\n \\\"required\\\": [\\n \\\"quality\\\"\\n ],\\n \\\"title\\\":
|
||||
\\\"TaskEvaluationPydanticOutput\\\",\\n \\\"type\\\": \\\"object\\\",\\n \\\"additionalProperties\\\":
|
||||
false\\n}\\n\\nIMPORTANT: Preserve the original content exactly as-is. Do NOT
|
||||
rewrite, paraphrase, or modify the meaning of the content. Only structure it
|
||||
to match the schema format.\\n\\nDo not include the OpenAPI schema in the final
|
||||
output. Ensure the final output does not include any code block markers like
|
||||
```json or ```python.\\n\\nProvide your complete response:\"}],\"model\":\"gpt-4o-mini\",\"response_format\":{\"type\":\"json_schema\",\"json_schema\":{\"schema\":{\"properties\":{\"quality\":{\"description\":\"A
|
||||
score from 1 to 10 evaluating on completion, quality, and overall performance
|
||||
from the task_description and task_expected_output to the actual Task Output.\",\"title\":\"Quality\",\"type\":\"number\"}},\"required\":[\"quality\"],\"title\":\"TaskEvaluationPydanticOutput\",\"type\":\"object\",\"additionalProperties\":false},\"name\":\"TaskEvaluationPydanticOutput\",\"strict\":true}},\"stream\":false}"
|
||||
headers:
|
||||
@@ -694,7 +674,7 @@ interactions:
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '7036'
|
||||
- '7196'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
@@ -720,33 +700,31 @@ interactions:
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
- 3.13.12
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DIjmAmRrx8ONo3OaHaBCNKzOa0Mzs\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1773358238,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
|
||||
string: "{\n \"id\": \"chatcmpl-DDFzaYq2i96GKjZisy507Xk2rVvjn\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1772052110,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"{\\\"quality\\\":9}\",\n \"refusal\":
|
||||
null,\n \"annotations\": []\n },\n \"logprobs\": null,\n
|
||||
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
1214,\n \"completion_tokens\": 5,\n \"total_tokens\": 1219,\n \"prompt_tokens_details\":
|
||||
1264,\n \"completion_tokens\": 5,\n \"total_tokens\": 1269,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_1d1f595505\"\n}\n"
|
||||
\"default\",\n \"system_fingerprint\": \"fp_bd4be55b21\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9db6967da94a10f3-EWR
|
||||
CF-RAY:
|
||||
- CF-RAY-XXX
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 12 Mar 2026 23:30:39 GMT
|
||||
- Wed, 25 Feb 2026 20:41:51 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
@@ -759,10 +737,12 @@ interactions:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
cf-cache-status:
|
||||
- DYNAMIC
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
openai-processing-ms:
|
||||
- '374'
|
||||
- '391'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
openai-version:
|
||||
|
||||
@@ -1,115 +1,111 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Researcher. You''re an
|
||||
expert researcher, specialized in technology, software engineering, AI and startups.
|
||||
You work as a freelancer and is now working on doing research and analysis for
|
||||
a new customer.\nYour personal goal is: Make the best research and analysis
|
||||
on content about AI and AI agents"},{"role":"user","content":"\nCurrent Task:
|
||||
Look at the available data and give me a sense on the total number of sales.\n\nThis
|
||||
is the expected criteria for your final answer: The total number of sales as
|
||||
an integer\nyou MUST return the actual complete content as the final answer,
|
||||
not a summary.\n\nProvide your complete response:"}],"model":"gpt-4.1-mini"}'
|
||||
body: '{"messages": [{"role": "system", "content": "You are Researcher. You''re
|
||||
an expert researcher, specialized in technology, software engineering, AI and
|
||||
startups. You work as a freelancer and is now working on doing research and
|
||||
analysis for a new customer.\nYour personal goal is: Make the best research
|
||||
and analysis on content about AI and AI agents\nTo give my best complete final
|
||||
answer to the task use the exact following format:\n\nThought: I now can give
|
||||
a great answer\nFinal Answer: Your final answer must be the great and the most
|
||||
complete as possible, it must be outcome described.\n\nI MUST use these formats,
|
||||
my job depends on it!"}, {"role": "user", "content": "\nCurrent Task: Look at
|
||||
the available data and give me a sense on the total number of sales.\n\nThis
|
||||
is the expect criteria for your final answer: The total number of sales as an
|
||||
integer\nyou MUST return the actual complete content as the final answer, not
|
||||
a summary.\n\nBegin! This is VERY important to you, use the tools available
|
||||
and give your best Final Answer, your job depends on it!\n\nThought:"}], "model":
|
||||
"gpt-4o"}'
|
||||
headers:
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '704'
|
||||
- '1097'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=9.8sBYBkvBR8R1K_bVF7xgU..80XKlEIg3N2OBbTSCU-1727214102-1.0.1.1-.qiTLXbPamYUMSuyNsOEB9jhGu.jOifujOrx9E2JZvStbIZ9RTIiE44xKKNfLPxQkOi6qAT3h6htK8lPDGV_5g;
|
||||
_cfuvid=lbRdAddVWV6W3f5Dm9SaOPWDUOxqtZBSPr_fTW26nEA-1727213194587-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.47.0
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
- 1.47.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
- 3.11.7
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DIjv4bkJSathhHXsLANGZgGhV3rl7\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1773358790,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"I don\u2019t see any data provided
|
||||
yet regarding sales figures. Please share the available data or provide additional
|
||||
details so I can analyze and calculate the total number of sales accurately.\",\n
|
||||
\ \"refusal\": null,\n \"annotations\": []\n },\n \"logprobs\":
|
||||
null,\n \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
130,\n \"completion_tokens\": 34,\n \"total_tokens\": 164,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_828130e5d4\"\n}\n"
|
||||
string: "{\n \"id\": \"chatcmpl-AB7cBo2TPJMkfJCtCzpXOEixI8VrG\",\n \"object\"\
|
||||
: \"chat.completion\",\n \"created\": 1727214243,\n \"model\": \"gpt-4o-2024-05-13\"\
|
||||
,\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \
|
||||
\ \"role\": \"assistant\",\n \"content\": \"Thought: I need to\
|
||||
\ analyze the available data to determine the total number of sales accurately.\\\
|
||||
n\\nFinal Answer: The total number of sales is [the exact integer value of\
|
||||
\ the total sales from the given data].\",\n \"refusal\": null\n \
|
||||
\ },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n \
|
||||
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 215,\n \"completion_tokens\"\
|
||||
: 41,\n \"total_tokens\": 256,\n \"completion_tokens_details\": {\n\
|
||||
\ \"reasoning_tokens\": 0\n }\n },\n \"system_fingerprint\": \"\
|
||||
fp_e375328146\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9db6a3f7ae211512-EWR
|
||||
CF-RAY:
|
||||
- 8c85f4176a8e1cf3-GRU
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Thu, 12 Mar 2026 23:39:51 GMT
|
||||
- Tue, 24 Sep 2024 21:44:03 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
- X-Request-ID
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '854'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
- '906'
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
- '30000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
- '29999735'
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
- 0s
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
- req_06bf7b348d3d142c9cb7cce4d956b8d6
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,118 +1,171 @@
|
||||
interactions:
|
||||
- request:
|
||||
body: '{"messages":[{"role":"system","content":"You are Researcher. You''re an
|
||||
expert researcher, specialized in technology, software engineering, AI and startups.
|
||||
You work as a freelancer and is now working on doing research and analysis for
|
||||
a new customer.\nYour personal goal is: Make the best research and analysis
|
||||
on content about AI and AI agents"},{"role":"user","content":"\nCurrent Task:
|
||||
Generate a list of 5 interesting ideas to explore for an article, where each
|
||||
bulletpoint is under 15 words.\n\nThis is the expected criteria for your final
|
||||
answer: Bullet point list of 5 important events. No additional commentary.\nyou
|
||||
MUST return the actual complete content as the final answer, not a summary.\n\nProvide
|
||||
your complete response:"}],"model":"gpt-4.1-mini"}'
|
||||
body: !!binary |
|
||||
CoEMCiQKIgoMc2VydmljZS5uYW1lEhIKEGNyZXdBSS10ZWxlbWV0cnkS2AsKEgoQY3Jld2FpLnRl
|
||||
bGVtZXRyeRKQAgoQ1lPH3Bis4hD4M7Sez2t96RIIIffb2kCAAqMqDlRhc2sgRXhlY3V0aW9uMAE5
|
||||
WIEZIiVM+BdBSK51sSZM+BdKLgoIY3Jld19rZXkSIgogM2Y4ZDVjM2FiODgyZDY4NjlkOTNjYjgx
|
||||
ZjBlMmVkNGFKMQoHY3Jld19pZBImCiQyYjZmY2ZmYS1lNDQ0LTQ4YjYtYWNjNi0xZTVhMDY2OTQ1
|
||||
NWJKLgoIdGFza19rZXkSIgogOTRhODI2YzE5MzA1NTk2ODZiYWZiNDA5ZWU4Mzg3NmZKMQoHdGFz
|
||||
a19pZBImCiQxMTU5NmU3OS0yYzllLTQzOWYtYWViMS0xMThhMTI2ZDNiYzN6AhgBhQEAAQAAEp0H
|
||||
ChBEYWf4sVuYMd8/Oxr4ONAsEghO/cKNNKdq0CoMQ3JldyBDcmVhdGVkMAE5KCBKsyZM+BdByI5P
|
||||
syZM+BdKGgoOY3Jld2FpX3ZlcnNpb24SCAoGMC42MS4wShoKDnB5dGhvbl92ZXJzaW9uEggKBjMu
|
||||
MTEuN0ouCghjcmV3X2tleRIiCiBhOWNjNWQ0MzM5NWIyMWIxODFjODBiZDQzNTFjY2VjOEoxCgdj
|
||||
cmV3X2lkEiYKJDkzNGJkMDZiLTY2ZDktNDE0MC1iZGE3LTQzMDZmNmM3Y2Q0N0ocCgxjcmV3X3By
|
||||
b2Nlc3MSDAoKc2VxdWVudGlhbEoRCgtjcmV3X21lbW9yeRICEABKGgoUY3Jld19udW1iZXJfb2Zf
|
||||
dGFza3MSAhgBShsKFWNyZXdfbnVtYmVyX29mX2FnZW50cxICGAFKzAIKC2NyZXdfYWdlbnRzErwC
|
||||
CrkCW3sia2V5IjogIjhiZDIxMzliNTk3NTE4MTUwNmU0MWZkOWM0NTYzZDc1IiwgImlkIjogIjY3
|
||||
MWMzYzdmLWNjMzUtNGU5MS1hYjgzLWVmZGVjOWU3Y2ZiNyIsICJyb2xlIjogIlJlc2VhcmNoZXIi
|
||||
LCAidmVyYm9zZT8iOiBmYWxzZSwgIm1heF9pdGVyIjogMTUsICJtYXhfcnBtIjogbnVsbCwgImZ1
|
||||
bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAiZ3B0LTRvIiwgImRlbGVnYXRpb25fZW5h
|
||||
YmxlZD8iOiBmYWxzZSwgImFsbG93X2NvZGVfZXhlY3V0aW9uPyI6IGZhbHNlLCAibWF4X3JldHJ5
|
||||
X2xpbWl0IjogMiwgInRvb2xzX25hbWVzIjogW119XUr+AQoKY3Jld190YXNrcxLvAQrsAVt7Imtl
|
||||
eSI6ICJlOWU2YjcyYWFjMzI2NDU5ZGQ3MDY4ZjBiMTcxN2MxYyIsICJpZCI6ICI4YmFkNTJiZi05
|
||||
MGM0LTQ0ZDgtYmNlZi0xODBkZTA2MjRiYWYiLCAiYXN5bmNfZXhlY3V0aW9uPyI6IHRydWUsICJo
|
||||
dW1hbl9pbnB1dD8iOiBmYWxzZSwgImFnZW50X3JvbGUiOiAiUmVzZWFyY2hlciIsICJhZ2VudF9r
|
||||
ZXkiOiAiOGJkMjEzOWI1OTc1MTgxNTA2ZTQxZmQ5YzQ1NjNkNzUiLCAidG9vbHNfbmFtZXMiOiBb
|
||||
XX1degIYAYUBAAEAABKOAgoQduJhIxVspIn9gWgZzmXHrhIILYsCkB2V4ckqDFRhc2sgQ3JlYXRl
|
||||
ZDABORCOYrMmTPgXQdDrYrMmTPgXSi4KCGNyZXdfa2V5EiIKIGE5Y2M1ZDQzMzk1YjIxYjE4MWM4
|
||||
MGJkNDM1MWNjZWM4SjEKB2NyZXdfaWQSJgokOTM0YmQwNmItNjZkOS00MTQwLWJkYTctNDMwNmY2
|
||||
YzdjZDQ3Si4KCHRhc2tfa2V5EiIKIGU5ZTZiNzJhYWMzMjY0NTlkZDcwNjhmMGIxNzE3YzFjSjEK
|
||||
B3Rhc2tfaWQSJgokOGJhZDUyYmYtOTBjNC00NGQ4LWJjZWYtMTgwZGUwNjI0YmFmegIYAYUBAAEA
|
||||
AA==
|
||||
headers:
|
||||
Accept:
|
||||
- '*/*'
|
||||
Accept-Encoding:
|
||||
- gzip, deflate
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Length:
|
||||
- '1540'
|
||||
Content-Type:
|
||||
- application/x-protobuf
|
||||
User-Agent:
|
||||
- X-USER-AGENT-XXX
|
||||
- OTel-OTLP-Exporter-Python/1.27.0
|
||||
method: POST
|
||||
uri: https://telemetry.crewai.com:4319/v1/traces
|
||||
response:
|
||||
body:
|
||||
string: "\n\0"
|
||||
headers:
|
||||
Content-Length:
|
||||
- '2'
|
||||
Content-Type:
|
||||
- application/x-protobuf
|
||||
Date:
|
||||
- Tue, 24 Sep 2024 21:43:06 GMT
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
- request:
|
||||
body: '{"messages": [{"role": "system", "content": "You are Researcher. You''re
|
||||
an expert researcher, specialized in technology, software engineering, AI and
|
||||
startups. You work as a freelancer and is now working on doing research and
|
||||
analysis for a new customer.\nYour personal goal is: Make the best research
|
||||
and analysis on content about AI and AI agents\nTo give my best complete final
|
||||
answer to the task use the exact following format:\n\nThought: I now can give
|
||||
a great answer\nFinal Answer: Your final answer must be the great and the most
|
||||
complete as possible, it must be outcome described.\n\nI MUST use these formats,
|
||||
my job depends on it!"}, {"role": "user", "content": "\nCurrent Task: Generate
|
||||
a list of 5 interesting ideas to explore for an article, where each bulletpoint
|
||||
is under 15 words.\n\nThis is the expect criteria for your final answer: Bullet
|
||||
point list of 5 important events. No additional commentary.\nyou MUST return
|
||||
the actual complete content as the final answer, not a summary.\n\nBegin! This
|
||||
is VERY important to you, use the tools available and give your best Final Answer,
|
||||
your job depends on it!\n\nThought:"}], "model": "gpt-4o"}'
|
||||
headers:
|
||||
accept:
|
||||
- application/json
|
||||
accept-encoding:
|
||||
- ACCEPT-ENCODING-XXX
|
||||
authorization:
|
||||
- AUTHORIZATION-XXX
|
||||
- gzip, deflate
|
||||
connection:
|
||||
- keep-alive
|
||||
content-length:
|
||||
- '762'
|
||||
- '1155'
|
||||
content-type:
|
||||
- application/json
|
||||
cookie:
|
||||
- __cf_bm=9.8sBYBkvBR8R1K_bVF7xgU..80XKlEIg3N2OBbTSCU-1727214102-1.0.1.1-.qiTLXbPamYUMSuyNsOEB9jhGu.jOifujOrx9E2JZvStbIZ9RTIiE44xKKNfLPxQkOi6qAT3h6htK8lPDGV_5g;
|
||||
_cfuvid=lbRdAddVWV6W3f5Dm9SaOPWDUOxqtZBSPr_fTW26nEA-1727213194587-0.0.1.1-604800000
|
||||
host:
|
||||
- api.openai.com
|
||||
user-agent:
|
||||
- OpenAI/Python 1.47.0
|
||||
x-stainless-arch:
|
||||
- X-STAINLESS-ARCH-XXX
|
||||
- arm64
|
||||
x-stainless-async:
|
||||
- 'false'
|
||||
x-stainless-lang:
|
||||
- python
|
||||
x-stainless-os:
|
||||
- X-STAINLESS-OS-XXX
|
||||
- MacOS
|
||||
x-stainless-package-version:
|
||||
- 1.83.0
|
||||
x-stainless-read-timeout:
|
||||
- X-STAINLESS-READ-TIMEOUT-XXX
|
||||
x-stainless-retry-count:
|
||||
- '0'
|
||||
- 1.47.0
|
||||
x-stainless-raw-response:
|
||||
- 'true'
|
||||
x-stainless-runtime:
|
||||
- CPython
|
||||
x-stainless-runtime-version:
|
||||
- 3.13.3
|
||||
- 3.11.7
|
||||
method: POST
|
||||
uri: https://api.openai.com/v1/chat/completions
|
||||
response:
|
||||
body:
|
||||
string: "{\n \"id\": \"chatcmpl-DIkLSp6YfhftRnhYHqjRHZXAI8Sji\",\n \"object\":
|
||||
\"chat.completion\",\n \"created\": 1773360426,\n \"model\": \"gpt-4.1-mini-2025-04-14\",\n
|
||||
\ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\":
|
||||
\"assistant\",\n \"content\": \"- Impact of autonomous AI agents on
|
||||
future workplace automation \\n- Ethical dilemmas in deploying AI decision-making
|
||||
systems \\n- Advances in AI-driven personalized learning technologies \\n-
|
||||
Role of AI in enhancing cybersecurity defense mechanisms \\n- Challenges
|
||||
in regulating AI innovations across global markets\",\n \"refusal\":
|
||||
null,\n \"annotations\": []\n },\n \"logprobs\": null,\n
|
||||
\ \"finish_reason\": \"stop\"\n }\n ],\n \"usage\": {\n \"prompt_tokens\":
|
||||
141,\n \"completion_tokens\": 50,\n \"total_tokens\": 191,\n \"prompt_tokens_details\":
|
||||
{\n \"cached_tokens\": 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\":
|
||||
{\n \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\":
|
||||
0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\":
|
||||
\"default\",\n \"system_fingerprint\": \"fp_5e793402c9\"\n}\n"
|
||||
string: "{\n \"id\": \"chatcmpl-AB7bGdQd8mh4zvM4UaLl93hex1Ys3\",\n \"object\"\
|
||||
: \"chat.completion\",\n \"created\": 1727214186,\n \"model\": \"gpt-4o-2024-05-13\"\
|
||||
,\n \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \
|
||||
\ \"role\": \"assistant\",\n \"content\": \"Thought: I now can\
|
||||
\ give a great answer.\\nFinal Answer:\\n- Ethical implications of AI in law\
|
||||
\ enforcement and surveillance.\\n- AI advancements in personalized healthcare\
|
||||
\ and diagnostics.\\n- Autonomous AI agents in financial market trading.\\\
|
||||
n- Collaboration between AI and humans in creative arts.\\n- AI-driven climate\
|
||||
\ modeling and environmental monitoring.\",\n \"refusal\": null\n \
|
||||
\ },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n \
|
||||
\ }\n ],\n \"usage\": {\n \"prompt_tokens\": 226,\n \"completion_tokens\"\
|
||||
: 61,\n \"total_tokens\": 287,\n \"completion_tokens_details\": {\n\
|
||||
\ \"reasoning_tokens\": 0\n }\n },\n \"system_fingerprint\": \"\
|
||||
fp_e375328146\"\n}\n"
|
||||
headers:
|
||||
CF-Cache-Status:
|
||||
- DYNAMIC
|
||||
CF-Ray:
|
||||
- 9db6cbea1bd29d36-EWR
|
||||
CF-RAY:
|
||||
- 8c85f2b7c92f1cf3-GRU
|
||||
Connection:
|
||||
- keep-alive
|
||||
Content-Type:
|
||||
- application/json
|
||||
Date:
|
||||
- Fri, 13 Mar 2026 00:07:08 GMT
|
||||
- Tue, 24 Sep 2024 21:43:07 GMT
|
||||
Server:
|
||||
- cloudflare
|
||||
Strict-Transport-Security:
|
||||
- STS-XXX
|
||||
Transfer-Encoding:
|
||||
- chunked
|
||||
X-Content-Type-Options:
|
||||
- X-CONTENT-TYPE-XXX
|
||||
- nosniff
|
||||
access-control-expose-headers:
|
||||
- ACCESS-CONTROL-XXX
|
||||
alt-svc:
|
||||
- h3=":443"; ma=86400
|
||||
- X-Request-ID
|
||||
openai-organization:
|
||||
- OPENAI-ORG-XXX
|
||||
- crewai-iuxna1
|
||||
openai-processing-ms:
|
||||
- '1357'
|
||||
openai-project:
|
||||
- OPENAI-PROJECT-XXX
|
||||
- '939'
|
||||
openai-version:
|
||||
- '2020-10-01'
|
||||
set-cookie:
|
||||
- SET-COOKIE-XXX
|
||||
x-openai-proxy-wasm:
|
||||
- v0.1
|
||||
strict-transport-security:
|
||||
- max-age=31536000; includeSubDomains; preload
|
||||
x-ratelimit-limit-requests:
|
||||
- X-RATELIMIT-LIMIT-REQUESTS-XXX
|
||||
- '10000'
|
||||
x-ratelimit-limit-tokens:
|
||||
- X-RATELIMIT-LIMIT-TOKENS-XXX
|
||||
- '30000000'
|
||||
x-ratelimit-remaining-requests:
|
||||
- X-RATELIMIT-REMAINING-REQUESTS-XXX
|
||||
- '9999'
|
||||
x-ratelimit-remaining-tokens:
|
||||
- X-RATELIMIT-REMAINING-TOKENS-XXX
|
||||
- '29999722'
|
||||
x-ratelimit-reset-requests:
|
||||
- X-RATELIMIT-RESET-REQUESTS-XXX
|
||||
- 6ms
|
||||
x-ratelimit-reset-tokens:
|
||||
- X-RATELIMIT-RESET-TOKENS-XXX
|
||||
- 0s
|
||||
x-request-id:
|
||||
- X-REQUEST-ID-XXX
|
||||
- req_4a6962cfb5b3418a75c19cfc1c2e7227
|
||||
status:
|
||||
code: 200
|
||||
message: OK
|
||||
|
||||
@@ -1121,345 +1121,3 @@ def test_anthropic_cached_prompt_tokens_with_tools():
|
||||
assert usage.successful_requests == 2
|
||||
# The second call should have cached prompt tokens
|
||||
assert usage.cached_prompt_tokens > 0
|
||||
|
||||
|
||||
# ---- Tool Search Tool Tests ----
|
||||
|
||||
|
||||
def test_tool_search_true_injects_bm25_and_defer_loading():
|
||||
"""tool_search=True should inject bm25 tool search and defer all tools."""
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
|
||||
|
||||
crewai_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "get_weather",
|
||||
"description": "Get weather for a location",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"location": {"type": "string"}},
|
||||
"required": ["location"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "calculator",
|
||||
"description": "Perform math calculations",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"expression": {"type": "string"}},
|
||||
"required": ["expression"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
formatted_messages, system_message = llm._format_messages_for_anthropic(
|
||||
[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
params = llm._prepare_completion_params(
|
||||
formatted_messages, system_message, crewai_tools
|
||||
)
|
||||
|
||||
tools = params["tools"]
|
||||
# Should have 3 tools: tool_search + 2 regular
|
||||
assert len(tools) == 3
|
||||
|
||||
# First tool should be the bm25 tool search tool
|
||||
assert tools[0]["type"] == "tool_search_tool_bm25_20251119"
|
||||
assert tools[0]["name"] == "tool_search_tool_bm25"
|
||||
assert "input_schema" not in tools[0]
|
||||
|
||||
# All regular tools should have defer_loading=True
|
||||
for t in tools[1:]:
|
||||
assert t.get("defer_loading") is True, f"Tool {t['name']} missing defer_loading"
|
||||
|
||||
|
||||
def test_tool_search_regex_config():
|
||||
"""tool_search with regex config should use regex variant."""
|
||||
from crewai.llms.providers.anthropic.completion import AnthropicToolSearchConfig
|
||||
|
||||
config = AnthropicToolSearchConfig(type="regex")
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=config)
|
||||
|
||||
crewai_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_a",
|
||||
"description": "First tool",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"q": {"type": "string"}},
|
||||
"required": ["q"],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "tool_b",
|
||||
"description": "Second tool",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"q": {"type": "string"}},
|
||||
"required": ["q"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
formatted_messages, system_message = llm._format_messages_for_anthropic(
|
||||
[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
params = llm._prepare_completion_params(
|
||||
formatted_messages, system_message, crewai_tools
|
||||
)
|
||||
|
||||
tools = params["tools"]
|
||||
assert tools[0]["type"] == "tool_search_tool_regex_20251119"
|
||||
assert tools[0]["name"] == "tool_search_tool_regex"
|
||||
|
||||
|
||||
def test_tool_search_disabled_by_default():
|
||||
"""tool_search=None (default) should NOT inject anything."""
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5")
|
||||
|
||||
crewai_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test_tool",
|
||||
"description": "A test tool",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"q": {"type": "string"}},
|
||||
"required": ["q"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
formatted_messages, system_message = llm._format_messages_for_anthropic(
|
||||
[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
params = llm._prepare_completion_params(
|
||||
formatted_messages, system_message, crewai_tools
|
||||
)
|
||||
|
||||
tools = params["tools"]
|
||||
assert len(tools) == 1
|
||||
for t in tools:
|
||||
assert t.get("type", "") not in (
|
||||
"tool_search_tool_bm25_20251119",
|
||||
"tool_search_tool_regex_20251119",
|
||||
)
|
||||
assert "defer_loading" not in t
|
||||
|
||||
|
||||
def test_tool_search_no_duplicate_when_manually_provided():
|
||||
"""If user passes a tool search tool manually, don't inject a duplicate."""
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
|
||||
|
||||
# User manually includes a tool search tool
|
||||
tools_with_search = [
|
||||
{"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"},
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test_tool",
|
||||
"description": "A test tool",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"q": {"type": "string"}},
|
||||
"required": ["q"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
formatted_messages, system_message = llm._format_messages_for_anthropic(
|
||||
[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
params = llm._prepare_completion_params(
|
||||
formatted_messages, system_message, tools_with_search
|
||||
)
|
||||
|
||||
tools = params["tools"]
|
||||
search_tools = [
|
||||
t for t in tools
|
||||
if t.get("type", "").startswith("tool_search_tool")
|
||||
]
|
||||
# Should only have 1 tool search tool (the user's manual one)
|
||||
assert len(search_tools) == 1
|
||||
assert search_tools[0]["type"] == "tool_search_tool_regex_20251119"
|
||||
|
||||
|
||||
def test_tool_search_passthrough_preserves_tool_search_type():
|
||||
"""_convert_tools_for_interference should pass through tool search tools unchanged."""
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5")
|
||||
|
||||
tools = [
|
||||
{"type": "tool_search_tool_regex_20251119", "name": "tool_search_tool_regex"},
|
||||
{
|
||||
"name": "get_weather",
|
||||
"description": "Get weather",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"location": {"type": "string"}},
|
||||
"required": ["location"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
converted = llm._convert_tools_for_interference(tools)
|
||||
assert len(converted) == 2
|
||||
# Tool search tool should be passed through exactly
|
||||
assert converted[0] == {
|
||||
"type": "tool_search_tool_regex_20251119",
|
||||
"name": "tool_search_tool_regex",
|
||||
}
|
||||
# Regular tool should be preserved
|
||||
assert converted[1]["name"] == "get_weather"
|
||||
assert "input_schema" in converted[1]
|
||||
|
||||
|
||||
def test_tool_search_single_tool_skips_search_and_forces_choice():
|
||||
"""With only 1 tool, tool_search is skipped (nothing to search) and the
|
||||
normal forced tool_choice optimisation still applies."""
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
|
||||
|
||||
crewai_tools = [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "test_tool",
|
||||
"description": "A test tool",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {"q": {"type": "string"}},
|
||||
"required": ["q"],
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
formatted_messages, system_message = llm._format_messages_for_anthropic(
|
||||
[{"role": "user", "content": "Hello"}]
|
||||
)
|
||||
params = llm._prepare_completion_params(
|
||||
formatted_messages,
|
||||
system_message,
|
||||
crewai_tools,
|
||||
available_functions={"test_tool": lambda q: "result"},
|
||||
)
|
||||
|
||||
# Single tool — tool_search skipped, tool_choice forced as normal
|
||||
assert "tool_choice" in params
|
||||
assert params["tool_choice"]["name"] == "test_tool"
|
||||
|
||||
# No tool search tool should be injected
|
||||
tool_types = [t.get("type", "") for t in params["tools"]]
|
||||
for ts_type in ("tool_search_tool_bm25_20251119", "tool_search_tool_regex_20251119"):
|
||||
assert ts_type not in tool_types
|
||||
|
||||
# No defer_loading on the single tool
|
||||
assert "defer_loading" not in params["tools"][0]
|
||||
|
||||
|
||||
def test_tool_search_via_llm_class():
|
||||
"""Verify tool_search param passes through LLM class correctly."""
|
||||
from crewai.llms.providers.anthropic.completion import (
|
||||
AnthropicCompletion,
|
||||
AnthropicToolSearchConfig,
|
||||
)
|
||||
|
||||
# Test with True
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
|
||||
assert isinstance(llm, AnthropicCompletion)
|
||||
assert llm.tool_search is not None
|
||||
assert llm.tool_search.type == "bm25"
|
||||
|
||||
# Test with config
|
||||
llm2 = LLM(
|
||||
model="anthropic/claude-sonnet-4-5",
|
||||
tool_search=AnthropicToolSearchConfig(type="regex"),
|
||||
)
|
||||
assert llm2.tool_search is not None
|
||||
assert llm2.tool_search.type == "regex"
|
||||
|
||||
# Test without (default)
|
||||
llm3 = LLM(model="anthropic/claude-sonnet-4-5")
|
||||
assert llm3.tool_search is None
|
||||
|
||||
|
||||
# Many tools shared by the VCR tests below
|
||||
_MANY_TOOLS = [
|
||||
{
|
||||
"name": name,
|
||||
"description": desc,
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"properties": {"input": {"type": "string", "description": f"Input for {name}"}},
|
||||
"required": ["input"],
|
||||
},
|
||||
}
|
||||
for name, desc in [
|
||||
("get_weather", "Get current weather conditions for a specified location"),
|
||||
("search_files", "Search through files in the workspace by name or content"),
|
||||
("read_database", "Read records from a database table with optional filtering"),
|
||||
("write_database", "Write or update records in a database table"),
|
||||
("send_email", "Send an email message to one or more recipients"),
|
||||
("read_email", "Read emails from inbox with filtering options"),
|
||||
("create_ticket", "Create a new support ticket in the ticketing system"),
|
||||
("update_ticket", "Update an existing support ticket status or description"),
|
||||
("list_users", "List all users in the system with optional filters"),
|
||||
("get_user_profile", "Get detailed profile information for a specific user"),
|
||||
("deploy_service", "Deploy a service to the specified environment"),
|
||||
("rollback_service", "Rollback a service deployment to a previous version"),
|
||||
("get_service_logs", "Get service logs filtered by time range and severity"),
|
||||
("run_sql_query", "Run a read-only SQL query against the analytics database"),
|
||||
("create_dashboard", "Create a new monitoring dashboard with widgets"),
|
||||
]
|
||||
]
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_tool_search_discovers_and_calls_tool():
|
||||
"""Tool search should discover the right tool and return a tool_use block."""
|
||||
llm = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
|
||||
|
||||
result = llm.call(
|
||||
"What is the weather in Tokyo?",
|
||||
tools=_MANY_TOOLS,
|
||||
)
|
||||
|
||||
# Should return tool_use blocks (list) since no available_functions provided
|
||||
assert isinstance(result, list)
|
||||
assert len(result) >= 1
|
||||
# The discovered tool should be get_weather
|
||||
tool_names = [getattr(block, "name", None) for block in result]
|
||||
assert "get_weather" in tool_names
|
||||
|
||||
|
||||
@pytest.mark.vcr()
|
||||
def test_tool_search_saves_input_tokens():
|
||||
"""Tool search with deferred loading should use fewer input tokens than loading all tools."""
|
||||
# Call WITHOUT tool search — all 15 tools loaded upfront
|
||||
llm_no_search = LLM(model="anthropic/claude-sonnet-4-5")
|
||||
llm_no_search.call("What is the weather in Tokyo?", tools=_MANY_TOOLS)
|
||||
usage_no_search = llm_no_search.get_token_usage_summary()
|
||||
|
||||
# Call WITH tool search — tools deferred
|
||||
llm_search = LLM(model="anthropic/claude-sonnet-4-5", tool_search=True)
|
||||
llm_search.call("What is the weather in Tokyo?", tools=_MANY_TOOLS)
|
||||
usage_search = llm_search.get_token_usage_summary()
|
||||
|
||||
# Tool search should use fewer input tokens
|
||||
assert usage_search.prompt_tokens < usage_no_search.prompt_tokens, (
|
||||
f"Expected tool_search ({usage_search.prompt_tokens}) to use fewer input tokens "
|
||||
f"than no search ({usage_no_search.prompt_tokens})"
|
||||
)
|
||||
|
||||
@@ -967,211 +967,3 @@ def test_bedrock_agent_kickoff_structured_output_with_tools():
|
||||
assert result.pydantic.result == 42, f"Expected result 42 but got {result.pydantic.result}"
|
||||
assert result.pydantic.operation, "Operation should not be empty"
|
||||
assert result.pydantic.explanation, "Explanation should not be empty"
|
||||
|
||||
|
||||
def test_bedrock_groups_three_tool_results():
|
||||
"""Consecutive tool results should be grouped into one Bedrock user message."""
|
||||
llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Use all three tools, then continue."},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "tool-1",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_weather",
|
||||
"arguments": '{"location": "New York"}',
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "tool-2",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_news",
|
||||
"arguments": '{"topic": "AI"}',
|
||||
},
|
||||
},
|
||||
{
|
||||
"id": "tool-3",
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": "lookup_stock",
|
||||
"arguments": '{"ticker": "AMZN"}',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "tool-1", "content": "72F and sunny"},
|
||||
{"role": "tool", "tool_call_id": "tool-2", "content": "AI news summary"},
|
||||
{"role": "tool", "tool_call_id": "tool-3", "content": "AMZN up 1.2%"},
|
||||
]
|
||||
|
||||
formatted_messages, system_message = llm._format_messages_for_converse(messages)
|
||||
|
||||
assert system_message is None
|
||||
assert [message["role"] for message in formatted_messages] == [
|
||||
"user",
|
||||
"assistant",
|
||||
"user",
|
||||
]
|
||||
assert len(formatted_messages[1]["content"]) == 3
|
||||
|
||||
tool_results = formatted_messages[2]["content"]
|
||||
assert len(tool_results) == 3
|
||||
assert [block["toolResult"]["toolUseId"] for block in tool_results] == [
|
||||
"tool-1",
|
||||
"tool-2",
|
||||
"tool-3",
|
||||
]
|
||||
assert [block["toolResult"]["content"][0]["text"] for block in tool_results] == [
|
||||
"72F and sunny",
|
||||
"AI news summary",
|
||||
"AMZN up 1.2%",
|
||||
]
|
||||
|
||||
|
||||
def test_bedrock_parallel_tool_results_grouped():
|
||||
"""Regression test for issue #4749.
|
||||
|
||||
When an assistant message contains multiple parallel tool calls,
|
||||
Bedrock requires all corresponding tool results to be grouped
|
||||
in a single user message. Previously each tool result was emitted
|
||||
as a separate user message, causing:
|
||||
ValidationException: Expected toolResult blocks at messages.2.content
|
||||
"""
|
||||
llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Calculate 25 + 17 AND 10 * 5"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_add",
|
||||
"type": "function",
|
||||
"function": {"name": "add_tool", "arguments": '{"a": 25, "b": 17}'},
|
||||
},
|
||||
{
|
||||
"id": "call_mul",
|
||||
"type": "function",
|
||||
"function": {"name": "multiply_tool", "arguments": '{"a": 10, "b": 5}'},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_add", "content": "42"},
|
||||
{"role": "tool", "tool_call_id": "call_mul", "content": "50"},
|
||||
]
|
||||
|
||||
converse_msgs, system_msg = llm._format_messages_for_converse(messages)
|
||||
|
||||
# Find the user message that contains toolResult blocks
|
||||
tool_result_messages = [
|
||||
m for m in converse_msgs
|
||||
if m.get("role") == "user"
|
||||
and any("toolResult" in b for b in m.get("content", []))
|
||||
]
|
||||
|
||||
# There must be exactly ONE user message with tool results (not two)
|
||||
assert len(tool_result_messages) == 1, (
|
||||
f"Expected 1 grouped tool-result message, got {len(tool_result_messages)}. "
|
||||
"Bedrock requires all parallel tool results in a single user message."
|
||||
)
|
||||
|
||||
# That single message must contain both tool results
|
||||
tool_results = tool_result_messages[0]["content"]
|
||||
assert len(tool_results) == 2, (
|
||||
f"Expected 2 toolResult blocks in grouped message, got {len(tool_results)}"
|
||||
)
|
||||
|
||||
# Verify the tool use IDs match
|
||||
tool_use_ids = {
|
||||
block["toolResult"]["toolUseId"] for block in tool_results
|
||||
}
|
||||
assert tool_use_ids == {"call_add", "call_mul"}
|
||||
|
||||
|
||||
def test_bedrock_single_tool_result_still_works():
|
||||
"""Ensure single tool call still produces a single-block user message."""
|
||||
llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "Add 1 + 2"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_single",
|
||||
"type": "function",
|
||||
"function": {"name": "add_tool", "arguments": '{"a": 1, "b": 2}'},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_single", "content": "3"},
|
||||
]
|
||||
|
||||
converse_msgs, _ = llm._format_messages_for_converse(messages)
|
||||
|
||||
tool_result_messages = [
|
||||
m for m in converse_msgs
|
||||
if m.get("role") == "user"
|
||||
and any("toolResult" in b for b in m.get("content", []))
|
||||
]
|
||||
assert len(tool_result_messages) == 1
|
||||
assert len(tool_result_messages[0]["content"]) == 1
|
||||
assert tool_result_messages[0]["content"][0]["toolResult"]["toolUseId"] == "call_single"
|
||||
|
||||
|
||||
def test_bedrock_tool_results_not_merged_across_assistant_messages():
|
||||
"""Tool results from different assistant turns must NOT be merged."""
|
||||
llm = LLM(model="bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0")
|
||||
|
||||
messages = [
|
||||
{"role": "user", "content": "First task"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_a",
|
||||
"type": "function",
|
||||
"function": {"name": "tool_a", "arguments": "{}"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_a", "content": "result_a"},
|
||||
{"role": "assistant", "content": "Now doing second task"},
|
||||
{"role": "user", "content": "Second task"},
|
||||
{
|
||||
"role": "assistant",
|
||||
"content": "",
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": "call_b",
|
||||
"type": "function",
|
||||
"function": {"name": "tool_b", "arguments": "{}"},
|
||||
},
|
||||
],
|
||||
},
|
||||
{"role": "tool", "tool_call_id": "call_b", "content": "result_b"},
|
||||
]
|
||||
|
||||
converse_msgs, _ = llm._format_messages_for_converse(messages)
|
||||
|
||||
tool_result_messages = [
|
||||
m for m in converse_msgs
|
||||
if m.get("role") == "user"
|
||||
and any("toolResult" in b for b in m.get("content", []))
|
||||
]
|
||||
|
||||
# Two separate tool-result messages (one per assistant turn)
|
||||
assert len(tool_result_messages) == 2, (
|
||||
"Tool results from different assistant turns must remain separate"
|
||||
)
|
||||
assert tool_result_messages[0]["content"][0]["toolResult"]["toolUseId"] == "call_a"
|
||||
assert tool_result_messages[1]["content"][0]["toolResult"]["toolUseId"] == "call_b"
|
||||
|
||||
@@ -268,54 +268,6 @@ class TestGetMCPToolsAmpIntegration:
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name == "mcp_notion_so_sse_search"
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
@patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
|
||||
def test_tool_filter_with_hyphenated_hash_syntax(
|
||||
self, mock_fetch, mock_client_class, agent
|
||||
):
|
||||
"""notion#get-page must match the tool whose sanitized name is get_page."""
|
||||
mock_fetch.return_value = {
|
||||
"notion": {
|
||||
"type": "sse",
|
||||
"url": "https://mcp.notion.so/sse",
|
||||
"headers": {"Authorization": "Bearer token"},
|
||||
},
|
||||
}
|
||||
|
||||
hyphenated_tool_definitions = [
|
||||
{
|
||||
"name": "get_page",
|
||||
"original_name": "get-page",
|
||||
"description": "Get a page",
|
||||
"inputSchema": {},
|
||||
},
|
||||
{
|
||||
"name": "search",
|
||||
"original_name": "search",
|
||||
"description": "Search tool",
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"query": {"type": "string", "description": "Search query"}
|
||||
},
|
||||
"required": ["query"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=hyphenated_tool_definitions)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
tools = agent.get_mcp_tools(["notion#get-page"])
|
||||
|
||||
mock_fetch.assert_called_once_with(["notion"])
|
||||
assert len(tools) == 1
|
||||
assert tools[0].name.endswith("_get_page")
|
||||
|
||||
@patch("crewai.mcp.tool_resolver.MCPClient")
|
||||
@patch.object(MCPToolResolver, "_fetch_amp_mcp_configs")
|
||||
def test_deduplicates_slugs(
|
||||
@@ -419,87 +371,3 @@ class TestGetMCPToolsAmpIntegration:
|
||||
mock_external.assert_called_once_with("https://external.mcp.com/api")
|
||||
# 2 from notion + 1 from external + 2 from http_config
|
||||
assert len(tools) == 5
|
||||
|
||||
|
||||
class TestResolveExternalToolFilter:
|
||||
"""Tests for _resolve_external with #tool-name filtering."""
|
||||
|
||||
@pytest.fixture
|
||||
def agent(self):
|
||||
return Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
)
|
||||
|
||||
@pytest.fixture
|
||||
def resolver(self, agent):
|
||||
return MCPToolResolver(agent=agent, logger=agent._logger)
|
||||
|
||||
@patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
|
||||
def test_filters_hyphenated_tool_name(self, mock_schemas, resolver):
|
||||
"""https://...#get-page must match the sanitized key get_page in schemas."""
|
||||
mock_schemas.return_value = {
|
||||
"get_page": {
|
||||
"description": "Get a page",
|
||||
"args_schema": None,
|
||||
},
|
||||
"search": {
|
||||
"description": "Search tool",
|
||||
"args_schema": None,
|
||||
},
|
||||
}
|
||||
|
||||
tools = resolver._resolve_external("https://mcp.example.com/api#get-page")
|
||||
|
||||
assert len(tools) == 1
|
||||
assert "get_page" in tools[0].name
|
||||
|
||||
@patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
|
||||
def test_filters_underscored_tool_name(self, mock_schemas, resolver):
|
||||
"""https://...#get_page must also match the sanitized key get_page."""
|
||||
mock_schemas.return_value = {
|
||||
"get_page": {
|
||||
"description": "Get a page",
|
||||
"args_schema": None,
|
||||
},
|
||||
"search": {
|
||||
"description": "Search tool",
|
||||
"args_schema": None,
|
||||
},
|
||||
}
|
||||
|
||||
tools = resolver._resolve_external("https://mcp.example.com/api#get_page")
|
||||
|
||||
assert len(tools) == 1
|
||||
assert "get_page" in tools[0].name
|
||||
|
||||
@patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
|
||||
def test_returns_all_tools_without_hash(self, mock_schemas, resolver):
|
||||
mock_schemas.return_value = {
|
||||
"get_page": {
|
||||
"description": "Get a page",
|
||||
"args_schema": None,
|
||||
},
|
||||
"search": {
|
||||
"description": "Search tool",
|
||||
"args_schema": None,
|
||||
},
|
||||
}
|
||||
|
||||
tools = resolver._resolve_external("https://mcp.example.com/api")
|
||||
|
||||
assert len(tools) == 2
|
||||
|
||||
@patch.object(MCPToolResolver, "_get_mcp_tool_schemas")
|
||||
def test_returns_empty_for_nonexistent_tool(self, mock_schemas, resolver):
|
||||
mock_schemas.return_value = {
|
||||
"search": {
|
||||
"description": "Search tool",
|
||||
"args_schema": None,
|
||||
},
|
||||
}
|
||||
|
||||
tools = resolver._resolve_external("https://mcp.example.com/api#nonexistent")
|
||||
|
||||
assert len(tools) == 0
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -31,17 +30,6 @@ def mock_tool_definitions():
|
||||
]
|
||||
|
||||
|
||||
def _make_mock_client(tool_definitions):
|
||||
"""Create a mock MCPClient that returns *tool_definitions*."""
|
||||
client = AsyncMock()
|
||||
client.list_tools = AsyncMock(return_value=tool_definitions)
|
||||
client.connected = False
|
||||
client.connect = AsyncMock()
|
||||
client.disconnect = AsyncMock()
|
||||
client.call_tool = AsyncMock(return_value="test result")
|
||||
return client
|
||||
|
||||
|
||||
def test_agent_with_stdio_mcp_config(mock_tool_definitions):
|
||||
"""Test agent setup with MCPServerStdio configuration."""
|
||||
stdio_config = MCPServerStdio(
|
||||
@@ -57,8 +45,14 @@ def test_agent_with_stdio_mcp_config(mock_tool_definitions):
|
||||
mcps=[stdio_config],
|
||||
)
|
||||
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient") as mock_client_class:
|
||||
mock_client_class.return_value = _make_mock_client(mock_tool_definitions)
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
mock_client.connected = False # Will trigger connect
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
tools = agent.get_mcp_tools([stdio_config])
|
||||
|
||||
@@ -66,7 +60,8 @@ def test_agent_with_stdio_mcp_config(mock_tool_definitions):
|
||||
assert all(isinstance(tool, BaseTool) for tool in tools)
|
||||
|
||||
mock_client_class.assert_called_once()
|
||||
transport = mock_client_class.call_args.kwargs["transport"]
|
||||
call_args = mock_client_class.call_args
|
||||
transport = call_args.kwargs["transport"]
|
||||
assert transport.command == "python"
|
||||
assert transport.args == ["server.py"]
|
||||
assert transport.env == {"API_KEY": "test_key"}
|
||||
@@ -88,7 +83,12 @@ def test_agent_with_http_mcp_config(mock_tool_definitions):
|
||||
)
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient") as mock_client_class:
|
||||
mock_client_class.return_value = _make_mock_client(mock_tool_definitions)
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
mock_client.connected = False # Will trigger connect
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
tools = agent.get_mcp_tools([http_config])
|
||||
|
||||
@@ -96,7 +96,8 @@ def test_agent_with_http_mcp_config(mock_tool_definitions):
|
||||
assert all(isinstance(tool, BaseTool) for tool in tools)
|
||||
|
||||
mock_client_class.assert_called_once()
|
||||
transport = mock_client_class.call_args.kwargs["transport"]
|
||||
call_args = mock_client_class.call_args
|
||||
transport = call_args.kwargs["transport"]
|
||||
assert transport.url == "https://api.example.com/mcp"
|
||||
assert transport.headers == {"Authorization": "Bearer test_token"}
|
||||
assert transport.streamable is True
|
||||
@@ -117,7 +118,12 @@ def test_agent_with_sse_mcp_config(mock_tool_definitions):
|
||||
)
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient") as mock_client_class:
|
||||
mock_client_class.return_value = _make_mock_client(mock_tool_definitions)
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
tools = agent.get_mcp_tools([sse_config])
|
||||
|
||||
@@ -125,7 +131,8 @@ def test_agent_with_sse_mcp_config(mock_tool_definitions):
|
||||
assert all(isinstance(tool, BaseTool) for tool in tools)
|
||||
|
||||
mock_client_class.assert_called_once()
|
||||
transport = mock_client_class.call_args.kwargs["transport"]
|
||||
call_args = mock_client_class.call_args
|
||||
transport = call_args.kwargs["transport"]
|
||||
assert transport.url == "https://api.example.com/mcp/sse"
|
||||
assert transport.headers == {"Authorization": "Bearer test_token"}
|
||||
|
||||
@@ -135,7 +142,13 @@ def test_mcp_tool_execution_in_sync_context(mock_tool_definitions):
|
||||
http_config = MCPServerHTTP(url="https://api.example.com/mcp")
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient") as mock_client_class:
|
||||
mock_client_class.return_value = _make_mock_client(mock_tool_definitions)
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client.call_tool = AsyncMock(return_value="test result")
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
@@ -147,12 +160,12 @@ def test_mcp_tool_execution_in_sync_context(mock_tool_definitions):
|
||||
tools = agent.get_mcp_tools([http_config])
|
||||
assert len(tools) == 2
|
||||
|
||||
|
||||
tool = tools[0]
|
||||
result = tool.run(query="test query")
|
||||
|
||||
assert result == "test result"
|
||||
# 1 discovery + 1 for the run() invocation
|
||||
assert mock_client_class.call_count == 2
|
||||
mock_client.call_tool.assert_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@@ -161,7 +174,13 @@ async def test_mcp_tool_execution_in_async_context(mock_tool_definitions):
|
||||
http_config = MCPServerHTTP(url="https://api.example.com/mcp")
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient") as mock_client_class:
|
||||
mock_client_class.return_value = _make_mock_client(mock_tool_definitions)
|
||||
mock_client = AsyncMock()
|
||||
mock_client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
mock_client.connected = False
|
||||
mock_client.connect = AsyncMock()
|
||||
mock_client.disconnect = AsyncMock()
|
||||
mock_client.call_tool = AsyncMock(return_value="test result")
|
||||
mock_client_class.return_value = mock_client
|
||||
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
@@ -173,129 +192,9 @@ async def test_mcp_tool_execution_in_async_context(mock_tool_definitions):
|
||||
tools = agent.get_mcp_tools([http_config])
|
||||
assert len(tools) == 2
|
||||
|
||||
|
||||
tool = tools[0]
|
||||
result = tool.run(query="test query")
|
||||
|
||||
assert result == "test result"
|
||||
assert mock_client_class.call_count == 2
|
||||
|
||||
|
||||
def test_each_invocation_gets_fresh_client(mock_tool_definitions):
|
||||
"""Every tool.run() must create its own MCPClient (no shared state)."""
|
||||
http_config = MCPServerHTTP(url="https://api.example.com/mcp")
|
||||
|
||||
clients_created: list = []
|
||||
|
||||
def _make_client(**kwargs):
|
||||
client = _make_mock_client(mock_tool_definitions)
|
||||
clients_created.append(client)
|
||||
return client
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient", side_effect=_make_client):
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
mcps=[http_config],
|
||||
)
|
||||
|
||||
tools = agent.get_mcp_tools([http_config])
|
||||
assert len(tools) == 2
|
||||
# 1 discovery client so far
|
||||
assert len(clients_created) == 1
|
||||
|
||||
# Two sequential calls to the same tool must create 2 new clients
|
||||
tools[0].run(query="q1")
|
||||
tools[0].run(query="q2")
|
||||
assert len(clients_created) == 3
|
||||
assert clients_created[1] is not clients_created[2]
|
||||
|
||||
|
||||
def test_parallel_mcp_tool_execution_same_tool(mock_tool_definitions):
|
||||
"""Parallel calls to the *same* tool must not interfere."""
|
||||
http_config = MCPServerHTTP(url="https://api.example.com/mcp")
|
||||
|
||||
call_log: list[str] = []
|
||||
|
||||
def _make_client(**kwargs):
|
||||
client = AsyncMock()
|
||||
client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
client.connected = False
|
||||
client.connect = AsyncMock()
|
||||
client.disconnect = AsyncMock()
|
||||
|
||||
async def _call_tool(name, args):
|
||||
call_log.append(name)
|
||||
await asyncio.sleep(0.05)
|
||||
return f"result-{name}"
|
||||
|
||||
client.call_tool = AsyncMock(side_effect=_call_tool)
|
||||
return client
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient", side_effect=_make_client):
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
mcps=[http_config],
|
||||
)
|
||||
|
||||
tools = agent.get_mcp_tools([http_config])
|
||||
assert len(tools) >= 1
|
||||
tool = tools[0]
|
||||
|
||||
# Call the SAME tool concurrently -- the exact scenario from the bug
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
||||
futures = [
|
||||
pool.submit(tool.run, query="q1"),
|
||||
pool.submit(tool.run, query="q2"),
|
||||
]
|
||||
results = [f.result() for f in concurrent.futures.as_completed(futures)]
|
||||
|
||||
assert len(results) == 2
|
||||
assert all("result-" in r for r in results)
|
||||
assert len(call_log) == 2
|
||||
|
||||
|
||||
def test_parallel_mcp_tool_execution_different_tools(mock_tool_definitions):
|
||||
"""Parallel calls to different tools from the same server must not interfere."""
|
||||
http_config = MCPServerHTTP(url="https://api.example.com/mcp")
|
||||
|
||||
call_log: list[str] = []
|
||||
|
||||
def _make_client(**kwargs):
|
||||
client = AsyncMock()
|
||||
client.list_tools = AsyncMock(return_value=mock_tool_definitions)
|
||||
client.connected = False
|
||||
client.connect = AsyncMock()
|
||||
client.disconnect = AsyncMock()
|
||||
|
||||
async def _call_tool(name, args):
|
||||
call_log.append(name)
|
||||
await asyncio.sleep(0.05)
|
||||
return f"result-{name}"
|
||||
|
||||
client.call_tool = AsyncMock(side_effect=_call_tool)
|
||||
return client
|
||||
|
||||
with patch("crewai.mcp.tool_resolver.MCPClient", side_effect=_make_client):
|
||||
agent = Agent(
|
||||
role="Test Agent",
|
||||
goal="Test goal",
|
||||
backstory="Test backstory",
|
||||
mcps=[http_config],
|
||||
)
|
||||
|
||||
tools = agent.get_mcp_tools([http_config])
|
||||
assert len(tools) == 2
|
||||
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=2) as pool:
|
||||
futures = [
|
||||
pool.submit(tools[0].run, query="q1"),
|
||||
pool.submit(tools[1].run, query="q2"),
|
||||
]
|
||||
results = [f.result() for f in concurrent.futures.as_completed(futures)]
|
||||
|
||||
assert len(results) == 2
|
||||
assert all("result-" in r for r in results)
|
||||
assert len(call_log) == 2
|
||||
mock_client.call_tool.assert_called()
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Stress tests for concurrent multi-process storage access.
|
||||
|
||||
Simulates the Airflow pattern: N worker processes each writing to the
|
||||
same storage directory simultaneously. Verifies no LockException and
|
||||
data integrity after all writes complete.
|
||||
|
||||
Uses temp files for IPC instead of multiprocessing.Manager (which uses
|
||||
sockets blocked by pytest_recording).
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
pytestmark = pytest.mark.skip(reason="Multiprocessing tests incompatible with xdist --import-mode=importlib")
|
||||
@@ -971,128 +971,6 @@ class TestCollapseToOutcomeJsonParsing:
|
||||
assert mock_llm.call.call_count == 2
|
||||
|
||||
|
||||
class TestLLMObjectPreservedInContext:
|
||||
"""Tests that BaseLLM objects have their model string preserved in PendingFeedbackContext."""
|
||||
|
||||
@patch("crewai.flow.flow.crewai_event_bus.emit")
|
||||
def test_basellm_object_model_string_survives_roundtrip(self, mock_emit: MagicMock) -> None:
|
||||
"""Test that when llm is a BaseLLM object, its model string is stored in context
|
||||
so that outcome collapsing works after async pause/resume.
|
||||
|
||||
This is the exact bug: locally the sync path keeps the LLM object in memory,
|
||||
but in production the async path serializes the context and the LLM object was
|
||||
discarded (stored as None), causing resume to skip classification and always
|
||||
fall back to emit[0].
|
||||
"""
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
db_path = os.path.join(tmpdir, "test_flows.db")
|
||||
persistence = SQLiteFlowPersistence(db_path)
|
||||
|
||||
# Create a mock BaseLLM object (not a string)
|
||||
mock_llm_obj = MagicMock()
|
||||
mock_llm_obj.model = "gemini/gemini-2.0-flash"
|
||||
|
||||
class PausingProvider:
|
||||
def __init__(self, persistence: SQLiteFlowPersistence):
|
||||
self.persistence = persistence
|
||||
self.captured_context: PendingFeedbackContext | None = None
|
||||
|
||||
def request_feedback(
|
||||
self, context: PendingFeedbackContext, flow: Flow
|
||||
) -> str:
|
||||
self.captured_context = context
|
||||
self.persistence.save_pending_feedback(
|
||||
flow_uuid=context.flow_id,
|
||||
context=context,
|
||||
state_data=flow.state if isinstance(flow.state, dict) else flow.state.model_dump(),
|
||||
)
|
||||
raise HumanFeedbackPending(context=context)
|
||||
|
||||
provider = PausingProvider(persistence)
|
||||
|
||||
class TestFlow(Flow):
|
||||
result_path: str = ""
|
||||
|
||||
@start()
|
||||
@human_feedback(
|
||||
message="Approve?",
|
||||
emit=["needs_changes", "approved"],
|
||||
llm=mock_llm_obj,
|
||||
default_outcome="approved",
|
||||
provider=provider,
|
||||
)
|
||||
def review(self):
|
||||
return "content for review"
|
||||
|
||||
@listen("approved")
|
||||
def handle_approved(self):
|
||||
self.result_path = "approved"
|
||||
return "Approved!"
|
||||
|
||||
@listen("needs_changes")
|
||||
def handle_changes(self):
|
||||
self.result_path = "needs_changes"
|
||||
return "Changes needed"
|
||||
|
||||
# Phase 1: Start flow (should pause)
|
||||
flow1 = TestFlow(persistence=persistence)
|
||||
result = flow1.kickoff()
|
||||
assert isinstance(result, HumanFeedbackPending)
|
||||
|
||||
# Verify the context stored the model STRING, not None
|
||||
assert provider.captured_context is not None
|
||||
assert provider.captured_context.llm == "gemini/gemini-2.0-flash"
|
||||
|
||||
# Verify it survives persistence roundtrip
|
||||
flow_id = result.context.flow_id
|
||||
loaded = persistence.load_pending_feedback(flow_id)
|
||||
assert loaded is not None
|
||||
_, loaded_context = loaded
|
||||
assert loaded_context.llm == "gemini/gemini-2.0-flash"
|
||||
|
||||
# Phase 2: Resume with positive feedback - should use LLM to classify
|
||||
flow2 = TestFlow.from_pending(flow_id, persistence)
|
||||
assert flow2._pending_feedback_context is not None
|
||||
assert flow2._pending_feedback_context.llm == "gemini/gemini-2.0-flash"
|
||||
|
||||
# Mock _collapse_to_outcome to verify it gets called (not skipped)
|
||||
with patch.object(flow2, "_collapse_to_outcome", return_value="approved") as mock_collapse:
|
||||
flow2.resume("this looks good, proceed!")
|
||||
|
||||
# The key assertion: _collapse_to_outcome was called (not skipped due to llm=None)
|
||||
mock_collapse.assert_called_once_with(
|
||||
feedback="this looks good, proceed!",
|
||||
outcomes=["needs_changes", "approved"],
|
||||
llm="gemini/gemini-2.0-flash",
|
||||
)
|
||||
assert flow2.last_human_feedback.outcome == "approved"
|
||||
assert flow2.result_path == "approved"
|
||||
|
||||
def test_string_llm_still_works(self) -> None:
|
||||
"""Test that passing llm as a string still works correctly."""
|
||||
context = PendingFeedbackContext(
|
||||
flow_id="str-llm-test",
|
||||
flow_class="test.Flow",
|
||||
method_name="review",
|
||||
method_output="output",
|
||||
message="Review:",
|
||||
emit=["approved", "rejected"],
|
||||
llm="gpt-4o-mini",
|
||||
)
|
||||
|
||||
serialized = context.to_dict()
|
||||
restored = PendingFeedbackContext.from_dict(serialized)
|
||||
assert restored.llm == "gpt-4o-mini"
|
||||
|
||||
def test_none_llm_when_no_model_attr(self) -> None:
|
||||
"""Test that llm is None when object has no model attribute."""
|
||||
mock_obj = MagicMock(spec=[]) # No attributes
|
||||
|
||||
# Simulate what the decorator does
|
||||
llm_value = mock_obj if isinstance(mock_obj, str) else getattr(mock_obj, "model", None)
|
||||
assert llm_value is None
|
||||
|
||||
|
||||
class TestAsyncHumanFeedbackEdgeCases:
|
||||
"""Edge case tests for async human feedback."""
|
||||
|
||||
|
||||
@@ -1101,7 +1101,7 @@ def test_single_task_with_async_execution():
|
||||
|
||||
result = crew.kickoff()
|
||||
assert result.raw.startswith(
|
||||
"- Impact of autonomous AI agents on future workplace automation"
|
||||
"- Ethical implications of AI in law enforcement and surveillance."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1893,163 +1893,3 @@ def test_or_condition_self_listen_fires_once():
|
||||
flow = OrSelfListenFlow()
|
||||
flow.kickoff()
|
||||
assert call_count == 1
|
||||
|
||||
class ListState(BaseModel):
|
||||
items: list = []
|
||||
|
||||
|
||||
class DictState(BaseModel):
|
||||
data: dict = {}
|
||||
|
||||
|
||||
class _ListFlow(Flow[ListState]):
|
||||
@start()
|
||||
def populate(self):
|
||||
self.state.items = [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
|
||||
|
||||
class _DictFlow(Flow[DictState]):
|
||||
@start()
|
||||
def populate(self):
|
||||
self.state.data = {"a": 1, "b": 2, "c": 3}
|
||||
|
||||
|
||||
def _make_list_flow():
|
||||
flow = _ListFlow()
|
||||
flow.kickoff()
|
||||
return flow
|
||||
|
||||
|
||||
def _make_dict_flow():
|
||||
flow = _DictFlow()
|
||||
flow.kickoff()
|
||||
return flow
|
||||
|
||||
|
||||
def test_locked_list_proxy_index():
|
||||
flow = _make_list_flow()
|
||||
assert flow.state.items.index(4) == 2
|
||||
assert flow.state.items.index(1, 2) == 3
|
||||
|
||||
|
||||
def test_locked_list_proxy_index_missing_raises():
|
||||
flow = _make_list_flow()
|
||||
with pytest.raises(ValueError):
|
||||
flow.state.items.index(999)
|
||||
|
||||
|
||||
def test_locked_list_proxy_count():
|
||||
flow = _make_list_flow()
|
||||
assert flow.state.items.count(1) == 2
|
||||
assert flow.state.items.count(999) == 0
|
||||
|
||||
|
||||
def test_locked_list_proxy_sort():
|
||||
flow = _make_list_flow()
|
||||
flow.state.items.sort()
|
||||
assert list(flow.state.items) == [1, 1, 2, 3, 4, 5, 6, 9]
|
||||
|
||||
|
||||
def test_locked_list_proxy_sort_reverse():
|
||||
flow = _make_list_flow()
|
||||
flow.state.items.sort(reverse=True)
|
||||
assert list(flow.state.items) == [9, 6, 5, 4, 3, 2, 1, 1]
|
||||
|
||||
|
||||
def test_locked_list_proxy_sort_key():
|
||||
flow = _make_list_flow()
|
||||
flow.state.items.sort(key=lambda x: -x)
|
||||
assert list(flow.state.items) == [9, 6, 5, 4, 3, 2, 1, 1]
|
||||
|
||||
|
||||
def test_locked_list_proxy_reverse():
|
||||
flow = _make_list_flow()
|
||||
original = list(flow.state.items)
|
||||
flow.state.items.reverse()
|
||||
assert list(flow.state.items) == list(reversed(original))
|
||||
|
||||
|
||||
def test_locked_list_proxy_copy():
|
||||
flow = _make_list_flow()
|
||||
copied = flow.state.items.copy()
|
||||
assert copied == [3, 1, 4, 1, 5, 9, 2, 6]
|
||||
assert isinstance(copied, list)
|
||||
copied.append(999)
|
||||
assert 999 not in flow.state.items
|
||||
|
||||
|
||||
def test_locked_list_proxy_add():
|
||||
flow = _make_list_flow()
|
||||
result = flow.state.items + [10, 11]
|
||||
assert result == [3, 1, 4, 1, 5, 9, 2, 6, 10, 11]
|
||||
assert len(flow.state.items) == 8
|
||||
|
||||
|
||||
def test_locked_list_proxy_radd():
|
||||
flow = _make_list_flow()
|
||||
result = [0] + flow.state.items
|
||||
assert result[0] == 0
|
||||
assert len(result) == 9
|
||||
|
||||
|
||||
def test_locked_list_proxy_iadd():
|
||||
flow = _make_list_flow()
|
||||
flow.state.items += [10]
|
||||
assert 10 in flow.state.items
|
||||
# Verify no deadlock: mutations must still work after +=
|
||||
flow.state.items.append(99)
|
||||
assert 99 in flow.state.items
|
||||
|
||||
|
||||
def test_locked_list_proxy_mul():
|
||||
flow = _make_list_flow()
|
||||
result = flow.state.items * 2
|
||||
assert len(result) == 16
|
||||
|
||||
|
||||
def test_locked_list_proxy_rmul():
|
||||
flow = _make_list_flow()
|
||||
result = 2 * flow.state.items
|
||||
assert len(result) == 16
|
||||
|
||||
|
||||
def test_locked_list_proxy_reversed():
|
||||
flow = _make_list_flow()
|
||||
original = list(flow.state.items)
|
||||
assert list(reversed(flow.state.items)) == list(reversed(original))
|
||||
|
||||
|
||||
def test_locked_dict_proxy_copy():
|
||||
flow = _make_dict_flow()
|
||||
copied = flow.state.data.copy()
|
||||
assert copied == {"a": 1, "b": 2, "c": 3}
|
||||
assert isinstance(copied, dict)
|
||||
copied["z"] = 99
|
||||
assert "z" not in flow.state.data
|
||||
|
||||
|
||||
def test_locked_dict_proxy_or():
|
||||
flow = _make_dict_flow()
|
||||
result = flow.state.data | {"d": 4}
|
||||
assert result == {"a": 1, "b": 2, "c": 3, "d": 4}
|
||||
assert "d" not in flow.state.data
|
||||
|
||||
|
||||
def test_locked_dict_proxy_ror():
|
||||
flow = _make_dict_flow()
|
||||
result = {"z": 0} | flow.state.data
|
||||
assert result == {"z": 0, "a": 1, "b": 2, "c": 3}
|
||||
|
||||
|
||||
def test_locked_dict_proxy_ior():
|
||||
flow = _make_dict_flow()
|
||||
flow.state.data |= {"d": 4}
|
||||
assert flow.state.data["d"] == 4
|
||||
# Verify no deadlock: mutations must still work after |=
|
||||
flow.state.data["e"] = 5
|
||||
assert flow.state.data["e"] == 5
|
||||
|
||||
|
||||
def test_locked_dict_proxy_reversed():
|
||||
flow = _make_dict_flow()
|
||||
assert list(reversed(flow.state.data)) == ["c", "b", "a"]
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.10.2a1"
|
||||
__version__ = "1.10.1"
|
||||
|
||||
Reference in New Issue
Block a user