mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-01-24 07:38:14 +00:00
Merge branch 'main' of github.com:crewAIInc/crewAI into devin/1741108142-custom-llm-support
This commit is contained in:
2
LICENSE
2
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
Copyright (c) 2018 The Python Packaging Authority
|
Copyright (c) 2025 crewAI, Inc.
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
173
README.md
173
README.md
@@ -2,21 +2,46 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
# **CrewAI**
|
|
||||||
|
|
||||||
**CrewAI**: Production-grade framework for orchestrating sophisticated AI agent systems. From simple automations to complex real-world applications, CrewAI provides precise control and deep customization. By fostering collaborative intelligence through flexible, production-ready architecture, CrewAI empowers agents to work together seamlessly, tackling complex business challenges with predictable, consistent results.
|
</div>
|
||||||
|
|
||||||
**CrewAI Enterprise**
|
### Fast and Flexible Multi-Agent Automation Framework
|
||||||
Want to plan, build (+ no code), deploy, monitor and interare your agents: [CrewAI Enterprise](https://www.crewai.com/enterprise). Designed for complex, real-world applications, our enterprise solution offers:
|
|
||||||
|
|
||||||
- **Seamless Integrations**
|
CrewAI is a lean, lightning-fast Python framework built entirely from
|
||||||
- **Scalable & Secure Deployment**
|
scratch—completely **independent of LangChain or other agent frameworks**.
|
||||||
- **Actionable Insights**
|
It empowers developers with both high-level simplicity and precise low-level
|
||||||
- **24/7 Support**
|
control, ideal for creating autonomous AI agents tailored to any scenario.
|
||||||
|
|
||||||
|
- **CrewAI Crews**: Optimize for autonomy and collaborative intelligence.
|
||||||
|
- **CrewAI Flows**: Enable granular, event-driven control, single LLM calls for precise task orchestration and supports Crews natively
|
||||||
|
|
||||||
|
With over 100,000 developers certified through our community courses at
|
||||||
|
[learn.crewai.com](https://learn.crewai.com), CrewAI is rapidly becoming the
|
||||||
|
standard for enterprise-ready AI automation.
|
||||||
|
|
||||||
|
# CrewAI Enterprise Suite
|
||||||
|
|
||||||
|
CrewAI Enterprise Suite is a comprehensive bundle tailored for organizations
|
||||||
|
that require secure, scalable, and easy-to-manage agent-driven automation.
|
||||||
|
|
||||||
|
You can try one part of the suite the [Crew Control Plane for free](https://app.crewai.com)
|
||||||
|
|
||||||
|
## Crew Control Plane Key Features:
|
||||||
|
- **Tracing & Observability**: Monitor and track your AI agents and workflows in real-time, including metrics, logs, and traces.
|
||||||
|
- **Unified Control Plane**: A centralized platform for managing, monitoring, and scaling your AI agents and workflows.
|
||||||
|
- **Seamless Integrations**: Easily connect with existing enterprise systems, data sources, and cloud infrastructure.
|
||||||
|
- **Advanced Security**: Built-in robust security and compliance measures ensuring safe deployment and management.
|
||||||
|
- **Actionable Insights**: Real-time analytics and reporting to optimize performance and decision-making.
|
||||||
|
- **24/7 Support**: Dedicated enterprise support to ensure uninterrupted operation and quick resolution of issues.
|
||||||
|
- **On-premise and Cloud Deployment Options**: Deploy CrewAI Enterprise on-premise or in the cloud, depending on your security and compliance requirements.
|
||||||
|
|
||||||
|
CrewAI Enterprise is designed for enterprises seeking a powerful,
|
||||||
|
reliable solution to transform complex business processes into efficient,
|
||||||
|
intelligent automations.
|
||||||
|
|
||||||
<h3>
|
<h3>
|
||||||
|
|
||||||
[Homepage](https://www.crewai.com/) | [Documentation](https://docs.crewai.com/) | [Chat with Docs](https://chatg.pt/DWjSBZn) | [Examples](https://github.com/crewAIInc/crewAI-examples) | [Discourse](https://community.crewai.com)
|
[Homepage](https://www.crewai.com/) | [Documentation](https://docs.crewai.com/) | [Chat with Docs](https://chatg.pt/DWjSBZn) | [Discourse](https://community.crewai.com)
|
||||||
|
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@@ -47,8 +72,19 @@ Want to plan, build (+ no code), deploy, monitor and interare your agents: [Crew
|
|||||||
|
|
||||||
## Why CrewAI?
|
## Why CrewAI?
|
||||||
|
|
||||||
The power of AI collaboration has too much to offer.
|
<div align="center" style="margin-bottom: 30px;">
|
||||||
CrewAI is a standalone framework, built from the ground up without dependencies on Langchain or other agent frameworks. It's designed to enable AI agents to assume roles, share goals, and operate in a cohesive unit - much like a well-oiled crew. Whether you're building a smart assistant platform, an automated customer service ensemble, or a multi-agent research team, CrewAI provides the backbone for sophisticated multi-agent interactions.
|
<img src="docs/asset.png" alt="CrewAI Logo" width="100%">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
CrewAI unlocks the true potential of multi-agent automation, delivering the best-in-class combination of speed, flexibility, and control with either Crews of AI Agents or Flows of Events:
|
||||||
|
|
||||||
|
- **Standalone Framework**: Built from scratch, independent of LangChain or any other agent framework.
|
||||||
|
- **High Performance**: Optimized for speed and minimal resource usage, enabling faster execution.
|
||||||
|
- **Flexible Low Level Customization**: Complete freedom to customize at both high and low levels - from overall workflows and system architecture to granular agent behaviors, internal prompts, and execution logic.
|
||||||
|
- **Ideal for Every Use Case**: Proven effective for both simple tasks and highly complex, real-world, enterprise-grade scenarios.
|
||||||
|
- **Robust Community**: Backed by a rapidly growing community of over **100,000 certified** developers offering comprehensive support and resources.
|
||||||
|
|
||||||
|
CrewAI empowers developers and enterprises to confidently build intelligent automations, bridging the gap between simplicity, flexibility, and performance.
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
@@ -321,18 +357,16 @@ In addition to the sequential process, you can use the hierarchical process, whi
|
|||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
**Note**: CrewAI is a standalone framework built from the ground up, without dependencies on Langchain or other agent frameworks.
|
CrewAI stands apart as a lean, standalone, high-performance framework delivering simplicity, flexibility, and precise control—free from the complexity and limitations found in other agent frameworks.
|
||||||
|
|
||||||
- **Deep Customization**: Build sophisticated agents with full control over the system - from overriding inner prompts to accessing low-level APIs. Customize roles, goals, tools, and behaviors while maintaining clean abstractions.
|
- **Standalone & Lean**: Completely independent from other frameworks like LangChain, offering faster execution and lighter resource demands.
|
||||||
- **Autonomous Inter-Agent Delegation**: Agents can autonomously delegate tasks and inquire amongst themselves, enabling complex problem-solving in real-world scenarios.
|
- **Flexible & Precise**: Easily orchestrate autonomous agents through intuitive [Crews](https://docs.crewai.com/concepts/crews) or precise [Flows](https://docs.crewai.com/concepts/flows), achieving perfect balance for your needs.
|
||||||
- **Flexible Task Management**: Define and customize tasks with granular control, from simple operations to complex multi-step processes.
|
- **Seamless Integration**: Effortlessly combine Crews (autonomy) and Flows (precision) to create complex, real-world automations.
|
||||||
- **Production-Grade Architecture**: Support for both high-level abstractions and low-level customization, with robust error handling and state management.
|
- **Deep Customization**: Tailor every aspect—from high-level workflows down to low-level internal prompts and agent behaviors.
|
||||||
- **Predictable Results**: Ensure consistent, accurate outputs through programmatic guardrails, agent training capabilities, and flow-based execution control. See our [documentation on guardrails](https://docs.crewai.com/how-to/guardrails/) for implementation details.
|
- **Reliable Performance**: Consistent results across simple tasks and complex, enterprise-level automations.
|
||||||
- **Model Flexibility**: Run your crew using OpenAI or open source models with production-ready integrations. See [Connect CrewAI to LLMs](https://docs.crewai.com/how-to/LLM-Connections/) for detailed configuration options.
|
- **Thriving Community**: Backed by robust documentation and over 100,000 certified developers, providing exceptional support and guidance.
|
||||||
- **Event-Driven Flows**: Build complex, real-world workflows with precise control over execution paths, state management, and conditional logic.
|
|
||||||
- **Process Orchestration**: Achieve any workflow pattern through flows - from simple sequential and hierarchical processes to complex, custom orchestration patterns with conditional branching and parallel execution.
|
|
||||||
|
|
||||||

|
Choose CrewAI to easily build powerful, adaptable, and production-ready AI automations.
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
|
||||||
@@ -563,13 +597,39 @@ Users can opt-in to Further Telemetry, sharing the complete telemetry data by se
|
|||||||
|
|
||||||
CrewAI is released under the [MIT License](https://github.com/crewAIInc/crewAI/blob/main/LICENSE).
|
CrewAI is released under the [MIT License](https://github.com/crewAIInc/crewAI/blob/main/LICENSE).
|
||||||
|
|
||||||
|
|
||||||
## Frequently Asked Questions (FAQ)
|
## Frequently Asked Questions (FAQ)
|
||||||
|
|
||||||
### Q: What is CrewAI?
|
### General
|
||||||
A: CrewAI is a cutting-edge framework for orchestrating role-playing, autonomous AI agents. It enables agents to work together seamlessly, tackling complex tasks through collaborative intelligence.
|
- [What exactly is CrewAI?](#q-what-exactly-is-crewai)
|
||||||
|
- [How do I install CrewAI?](#q-how-do-i-install-crewai)
|
||||||
|
- [Does CrewAI depend on LangChain?](#q-does-crewai-depend-on-langchain)
|
||||||
|
- [Is CrewAI open-source?](#q-is-crewai-open-source)
|
||||||
|
- [Does CrewAI collect data from users?](#q-does-crewai-collect-data-from-users)
|
||||||
|
|
||||||
|
### Features and Capabilities
|
||||||
|
- [Can CrewAI handle complex use cases?](#q-can-crewai-handle-complex-use-cases)
|
||||||
|
- [Can I use CrewAI with local AI models?](#q-can-i-use-crewai-with-local-ai-models)
|
||||||
|
- [What makes Crews different from Flows?](#q-what-makes-crews-different-from-flows)
|
||||||
|
- [How is CrewAI better than LangChain?](#q-how-is-crewai-better-than-langchain)
|
||||||
|
- [Does CrewAI support fine-tuning or training custom models?](#q-does-crewai-support-fine-tuning-or-training-custom-models)
|
||||||
|
|
||||||
|
### Resources and Community
|
||||||
|
- [Where can I find real-world CrewAI examples?](#q-where-can-i-find-real-world-crewai-examples)
|
||||||
|
- [How can I contribute to CrewAI?](#q-how-can-i-contribute-to-crewai)
|
||||||
|
|
||||||
|
### Enterprise Features
|
||||||
|
- [What additional features does CrewAI Enterprise offer?](#q-what-additional-features-does-crewai-enterprise-offer)
|
||||||
|
- [Is CrewAI Enterprise available for cloud and on-premise deployments?](#q-is-crewai-enterprise-available-for-cloud-and-on-premise-deployments)
|
||||||
|
- [Can I try CrewAI Enterprise for free?](#q-can-i-try-crewai-enterprise-for-free)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Q: What exactly is CrewAI?
|
||||||
|
A: CrewAI is a standalone, lean, and fast Python framework built specifically for orchestrating autonomous AI agents. Unlike frameworks like LangChain, CrewAI does not rely on external dependencies, making it leaner, faster, and simpler.
|
||||||
|
|
||||||
### Q: How do I install CrewAI?
|
### Q: How do I install CrewAI?
|
||||||
A: You can install CrewAI using pip:
|
A: Install CrewAI using pip:
|
||||||
```shell
|
```shell
|
||||||
pip install crewai
|
pip install crewai
|
||||||
```
|
```
|
||||||
@@ -577,27 +637,62 @@ For additional tools, use:
|
|||||||
```shell
|
```shell
|
||||||
pip install 'crewai[tools]'
|
pip install 'crewai[tools]'
|
||||||
```
|
```
|
||||||
|
### Q: Does CrewAI depend on LangChain?
|
||||||
|
A: No. CrewAI is built entirely from the ground up, with no dependencies on LangChain or other agent frameworks. This ensures a lean, fast, and flexible experience.
|
||||||
|
|
||||||
### Q: Can I use CrewAI with local models?
|
### Q: Can CrewAI handle complex use cases?
|
||||||
A: Yes, CrewAI supports various LLMs, including local models. You can configure your agents to use local models via tools like Ollama & LM Studio. Check the [LLM Connections documentation](https://docs.crewai.com/how-to/LLM-Connections/) for more details.
|
A: Yes. CrewAI excels at both simple and highly complex real-world scenarios, offering deep customization options at both high and low levels, from internal prompts to sophisticated workflow orchestration.
|
||||||
|
|
||||||
### Q: What are the key features of CrewAI?
|
### Q: Can I use CrewAI with local AI models?
|
||||||
A: Key features include role-based agent design, autonomous inter-agent delegation, flexible task management, process-driven execution, output saving as files, and compatibility with both open-source and proprietary models.
|
A: Absolutely! CrewAI supports various language models, including local ones. Tools like Ollama and LM Studio allow seamless integration. Check the [LLM Connections documentation](https://docs.crewai.com/how-to/LLM-Connections/) for more details.
|
||||||
|
|
||||||
### Q: How does CrewAI compare to other AI orchestration tools?
|
### Q: What makes Crews different from Flows?
|
||||||
A: CrewAI is designed with production in mind, offering flexibility similar to Autogen's conversational agents and structured processes like ChatDev, but with more adaptability for real-world applications.
|
A: Crews provide autonomous agent collaboration, ideal for tasks requiring flexible decision-making and dynamic interaction. Flows offer precise, event-driven control, ideal for managing detailed execution paths and secure state management. You can seamlessly combine both for maximum effectiveness.
|
||||||
|
|
||||||
|
### Q: How is CrewAI better than LangChain?
|
||||||
|
A: CrewAI provides simpler, more intuitive APIs, faster execution speeds, more reliable and consistent results, robust documentation, and an active community—addressing common criticisms and limitations associated with LangChain.
|
||||||
|
|
||||||
### Q: Is CrewAI open-source?
|
### Q: Is CrewAI open-source?
|
||||||
A: Yes, CrewAI is open-source and welcomes contributions from the community.
|
A: Yes, CrewAI is open-source and actively encourages community contributions and collaboration.
|
||||||
|
|
||||||
### Q: Does CrewAI collect any data?
|
### Q: Does CrewAI collect data from users?
|
||||||
A: CrewAI uses anonymous telemetry to collect usage data for improvement purposes. No sensitive data (like prompts, task descriptions, or API calls) is collected. Users can opt-in to share more detailed data by setting `share_crew=True` on their Crews.
|
A: CrewAI collects anonymous telemetry data strictly for improvement purposes. Sensitive data such as prompts, tasks, or API responses are never collected unless explicitly enabled by the user.
|
||||||
|
|
||||||
### Q: Where can I find examples of CrewAI in action?
|
### Q: Where can I find real-world CrewAI examples?
|
||||||
A: You can find various real-life examples in the [CrewAI-examples repository](https://github.com/crewAIInc/crewAI-examples), including trip planners, stock analysis tools, and more.
|
A: Check out practical examples in the [CrewAI-examples repository](https://github.com/crewAIInc/crewAI-examples), covering use cases like trip planners, stock analysis, and job postings.
|
||||||
|
|
||||||
### Q: What is the difference between Crews and Flows?
|
|
||||||
A: Crews and Flows serve different but complementary purposes in CrewAI. Crews are teams of AI agents working together to accomplish specific tasks through role-based collaboration, delivering accurate and predictable results. Flows, on the other hand, are event-driven workflows that can orchestrate both Crews and regular Python code, allowing you to build complex automation pipelines with secure state management and conditional execution paths.
|
|
||||||
|
|
||||||
### Q: How can I contribute to CrewAI?
|
### Q: How can I contribute to CrewAI?
|
||||||
A: Contributions are welcome! You can fork the repository, create a new branch for your feature, add your improvement, and send a pull request. Check the Contribution section in the README for more details.
|
A: Contributions are warmly welcomed! Fork the repository, create your branch, implement your changes, and submit a pull request. See the Contribution section of the README for detailed guidelines.
|
||||||
|
|
||||||
|
### Q: What additional features does CrewAI Enterprise offer?
|
||||||
|
A: CrewAI Enterprise provides advanced features such as a unified control plane, real-time observability, secure integrations, advanced security, actionable insights, and dedicated 24/7 enterprise support.
|
||||||
|
|
||||||
|
### Q: Is CrewAI Enterprise available for cloud and on-premise deployments?
|
||||||
|
A: Yes, CrewAI Enterprise supports both cloud-based and on-premise deployment options, allowing enterprises to meet their specific security and compliance requirements.
|
||||||
|
|
||||||
|
### Q: Can I try CrewAI Enterprise for free?
|
||||||
|
A: Yes, you can explore part of the CrewAI Enterprise Suite by accessing the [Crew Control Plane](https://app.crewai.com) for free.
|
||||||
|
|
||||||
|
### Q: Does CrewAI support fine-tuning or training custom models?
|
||||||
|
A: Yes, CrewAI can integrate with custom-trained or fine-tuned models, allowing you to enhance your agents with domain-specific knowledge and accuracy.
|
||||||
|
|
||||||
|
### Q: Can CrewAI agents interact with external tools and APIs?
|
||||||
|
A: Absolutely! CrewAI agents can easily integrate with external tools, APIs, and databases, empowering them to leverage real-world data and resources.
|
||||||
|
|
||||||
|
### Q: Is CrewAI suitable for production environments?
|
||||||
|
A: Yes, CrewAI is explicitly designed with production-grade standards, ensuring reliability, stability, and scalability for enterprise deployments.
|
||||||
|
|
||||||
|
### Q: How scalable is CrewAI?
|
||||||
|
A: CrewAI is highly scalable, supporting simple automations and large-scale enterprise workflows involving numerous agents and complex tasks simultaneously.
|
||||||
|
|
||||||
|
### Q: Does CrewAI offer debugging and monitoring tools?
|
||||||
|
A: Yes, CrewAI Enterprise includes advanced debugging, tracing, and real-time observability features, simplifying the management and troubleshooting of your automations.
|
||||||
|
|
||||||
|
### Q: What programming languages does CrewAI support?
|
||||||
|
A: CrewAI is primarily Python-based but easily integrates with services and APIs written in any programming language through its flexible API integration capabilities.
|
||||||
|
|
||||||
|
### Q: Does CrewAI offer educational resources for beginners?
|
||||||
|
A: Yes, CrewAI provides extensive beginner-friendly tutorials, courses, and documentation through learn.crewai.com, supporting developers at all skill levels.
|
||||||
|
|
||||||
|
### Q: Can CrewAI automate human-in-the-loop workflows?
|
||||||
|
A: Yes, CrewAI fully supports human-in-the-loop workflows, allowing seamless collaboration between human experts and AI agents for enhanced decision-making.
|
||||||
|
|||||||
BIN
docs/asset.png
Normal file
BIN
docs/asset.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -224,6 +224,7 @@ CrewAI provides a wide range of events that you can listen for:
|
|||||||
- **LLMCallStartedEvent**: Emitted when an LLM call starts
|
- **LLMCallStartedEvent**: Emitted when an LLM call starts
|
||||||
- **LLMCallCompletedEvent**: Emitted when an LLM call completes
|
- **LLMCallCompletedEvent**: Emitted when an LLM call completes
|
||||||
- **LLMCallFailedEvent**: Emitted when an LLM call fails
|
- **LLMCallFailedEvent**: Emitted when an LLM call fails
|
||||||
|
- **LLMStreamChunkEvent**: Emitted for each chunk received during streaming LLM responses
|
||||||
|
|
||||||
## Event Handler Structure
|
## Event Handler Structure
|
||||||
|
|
||||||
|
|||||||
@@ -540,6 +540,46 @@ In this section, you'll find detailed examples that help you select, configure,
|
|||||||
</Accordion>
|
</Accordion>
|
||||||
</AccordionGroup>
|
</AccordionGroup>
|
||||||
|
|
||||||
|
## Streaming Responses
|
||||||
|
|
||||||
|
CrewAI supports streaming responses from LLMs, allowing your application to receive and process outputs in real-time as they're generated.
|
||||||
|
|
||||||
|
<Tabs>
|
||||||
|
<Tab title="Basic Setup">
|
||||||
|
Enable streaming by setting the `stream` parameter to `True` when initializing your LLM:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from crewai import LLM
|
||||||
|
|
||||||
|
# Create an LLM with streaming enabled
|
||||||
|
llm = LLM(
|
||||||
|
model="openai/gpt-4o",
|
||||||
|
stream=True # Enable streaming
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
When streaming is enabled, responses are delivered in chunks as they're generated, creating a more responsive user experience.
|
||||||
|
</Tab>
|
||||||
|
|
||||||
|
<Tab title="Event Handling">
|
||||||
|
CrewAI emits events for each chunk received during streaming:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from crewai import LLM
|
||||||
|
from crewai.utilities.events import EventHandler, LLMStreamChunkEvent
|
||||||
|
|
||||||
|
class MyEventHandler(EventHandler):
|
||||||
|
def on_llm_stream_chunk(self, event: LLMStreamChunkEvent):
|
||||||
|
# Process each chunk as it arrives
|
||||||
|
print(f"Received chunk: {event.chunk}")
|
||||||
|
|
||||||
|
# Register the event handler
|
||||||
|
from crewai.utilities.events import crewai_event_bus
|
||||||
|
crewai_event_bus.register_handler(MyEventHandler())
|
||||||
|
```
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
## Structured LLM Calls
|
## Structured LLM Calls
|
||||||
|
|
||||||
CrewAI supports structured responses from LLM calls by allowing you to define a `response_format` using a Pydantic model. This enables the framework to automatically parse and validate the output, making it easier to integrate the response into your application without manual post-processing.
|
CrewAI supports structured responses from LLM calls by allowing you to define a `response_format` using a Pydantic model. This enables the framework to automatically parse and validate the output, making it easier to integrate the response into your application without manual post-processing.
|
||||||
@@ -669,46 +709,4 @@ Learn how to get the most out of your LLM configuration:
|
|||||||
Use larger context models for extensive tasks
|
Use larger context models for extensive tasks
|
||||||
</Tip>
|
</Tip>
|
||||||
|
|
||||||
```python
|
|
||||||
# Large context model
|
|
||||||
llm = LLM(model="openai/gpt-4o") # 128K tokens
|
|
||||||
```
|
```
|
||||||
</Tab>
|
|
||||||
</Tabs>
|
|
||||||
|
|
||||||
## Getting Help
|
|
||||||
|
|
||||||
If you need assistance, these resources are available:
|
|
||||||
|
|
||||||
<CardGroup cols={3}>
|
|
||||||
<Card
|
|
||||||
title="LiteLLM Documentation"
|
|
||||||
href="https://docs.litellm.ai/docs/"
|
|
||||||
icon="book"
|
|
||||||
>
|
|
||||||
Comprehensive documentation for LiteLLM integration and troubleshooting common issues.
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
title="GitHub Issues"
|
|
||||||
href="https://github.com/joaomdmoura/crewAI/issues"
|
|
||||||
icon="bug"
|
|
||||||
>
|
|
||||||
Report bugs, request features, or browse existing issues for solutions.
|
|
||||||
</Card>
|
|
||||||
<Card
|
|
||||||
title="Community Forum"
|
|
||||||
href="https://community.crewai.com"
|
|
||||||
icon="comment-question"
|
|
||||||
>
|
|
||||||
Connect with other CrewAI users, share experiences, and get help from the community.
|
|
||||||
</Card>
|
|
||||||
</CardGroup>
|
|
||||||
|
|
||||||
<Note>
|
|
||||||
Best Practices for API Key Security:
|
|
||||||
- Use environment variables or secure vaults
|
|
||||||
- Never commit keys to version control
|
|
||||||
- Rotate keys regularly
|
|
||||||
- Use separate keys for development and production
|
|
||||||
- Monitor key usage for unusual patterns
|
|
||||||
</Note>
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ icon: handshake
|
|||||||
|
|
||||||
# What is CrewAI?
|
# What is CrewAI?
|
||||||
|
|
||||||
**CrewAI is a cutting-edge framework for orchestrating autonomous AI agents.**
|
**CrewAI is a cutting-edge framework for orchestrating autonomous AI agents.**
|
||||||
|
|
||||||
CrewAI enables you to create AI teams where each agent has specific roles, tools, and goals, working together to accomplish complex tasks.
|
CrewAI enables you to create AI teams where each agent has specific roles, tools, and goals, working together to accomplish complex tasks.
|
||||||
|
|
||||||
@@ -19,7 +19,7 @@ Think of it as assembling your dream team - each member (agent) brings unique sk
|
|||||||
</Note>
|
</Note>
|
||||||
|
|
||||||
<Frame caption="CrewAI Framework Overview">
|
<Frame caption="CrewAI Framework Overview">
|
||||||
<img src="crewAI-mindmap.png" alt="CrewAI Framework Overview" />
|
<img src="asset.png" alt="CrewAI Framework Overview" />
|
||||||
</Frame>
|
</Frame>
|
||||||
|
|
||||||
| Component | Description | Key Features |
|
| Component | Description | Key Features |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "crewai"
|
name = "crewai"
|
||||||
version = "0.102.0"
|
version = "0.105.0"
|
||||||
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
description = "Cutting-edge framework for orchestrating role-playing, autonomous AI agents. By fostering collaborative intelligence, CrewAI empowers agents to work together seamlessly, tackling complex tasks."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
@@ -45,7 +45,7 @@ Documentation = "https://docs.crewai.com"
|
|||||||
Repository = "https://github.com/crewAIInc/crewAI"
|
Repository = "https://github.com/crewAIInc/crewAI"
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
tools = ["crewai-tools>=0.36.0"]
|
tools = ["crewai-tools>=0.37.0"]
|
||||||
embeddings = [
|
embeddings = [
|
||||||
"tiktoken~=0.7.0"
|
"tiktoken~=0.7.0"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ warnings.filterwarnings(
|
|||||||
category=UserWarning,
|
category=UserWarning,
|
||||||
module="pydantic.main",
|
module="pydantic.main",
|
||||||
)
|
)
|
||||||
__version__ = "0.102.0"
|
__version__ = "0.105.0"
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Agent",
|
"Agent",
|
||||||
"Crew",
|
"Crew",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
|||||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crewai[tools]>=0.102.0,<1.0.0"
|
"crewai[tools]>=0.105.0,<1.0.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
|||||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crewai[tools]>=0.102.0,<1.0.0",
|
"crewai[tools]>=0.105.0,<1.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10,<3.13"
|
requires-python = ">=3.10,<3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"crewai[tools]>=0.102.0"
|
"crewai[tools]>=0.105.0"
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.crewai]
|
[tool.crewai]
|
||||||
|
|||||||
@@ -5,7 +5,17 @@ import sys
|
|||||||
import threading
|
import threading
|
||||||
import warnings
|
import warnings
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from typing import Any, Dict, List, Literal, Optional, Type, Union, cast
|
from typing import (
|
||||||
|
Any,
|
||||||
|
Dict,
|
||||||
|
List,
|
||||||
|
Literal,
|
||||||
|
Optional,
|
||||||
|
Type,
|
||||||
|
TypedDict,
|
||||||
|
Union,
|
||||||
|
cast,
|
||||||
|
)
|
||||||
|
|
||||||
from dotenv import load_dotenv
|
from dotenv import load_dotenv
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
@@ -15,6 +25,7 @@ from crewai.utilities.events.llm_events import (
|
|||||||
LLMCallFailedEvent,
|
LLMCallFailedEvent,
|
||||||
LLMCallStartedEvent,
|
LLMCallStartedEvent,
|
||||||
LLMCallType,
|
LLMCallType,
|
||||||
|
LLMStreamChunkEvent,
|
||||||
)
|
)
|
||||||
from crewai.utilities.events.tool_usage_events import ToolExecutionErrorEvent
|
from crewai.utilities.events.tool_usage_events import ToolExecutionErrorEvent
|
||||||
|
|
||||||
@@ -22,8 +33,11 @@ with warnings.catch_warnings():
|
|||||||
warnings.simplefilter("ignore", UserWarning)
|
warnings.simplefilter("ignore", UserWarning)
|
||||||
import litellm
|
import litellm
|
||||||
from litellm import Choices
|
from litellm import Choices
|
||||||
|
from litellm.litellm_core_utils.get_supported_openai_params import (
|
||||||
|
get_supported_openai_params,
|
||||||
|
)
|
||||||
from litellm.types.utils import ModelResponse
|
from litellm.types.utils import ModelResponse
|
||||||
from litellm.utils import get_supported_openai_params, supports_response_schema
|
from litellm.utils import supports_response_schema
|
||||||
|
|
||||||
|
|
||||||
from crewai.llms.base_llm import BaseLLM
|
from crewai.llms.base_llm import BaseLLM
|
||||||
@@ -101,6 +115,19 @@ LLM_CONTEXT_WINDOW_SIZES = {
|
|||||||
"Llama-3.2-11B-Vision-Instruct": 16384,
|
"Llama-3.2-11B-Vision-Instruct": 16384,
|
||||||
"Meta-Llama-3.2-3B-Instruct": 4096,
|
"Meta-Llama-3.2-3B-Instruct": 4096,
|
||||||
"Meta-Llama-3.2-1B-Instruct": 16384,
|
"Meta-Llama-3.2-1B-Instruct": 16384,
|
||||||
|
# mistral
|
||||||
|
"mistral-tiny": 32768,
|
||||||
|
"mistral-small-latest": 32768,
|
||||||
|
"mistral-medium-latest": 32768,
|
||||||
|
"mistral-large-latest": 32768,
|
||||||
|
"mistral-large-2407": 32768,
|
||||||
|
"mistral-large-2402": 32768,
|
||||||
|
"mistral/mistral-tiny": 32768,
|
||||||
|
"mistral/mistral-small-latest": 32768,
|
||||||
|
"mistral/mistral-medium-latest": 32768,
|
||||||
|
"mistral/mistral-large-latest": 32768,
|
||||||
|
"mistral/mistral-large-2407": 32768,
|
||||||
|
"mistral/mistral-large-2402": 32768,
|
||||||
}
|
}
|
||||||
|
|
||||||
DEFAULT_CONTEXT_WINDOW_SIZE = 8192
|
DEFAULT_CONTEXT_WINDOW_SIZE = 8192
|
||||||
@@ -127,7 +154,18 @@ def suppress_warnings():
|
|||||||
sys.stderr = old_stderr
|
sys.stderr = old_stderr
|
||||||
|
|
||||||
|
|
||||||
class LLM(BaseLLM):
|
class Delta(TypedDict):
|
||||||
|
content: Optional[str]
|
||||||
|
role: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class StreamingChoices(TypedDict):
|
||||||
|
delta: Delta
|
||||||
|
index: int
|
||||||
|
finish_reason: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
|
class LLM:
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
model: str,
|
model: str,
|
||||||
@@ -151,6 +189,7 @@ class LLM(BaseLLM):
|
|||||||
api_key: Optional[str] = None,
|
api_key: Optional[str] = None,
|
||||||
callbacks: List[Any] = [],
|
callbacks: List[Any] = [],
|
||||||
reasoning_effort: Optional[Literal["none", "low", "medium", "high"]] = None,
|
reasoning_effort: Optional[Literal["none", "low", "medium", "high"]] = None,
|
||||||
|
stream: bool = False,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
self.model = model
|
self.model = model
|
||||||
@@ -176,6 +215,7 @@ class LLM(BaseLLM):
|
|||||||
self.reasoning_effort = reasoning_effort
|
self.reasoning_effort = reasoning_effort
|
||||||
self.additional_params = kwargs
|
self.additional_params = kwargs
|
||||||
self.is_anthropic = self._is_anthropic_model(model)
|
self.is_anthropic = self._is_anthropic_model(model)
|
||||||
|
self.stream = stream
|
||||||
|
|
||||||
litellm.drop_params = True
|
litellm.drop_params = True
|
||||||
|
|
||||||
@@ -202,6 +242,432 @@ class LLM(BaseLLM):
|
|||||||
ANTHROPIC_PREFIXES = ("anthropic/", "claude-", "claude/")
|
ANTHROPIC_PREFIXES = ("anthropic/", "claude-", "claude/")
|
||||||
return any(prefix in model.lower() for prefix in ANTHROPIC_PREFIXES)
|
return any(prefix in model.lower() for prefix in ANTHROPIC_PREFIXES)
|
||||||
|
|
||||||
|
def _prepare_completion_params(
|
||||||
|
self,
|
||||||
|
messages: Union[str, List[Dict[str, str]]],
|
||||||
|
tools: Optional[List[dict]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Prepare parameters for the completion call.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
messages: Input messages for the LLM
|
||||||
|
tools: Optional list of tool schemas
|
||||||
|
callbacks: Optional list of callback functions
|
||||||
|
available_functions: Optional dict of available functions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: Parameters for the completion call
|
||||||
|
"""
|
||||||
|
# --- 1) Format messages according to provider requirements
|
||||||
|
if isinstance(messages, str):
|
||||||
|
messages = [{"role": "user", "content": messages}]
|
||||||
|
formatted_messages = self._format_messages_for_provider(messages)
|
||||||
|
|
||||||
|
# --- 2) Prepare the parameters for the completion call
|
||||||
|
params = {
|
||||||
|
"model": self.model,
|
||||||
|
"messages": formatted_messages,
|
||||||
|
"timeout": self.timeout,
|
||||||
|
"temperature": self.temperature,
|
||||||
|
"top_p": self.top_p,
|
||||||
|
"n": self.n,
|
||||||
|
"stop": self.stop,
|
||||||
|
"max_tokens": self.max_tokens or self.max_completion_tokens,
|
||||||
|
"presence_penalty": self.presence_penalty,
|
||||||
|
"frequency_penalty": self.frequency_penalty,
|
||||||
|
"logit_bias": self.logit_bias,
|
||||||
|
"response_format": self.response_format,
|
||||||
|
"seed": self.seed,
|
||||||
|
"logprobs": self.logprobs,
|
||||||
|
"top_logprobs": self.top_logprobs,
|
||||||
|
"api_base": self.api_base,
|
||||||
|
"base_url": self.base_url,
|
||||||
|
"api_version": self.api_version,
|
||||||
|
"api_key": self.api_key,
|
||||||
|
"stream": self.stream,
|
||||||
|
"tools": tools,
|
||||||
|
"reasoning_effort": self.reasoning_effort,
|
||||||
|
**self.additional_params,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove None values from params
|
||||||
|
return {k: v for k, v in params.items() if v is not None}
|
||||||
|
|
||||||
|
def _handle_streaming_response(
|
||||||
|
self,
|
||||||
|
params: Dict[str, Any],
|
||||||
|
callbacks: Optional[List[Any]] = None,
|
||||||
|
available_functions: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Handle a streaming response from the LLM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: Parameters for the completion call
|
||||||
|
callbacks: Optional list of callback functions
|
||||||
|
available_functions: Dict of available functions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The complete response text
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
Exception: If no content is received from the streaming response
|
||||||
|
"""
|
||||||
|
# --- 1) Initialize response tracking
|
||||||
|
full_response = ""
|
||||||
|
last_chunk = None
|
||||||
|
chunk_count = 0
|
||||||
|
usage_info = None
|
||||||
|
|
||||||
|
# --- 2) Make sure stream is set to True and include usage metrics
|
||||||
|
params["stream"] = True
|
||||||
|
params["stream_options"] = {"include_usage": True}
|
||||||
|
|
||||||
|
try:
|
||||||
|
# --- 3) Process each chunk in the stream
|
||||||
|
for chunk in litellm.completion(**params):
|
||||||
|
chunk_count += 1
|
||||||
|
last_chunk = chunk
|
||||||
|
|
||||||
|
# Extract content from the chunk
|
||||||
|
chunk_content = None
|
||||||
|
|
||||||
|
# Safely extract content from various chunk formats
|
||||||
|
try:
|
||||||
|
# Try to access choices safely
|
||||||
|
choices = None
|
||||||
|
if isinstance(chunk, dict) and "choices" in chunk:
|
||||||
|
choices = chunk["choices"]
|
||||||
|
elif hasattr(chunk, "choices"):
|
||||||
|
# Check if choices is not a type but an actual attribute with value
|
||||||
|
if not isinstance(getattr(chunk, "choices"), type):
|
||||||
|
choices = getattr(chunk, "choices")
|
||||||
|
|
||||||
|
# Try to extract usage information if available
|
||||||
|
if isinstance(chunk, dict) and "usage" in chunk:
|
||||||
|
usage_info = chunk["usage"]
|
||||||
|
elif hasattr(chunk, "usage"):
|
||||||
|
# Check if usage is not a type but an actual attribute with value
|
||||||
|
if not isinstance(getattr(chunk, "usage"), type):
|
||||||
|
usage_info = getattr(chunk, "usage")
|
||||||
|
|
||||||
|
if choices and len(choices) > 0:
|
||||||
|
choice = choices[0]
|
||||||
|
|
||||||
|
# Handle different delta formats
|
||||||
|
delta = None
|
||||||
|
if isinstance(choice, dict) and "delta" in choice:
|
||||||
|
delta = choice["delta"]
|
||||||
|
elif hasattr(choice, "delta"):
|
||||||
|
delta = getattr(choice, "delta")
|
||||||
|
|
||||||
|
# Extract content from delta
|
||||||
|
if delta:
|
||||||
|
# Handle dict format
|
||||||
|
if isinstance(delta, dict):
|
||||||
|
if "content" in delta and delta["content"] is not None:
|
||||||
|
chunk_content = delta["content"]
|
||||||
|
# Handle object format
|
||||||
|
elif hasattr(delta, "content"):
|
||||||
|
chunk_content = getattr(delta, "content")
|
||||||
|
|
||||||
|
# Handle case where content might be None or empty
|
||||||
|
if chunk_content is None and isinstance(delta, dict):
|
||||||
|
# Some models might send empty content chunks
|
||||||
|
chunk_content = ""
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Error extracting content from chunk: {e}")
|
||||||
|
logging.debug(f"Chunk format: {type(chunk)}, content: {chunk}")
|
||||||
|
|
||||||
|
# Only add non-None content to the response
|
||||||
|
if chunk_content is not None:
|
||||||
|
# Add the chunk content to the full response
|
||||||
|
full_response += chunk_content
|
||||||
|
|
||||||
|
# Emit the chunk event
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=LLMStreamChunkEvent(chunk=chunk_content),
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 4) Fallback to non-streaming if no content received
|
||||||
|
if not full_response.strip() and chunk_count == 0:
|
||||||
|
logging.warning(
|
||||||
|
"No chunks received in streaming response, falling back to non-streaming"
|
||||||
|
)
|
||||||
|
non_streaming_params = params.copy()
|
||||||
|
non_streaming_params["stream"] = False
|
||||||
|
non_streaming_params.pop(
|
||||||
|
"stream_options", None
|
||||||
|
) # Remove stream_options for non-streaming call
|
||||||
|
return self._handle_non_streaming_response(
|
||||||
|
non_streaming_params, callbacks, available_functions
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 5) Handle empty response with chunks
|
||||||
|
if not full_response.strip() and chunk_count > 0:
|
||||||
|
logging.warning(
|
||||||
|
f"Received {chunk_count} chunks but no content was extracted"
|
||||||
|
)
|
||||||
|
if last_chunk is not None:
|
||||||
|
try:
|
||||||
|
# Try to extract content from the last chunk's message
|
||||||
|
choices = None
|
||||||
|
if isinstance(last_chunk, dict) and "choices" in last_chunk:
|
||||||
|
choices = last_chunk["choices"]
|
||||||
|
elif hasattr(last_chunk, "choices"):
|
||||||
|
if not isinstance(getattr(last_chunk, "choices"), type):
|
||||||
|
choices = getattr(last_chunk, "choices")
|
||||||
|
|
||||||
|
if choices and len(choices) > 0:
|
||||||
|
choice = choices[0]
|
||||||
|
|
||||||
|
# Try to get content from message
|
||||||
|
message = None
|
||||||
|
if isinstance(choice, dict) and "message" in choice:
|
||||||
|
message = choice["message"]
|
||||||
|
elif hasattr(choice, "message"):
|
||||||
|
message = getattr(choice, "message")
|
||||||
|
|
||||||
|
if message:
|
||||||
|
content = None
|
||||||
|
if isinstance(message, dict) and "content" in message:
|
||||||
|
content = message["content"]
|
||||||
|
elif hasattr(message, "content"):
|
||||||
|
content = getattr(message, "content")
|
||||||
|
|
||||||
|
if content:
|
||||||
|
full_response = content
|
||||||
|
logging.info(
|
||||||
|
f"Extracted content from last chunk message: {full_response}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Error extracting content from last chunk: {e}")
|
||||||
|
logging.debug(
|
||||||
|
f"Last chunk format: {type(last_chunk)}, content: {last_chunk}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 6) If still empty, raise an error instead of using a default response
|
||||||
|
if not full_response.strip():
|
||||||
|
raise Exception(
|
||||||
|
"No content received from streaming response. Received empty chunks or failed to extract content."
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 7) Check for tool calls in the final response
|
||||||
|
tool_calls = None
|
||||||
|
try:
|
||||||
|
if last_chunk:
|
||||||
|
choices = None
|
||||||
|
if isinstance(last_chunk, dict) and "choices" in last_chunk:
|
||||||
|
choices = last_chunk["choices"]
|
||||||
|
elif hasattr(last_chunk, "choices"):
|
||||||
|
if not isinstance(getattr(last_chunk, "choices"), type):
|
||||||
|
choices = getattr(last_chunk, "choices")
|
||||||
|
|
||||||
|
if choices and len(choices) > 0:
|
||||||
|
choice = choices[0]
|
||||||
|
|
||||||
|
message = None
|
||||||
|
if isinstance(choice, dict) and "message" in choice:
|
||||||
|
message = choice["message"]
|
||||||
|
elif hasattr(choice, "message"):
|
||||||
|
message = getattr(choice, "message")
|
||||||
|
|
||||||
|
if message:
|
||||||
|
if isinstance(message, dict) and "tool_calls" in message:
|
||||||
|
tool_calls = message["tool_calls"]
|
||||||
|
elif hasattr(message, "tool_calls"):
|
||||||
|
tool_calls = getattr(message, "tool_calls")
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Error checking for tool calls: {e}")
|
||||||
|
|
||||||
|
# --- 8) If no tool calls or no available functions, return the text response directly
|
||||||
|
if not tool_calls or not available_functions:
|
||||||
|
# Log token usage if available in streaming mode
|
||||||
|
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
|
||||||
|
# Emit completion event and return response
|
||||||
|
self._handle_emit_call_events(full_response, LLMCallType.LLM_CALL)
|
||||||
|
return full_response
|
||||||
|
|
||||||
|
# --- 9) Handle tool calls if present
|
||||||
|
tool_result = self._handle_tool_call(tool_calls, available_functions)
|
||||||
|
if tool_result is not None:
|
||||||
|
return tool_result
|
||||||
|
|
||||||
|
# --- 10) Log token usage if available in streaming mode
|
||||||
|
self._handle_streaming_callbacks(callbacks, usage_info, last_chunk)
|
||||||
|
|
||||||
|
# --- 11) Emit completion event and return response
|
||||||
|
self._handle_emit_call_events(full_response, LLMCallType.LLM_CALL)
|
||||||
|
return full_response
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Error in streaming response: {str(e)}")
|
||||||
|
if full_response.strip():
|
||||||
|
logging.warning(f"Returning partial response despite error: {str(e)}")
|
||||||
|
self._handle_emit_call_events(full_response, LLMCallType.LLM_CALL)
|
||||||
|
return full_response
|
||||||
|
|
||||||
|
# Emit failed event and re-raise the exception
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=LLMCallFailedEvent(error=str(e)),
|
||||||
|
)
|
||||||
|
raise Exception(f"Failed to get streaming response: {str(e)}")
|
||||||
|
|
||||||
|
def _handle_streaming_callbacks(
|
||||||
|
self,
|
||||||
|
callbacks: Optional[List[Any]],
|
||||||
|
usage_info: Optional[Dict[str, Any]],
|
||||||
|
last_chunk: Optional[Any],
|
||||||
|
) -> None:
|
||||||
|
"""Handle callbacks with usage info for streaming responses.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
callbacks: Optional list of callback functions
|
||||||
|
usage_info: Usage information collected during streaming
|
||||||
|
last_chunk: The last chunk received from the streaming response
|
||||||
|
"""
|
||||||
|
if callbacks and len(callbacks) > 0:
|
||||||
|
for callback in callbacks:
|
||||||
|
if hasattr(callback, "log_success_event"):
|
||||||
|
# Use the usage_info we've been tracking
|
||||||
|
if not usage_info:
|
||||||
|
# Try to get usage from the last chunk if we haven't already
|
||||||
|
try:
|
||||||
|
if last_chunk:
|
||||||
|
if (
|
||||||
|
isinstance(last_chunk, dict)
|
||||||
|
and "usage" in last_chunk
|
||||||
|
):
|
||||||
|
usage_info = last_chunk["usage"]
|
||||||
|
elif hasattr(last_chunk, "usage"):
|
||||||
|
if not isinstance(
|
||||||
|
getattr(last_chunk, "usage"), type
|
||||||
|
):
|
||||||
|
usage_info = getattr(last_chunk, "usage")
|
||||||
|
except Exception as e:
|
||||||
|
logging.debug(f"Error extracting usage info: {e}")
|
||||||
|
|
||||||
|
if usage_info:
|
||||||
|
callback.log_success_event(
|
||||||
|
kwargs={}, # We don't have the original params here
|
||||||
|
response_obj={"usage": usage_info},
|
||||||
|
start_time=0,
|
||||||
|
end_time=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _handle_non_streaming_response(
|
||||||
|
self,
|
||||||
|
params: Dict[str, Any],
|
||||||
|
callbacks: Optional[List[Any]] = None,
|
||||||
|
available_functions: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> str:
|
||||||
|
"""Handle a non-streaming response from the LLM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: Parameters for the completion call
|
||||||
|
callbacks: Optional list of callback functions
|
||||||
|
available_functions: Dict of available functions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: The response text
|
||||||
|
"""
|
||||||
|
# --- 1) Make the completion call
|
||||||
|
response = litellm.completion(**params)
|
||||||
|
|
||||||
|
# --- 2) Extract response message and content
|
||||||
|
response_message = cast(Choices, cast(ModelResponse, response).choices)[
|
||||||
|
0
|
||||||
|
].message
|
||||||
|
text_response = response_message.content or ""
|
||||||
|
|
||||||
|
# --- 3) Handle callbacks with usage info
|
||||||
|
if callbacks and len(callbacks) > 0:
|
||||||
|
for callback in callbacks:
|
||||||
|
if hasattr(callback, "log_success_event"):
|
||||||
|
usage_info = getattr(response, "usage", None)
|
||||||
|
if usage_info:
|
||||||
|
callback.log_success_event(
|
||||||
|
kwargs=params,
|
||||||
|
response_obj={"usage": usage_info},
|
||||||
|
start_time=0,
|
||||||
|
end_time=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
# --- 4) Check for tool calls
|
||||||
|
tool_calls = getattr(response_message, "tool_calls", [])
|
||||||
|
|
||||||
|
# --- 5) If no tool calls or no available functions, return the text response directly
|
||||||
|
if not tool_calls or not available_functions:
|
||||||
|
self._handle_emit_call_events(text_response, LLMCallType.LLM_CALL)
|
||||||
|
return text_response
|
||||||
|
|
||||||
|
# --- 6) Handle tool calls if present
|
||||||
|
tool_result = self._handle_tool_call(tool_calls, available_functions)
|
||||||
|
if tool_result is not None:
|
||||||
|
return tool_result
|
||||||
|
|
||||||
|
# --- 7) If tool call handling didn't return a result, emit completion event and return text response
|
||||||
|
self._handle_emit_call_events(text_response, LLMCallType.LLM_CALL)
|
||||||
|
return text_response
|
||||||
|
|
||||||
|
def _handle_tool_call(
|
||||||
|
self,
|
||||||
|
tool_calls: List[Any],
|
||||||
|
available_functions: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Optional[str]:
|
||||||
|
"""Handle a tool call from the LLM.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
tool_calls: List of tool calls from the LLM
|
||||||
|
available_functions: Dict of available functions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Optional[str]: The result of the tool call, or None if no tool call was made
|
||||||
|
"""
|
||||||
|
# --- 1) Validate tool calls and available functions
|
||||||
|
if not tool_calls or not available_functions:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# --- 2) Extract function name from first tool call
|
||||||
|
tool_call = tool_calls[0]
|
||||||
|
function_name = tool_call.function.name
|
||||||
|
function_args = {} # Initialize to empty dict to avoid unbound variable
|
||||||
|
|
||||||
|
# --- 3) Check if function is available
|
||||||
|
if function_name in available_functions:
|
||||||
|
try:
|
||||||
|
# --- 3.1) Parse function arguments
|
||||||
|
function_args = json.loads(tool_call.function.arguments)
|
||||||
|
fn = available_functions[function_name]
|
||||||
|
|
||||||
|
# --- 3.2) Execute function
|
||||||
|
result = fn(**function_args)
|
||||||
|
|
||||||
|
# --- 3.3) Emit success event
|
||||||
|
self._handle_emit_call_events(result, LLMCallType.TOOL_CALL)
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
# --- 3.4) Handle execution errors
|
||||||
|
fn = available_functions.get(
|
||||||
|
function_name, lambda: None
|
||||||
|
) # Ensure fn is always a callable
|
||||||
|
logging.error(f"Error executing function '{function_name}': {e}")
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=ToolExecutionErrorEvent(
|
||||||
|
tool_name=function_name,
|
||||||
|
tool_args=function_args,
|
||||||
|
tool_class=fn,
|
||||||
|
error=str(e),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
crewai_event_bus.emit(
|
||||||
|
self,
|
||||||
|
event=LLMCallFailedEvent(error=f"Tool execution error: {str(e)}"),
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
def call(
|
def call(
|
||||||
self,
|
self,
|
||||||
messages: Union[str, List[Dict[str, str]]],
|
messages: Union[str, List[Dict[str, str]]],
|
||||||
@@ -231,22 +697,8 @@ class LLM(BaseLLM):
|
|||||||
TypeError: If messages format is invalid
|
TypeError: If messages format is invalid
|
||||||
ValueError: If response format is not supported
|
ValueError: If response format is not supported
|
||||||
LLMContextLengthExceededException: If input exceeds model's context limit
|
LLMContextLengthExceededException: If input exceeds model's context limit
|
||||||
|
|
||||||
Examples:
|
|
||||||
# Example 1: Simple string input
|
|
||||||
>>> response = llm.call("Return the name of a random city.")
|
|
||||||
>>> print(response)
|
|
||||||
"Paris"
|
|
||||||
|
|
||||||
# Example 2: Message list with system and user messages
|
|
||||||
>>> messages = [
|
|
||||||
... {"role": "system", "content": "You are a geography expert"},
|
|
||||||
... {"role": "user", "content": "What is France's capital?"}
|
|
||||||
... ]
|
|
||||||
>>> response = llm.call(messages)
|
|
||||||
>>> print(response)
|
|
||||||
"The capital of France is Paris."
|
|
||||||
"""
|
"""
|
||||||
|
# --- 1) Emit call started event
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
self,
|
self,
|
||||||
event=LLMCallStartedEvent(
|
event=LLMCallStartedEvent(
|
||||||
@@ -256,127 +708,38 @@ class LLM(BaseLLM):
|
|||||||
available_functions=available_functions,
|
available_functions=available_functions,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
# Validate parameters before proceeding with the call.
|
|
||||||
|
# --- 2) Validate parameters before proceeding with the call
|
||||||
self._validate_call_params()
|
self._validate_call_params()
|
||||||
|
|
||||||
|
# --- 3) Convert string messages to proper format if needed
|
||||||
if isinstance(messages, str):
|
if isinstance(messages, str):
|
||||||
messages = [{"role": "user", "content": messages}]
|
messages = [{"role": "user", "content": messages}]
|
||||||
|
|
||||||
# For O1 models, system messages are not supported.
|
# --- 4) Handle O1 model special case (system messages not supported)
|
||||||
# Convert any system messages into assistant messages.
|
|
||||||
if "o1" in self.model.lower():
|
if "o1" in self.model.lower():
|
||||||
for message in messages:
|
for message in messages:
|
||||||
if message.get("role") == "system":
|
if message.get("role") == "system":
|
||||||
message["role"] = "assistant"
|
message["role"] = "assistant"
|
||||||
|
|
||||||
|
# --- 5) Set up callbacks if provided
|
||||||
with suppress_warnings():
|
with suppress_warnings():
|
||||||
if callbacks and len(callbacks) > 0:
|
if callbacks and len(callbacks) > 0:
|
||||||
self.set_callbacks(callbacks)
|
self.set_callbacks(callbacks)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# --- 1) Format messages according to provider requirements
|
# --- 6) Prepare parameters for the completion call
|
||||||
formatted_messages = self._format_messages_for_provider(messages)
|
params = self._prepare_completion_params(messages, tools)
|
||||||
|
|
||||||
# --- 2) Prepare the parameters for the completion call
|
# --- 7) Make the completion call and handle response
|
||||||
params = {
|
if self.stream:
|
||||||
"model": self.model,
|
return self._handle_streaming_response(
|
||||||
"messages": formatted_messages,
|
params, callbacks, available_functions
|
||||||
"timeout": self.timeout,
|
)
|
||||||
"temperature": self.temperature,
|
else:
|
||||||
"top_p": self.top_p,
|
return self._handle_non_streaming_response(
|
||||||
"n": self.n,
|
params, callbacks, available_functions
|
||||||
"stop": self.stop,
|
|
||||||
"max_tokens": self.max_tokens or self.max_completion_tokens,
|
|
||||||
"presence_penalty": self.presence_penalty,
|
|
||||||
"frequency_penalty": self.frequency_penalty,
|
|
||||||
"logit_bias": self.logit_bias,
|
|
||||||
"response_format": self.response_format,
|
|
||||||
"seed": self.seed,
|
|
||||||
"logprobs": self.logprobs,
|
|
||||||
"top_logprobs": self.top_logprobs,
|
|
||||||
"api_base": self.api_base,
|
|
||||||
"base_url": self.base_url,
|
|
||||||
"api_version": self.api_version,
|
|
||||||
"api_key": self.api_key,
|
|
||||||
"stream": False,
|
|
||||||
"tools": tools,
|
|
||||||
"reasoning_effort": self.reasoning_effort,
|
|
||||||
**self.additional_params,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Remove None values from params
|
|
||||||
params = {k: v for k, v in params.items() if v is not None}
|
|
||||||
|
|
||||||
# --- 2) Make the completion call
|
|
||||||
response = litellm.completion(**params)
|
|
||||||
response_message = cast(Choices, cast(ModelResponse, response).choices)[
|
|
||||||
0
|
|
||||||
].message
|
|
||||||
text_response = response_message.content or ""
|
|
||||||
tool_calls = getattr(response_message, "tool_calls", [])
|
|
||||||
|
|
||||||
# --- 3) Handle callbacks with usage info
|
|
||||||
if callbacks and len(callbacks) > 0:
|
|
||||||
for callback in callbacks:
|
|
||||||
if hasattr(callback, "log_success_event"):
|
|
||||||
usage_info = getattr(response, "usage", None)
|
|
||||||
if usage_info:
|
|
||||||
callback.log_success_event(
|
|
||||||
kwargs=params,
|
|
||||||
response_obj={"usage": usage_info},
|
|
||||||
start_time=0,
|
|
||||||
end_time=0,
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- 4) If no tool calls, return the text response
|
|
||||||
if not tool_calls or not available_functions:
|
|
||||||
self._handle_emit_call_events(text_response, LLMCallType.LLM_CALL)
|
|
||||||
return text_response
|
|
||||||
|
|
||||||
# --- 5) Handle the tool call
|
|
||||||
tool_call = tool_calls[0]
|
|
||||||
function_name = tool_call.function.name
|
|
||||||
|
|
||||||
if function_name in available_functions:
|
|
||||||
try:
|
|
||||||
function_args = json.loads(tool_call.function.arguments)
|
|
||||||
except json.JSONDecodeError as e:
|
|
||||||
logging.warning(f"Failed to parse function arguments: {e}")
|
|
||||||
return text_response
|
|
||||||
|
|
||||||
fn = available_functions[function_name]
|
|
||||||
try:
|
|
||||||
# Call the actual tool function
|
|
||||||
result = fn(**function_args)
|
|
||||||
self._handle_emit_call_events(result, LLMCallType.TOOL_CALL)
|
|
||||||
return result
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logging.error(
|
|
||||||
f"Error executing function '{function_name}': {e}"
|
|
||||||
)
|
|
||||||
crewai_event_bus.emit(
|
|
||||||
self,
|
|
||||||
event=ToolExecutionErrorEvent(
|
|
||||||
tool_name=function_name,
|
|
||||||
tool_args=function_args,
|
|
||||||
tool_class=fn,
|
|
||||||
error=str(e),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
crewai_event_bus.emit(
|
|
||||||
self,
|
|
||||||
event=LLMCallFailedEvent(
|
|
||||||
error=f"Tool execution error: {str(e)}"
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return text_response
|
|
||||||
|
|
||||||
else:
|
|
||||||
logging.warning(
|
|
||||||
f"Tool call requested unknown function '{function_name}'"
|
|
||||||
)
|
)
|
||||||
return text_response
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
@@ -427,6 +790,31 @@ class LLM(BaseLLM):
|
|||||||
"Invalid message format. Each message must be a dict with 'role' and 'content' keys"
|
"Invalid message format. Each message must be a dict with 'role' and 'content' keys"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Handle O1 models specially
|
||||||
|
if "o1" in self.model.lower():
|
||||||
|
formatted_messages = []
|
||||||
|
for msg in messages:
|
||||||
|
# Convert system messages to assistant messages
|
||||||
|
if msg["role"] == "system":
|
||||||
|
formatted_messages.append(
|
||||||
|
{"role": "assistant", "content": msg["content"]}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
formatted_messages.append(msg)
|
||||||
|
return formatted_messages
|
||||||
|
|
||||||
|
# Handle Mistral models - they require the last message to have a role of 'user' or 'tool'
|
||||||
|
if "mistral" in self.model.lower():
|
||||||
|
# Check if the last message has a role of 'assistant'
|
||||||
|
if messages and messages[-1]["role"] == "assistant":
|
||||||
|
# Add a dummy user message to ensure the last message has a role of 'user'
|
||||||
|
messages = (
|
||||||
|
messages.copy()
|
||||||
|
) # Create a copy to avoid modifying the original
|
||||||
|
messages.append({"role": "user", "content": "Please continue."})
|
||||||
|
return messages
|
||||||
|
|
||||||
|
# Handle Anthropic models
|
||||||
if not self.is_anthropic:
|
if not self.is_anthropic:
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
@@ -437,7 +825,7 @@ class LLM(BaseLLM):
|
|||||||
|
|
||||||
return messages
|
return messages
|
||||||
|
|
||||||
def _get_custom_llm_provider(self) -> str:
|
def _get_custom_llm_provider(self) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
Derives the custom_llm_provider from the model string.
|
Derives the custom_llm_provider from the model string.
|
||||||
- For example, if the model is "openrouter/deepseek/deepseek-chat", returns "openrouter".
|
- For example, if the model is "openrouter/deepseek/deepseek-chat", returns "openrouter".
|
||||||
@@ -446,7 +834,7 @@ class LLM(BaseLLM):
|
|||||||
"""
|
"""
|
||||||
if "/" in self.model:
|
if "/" in self.model:
|
||||||
return self.model.split("/")[0]
|
return self.model.split("/")[0]
|
||||||
return "openai"
|
return None
|
||||||
|
|
||||||
def _validate_call_params(self) -> None:
|
def _validate_call_params(self) -> None:
|
||||||
"""
|
"""
|
||||||
@@ -469,10 +857,12 @@ class LLM(BaseLLM):
|
|||||||
|
|
||||||
def supports_function_calling(self) -> bool:
|
def supports_function_calling(self) -> bool:
|
||||||
try:
|
try:
|
||||||
params = get_supported_openai_params(model=self.model)
|
provider = self._get_custom_llm_provider()
|
||||||
return params is not None and "tools" in params
|
return litellm.utils.supports_function_calling(
|
||||||
|
self.model, custom_llm_provider=provider
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to get supported params: {str(e)}")
|
logging.error(f"Failed to check function calling support: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def supports_stop_words(self) -> bool:
|
def supports_stop_words(self) -> bool:
|
||||||
|
|||||||
@@ -14,7 +14,12 @@ from .agent_events import (
|
|||||||
AgentExecutionCompletedEvent,
|
AgentExecutionCompletedEvent,
|
||||||
AgentExecutionErrorEvent,
|
AgentExecutionErrorEvent,
|
||||||
)
|
)
|
||||||
from .task_events import TaskStartedEvent, TaskCompletedEvent, TaskFailedEvent, TaskEvaluationEvent
|
from .task_events import (
|
||||||
|
TaskStartedEvent,
|
||||||
|
TaskCompletedEvent,
|
||||||
|
TaskFailedEvent,
|
||||||
|
TaskEvaluationEvent,
|
||||||
|
)
|
||||||
from .flow_events import (
|
from .flow_events import (
|
||||||
FlowCreatedEvent,
|
FlowCreatedEvent,
|
||||||
FlowStartedEvent,
|
FlowStartedEvent,
|
||||||
@@ -34,7 +39,13 @@ from .tool_usage_events import (
|
|||||||
ToolUsageEvent,
|
ToolUsageEvent,
|
||||||
ToolValidateInputErrorEvent,
|
ToolValidateInputErrorEvent,
|
||||||
)
|
)
|
||||||
from .llm_events import LLMCallCompletedEvent, LLMCallFailedEvent, LLMCallStartedEvent
|
from .llm_events import (
|
||||||
|
LLMCallCompletedEvent,
|
||||||
|
LLMCallFailedEvent,
|
||||||
|
LLMCallStartedEvent,
|
||||||
|
LLMCallType,
|
||||||
|
LLMStreamChunkEvent,
|
||||||
|
)
|
||||||
|
|
||||||
# events
|
# events
|
||||||
from .event_listener import EventListener
|
from .event_listener import EventListener
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from io import StringIO
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict
|
||||||
|
|
||||||
from pydantic import Field, PrivateAttr
|
from pydantic import Field, PrivateAttr
|
||||||
@@ -11,6 +12,7 @@ from crewai.utilities.events.llm_events import (
|
|||||||
LLMCallCompletedEvent,
|
LLMCallCompletedEvent,
|
||||||
LLMCallFailedEvent,
|
LLMCallFailedEvent,
|
||||||
LLMCallStartedEvent,
|
LLMCallStartedEvent,
|
||||||
|
LLMStreamChunkEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .agent_events import AgentExecutionCompletedEvent, AgentExecutionStartedEvent
|
from .agent_events import AgentExecutionCompletedEvent, AgentExecutionStartedEvent
|
||||||
@@ -46,6 +48,8 @@ class EventListener(BaseEventListener):
|
|||||||
_telemetry: Telemetry = PrivateAttr(default_factory=lambda: Telemetry())
|
_telemetry: Telemetry = PrivateAttr(default_factory=lambda: Telemetry())
|
||||||
logger = Logger(verbose=True, default_color=EMITTER_COLOR)
|
logger = Logger(verbose=True, default_color=EMITTER_COLOR)
|
||||||
execution_spans: Dict[Task, Any] = Field(default_factory=dict)
|
execution_spans: Dict[Task, Any] = Field(default_factory=dict)
|
||||||
|
next_chunk = 0
|
||||||
|
text_stream = StringIO()
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
@@ -280,9 +284,20 @@ class EventListener(BaseEventListener):
|
|||||||
@crewai_event_bus.on(LLMCallFailedEvent)
|
@crewai_event_bus.on(LLMCallFailedEvent)
|
||||||
def on_llm_call_failed(source, event: LLMCallFailedEvent):
|
def on_llm_call_failed(source, event: LLMCallFailedEvent):
|
||||||
self.logger.log(
|
self.logger.log(
|
||||||
f"❌ LLM Call Failed: '{event.error}'",
|
f"❌ LLM call failed: {event.error}",
|
||||||
event.timestamp,
|
event.timestamp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||||
|
def on_llm_stream_chunk(source, event: LLMStreamChunkEvent):
|
||||||
|
self.text_stream.write(event.chunk)
|
||||||
|
|
||||||
|
self.text_stream.seek(self.next_chunk)
|
||||||
|
|
||||||
|
# Read from the in-memory stream
|
||||||
|
content = self.text_stream.read()
|
||||||
|
print(content, end="", flush=True)
|
||||||
|
self.next_chunk = self.text_stream.tell()
|
||||||
|
|
||||||
|
|
||||||
event_listener = EventListener()
|
event_listener = EventListener()
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ from .flow_events import (
|
|||||||
MethodExecutionFinishedEvent,
|
MethodExecutionFinishedEvent,
|
||||||
MethodExecutionStartedEvent,
|
MethodExecutionStartedEvent,
|
||||||
)
|
)
|
||||||
|
from .llm_events import (
|
||||||
|
LLMCallCompletedEvent,
|
||||||
|
LLMCallFailedEvent,
|
||||||
|
LLMCallStartedEvent,
|
||||||
|
LLMStreamChunkEvent,
|
||||||
|
)
|
||||||
from .task_events import (
|
from .task_events import (
|
||||||
TaskCompletedEvent,
|
TaskCompletedEvent,
|
||||||
TaskFailedEvent,
|
TaskFailedEvent,
|
||||||
@@ -58,4 +64,8 @@ EventTypes = Union[
|
|||||||
ToolUsageFinishedEvent,
|
ToolUsageFinishedEvent,
|
||||||
ToolUsageErrorEvent,
|
ToolUsageErrorEvent,
|
||||||
ToolUsageStartedEvent,
|
ToolUsageStartedEvent,
|
||||||
|
LLMCallStartedEvent,
|
||||||
|
LLMCallCompletedEvent,
|
||||||
|
LLMCallFailedEvent,
|
||||||
|
LLMStreamChunkEvent,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -34,3 +34,10 @@ class LLMCallFailedEvent(CrewEvent):
|
|||||||
|
|
||||||
error: str
|
error: str
|
||||||
type: str = "llm_call_failed"
|
type: str = "llm_call_failed"
|
||||||
|
|
||||||
|
|
||||||
|
class LLMStreamChunkEvent(CrewEvent):
|
||||||
|
"""Event emitted when a streaming chunk is received"""
|
||||||
|
|
||||||
|
type: str = "llm_stream_chunk"
|
||||||
|
chunk: str
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ def create_llm(
|
|||||||
try:
|
try:
|
||||||
# Extract attributes with explicit types
|
# Extract attributes with explicit types
|
||||||
model = (
|
model = (
|
||||||
getattr(llm_value, "model_name", None)
|
getattr(llm_value, "model", None)
|
||||||
or getattr(llm_value, "model", None)
|
or getattr(llm_value, "model_name", None)
|
||||||
or getattr(llm_value, "deployment_name", None)
|
or getattr(llm_value, "deployment_name", None)
|
||||||
or str(llm_value)
|
or str(llm_value)
|
||||||
)
|
)
|
||||||
@@ -77,8 +77,9 @@ def _llm_via_environment_or_fallback() -> Optional[LLM]:
|
|||||||
Helper function: if llm_value is None, we load environment variables or fallback default model.
|
Helper function: if llm_value is None, we load environment variables or fallback default model.
|
||||||
"""
|
"""
|
||||||
model_name = (
|
model_name = (
|
||||||
os.environ.get("OPENAI_MODEL_NAME")
|
os.environ.get("MODEL")
|
||||||
or os.environ.get("MODEL")
|
or os.environ.get("MODEL_NAME")
|
||||||
|
or os.environ.get("OPENAI_MODEL_NAME")
|
||||||
or DEFAULT_LLM_MODEL
|
or DEFAULT_LLM_MODEL
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ from crewai.tools.tool_calling import InstructorToolCalling
|
|||||||
from crewai.tools.tool_usage import ToolUsage
|
from crewai.tools.tool_usage import ToolUsage
|
||||||
from crewai.utilities import RPMController
|
from crewai.utilities import RPMController
|
||||||
from crewai.utilities.events import crewai_event_bus
|
from crewai.utilities.events import crewai_event_bus
|
||||||
|
from crewai.utilities.events.llm_events import LLMStreamChunkEvent
|
||||||
from crewai.utilities.events.tool_usage_events import ToolUsageFinishedEvent
|
from crewai.utilities.events.tool_usage_events import ToolUsageFinishedEvent
|
||||||
|
|
||||||
|
|
||||||
@@ -259,9 +260,7 @@ def test_cache_hitting():
|
|||||||
def handle_tool_end(source, event):
|
def handle_tool_end(source, event):
|
||||||
received_events.append(event)
|
received_events.append(event)
|
||||||
|
|
||||||
with (
|
with (patch.object(CacheHandler, "read") as read,):
|
||||||
patch.object(CacheHandler, "read") as read,
|
|
||||||
):
|
|
||||||
read.return_value = "0"
|
read.return_value = "0"
|
||||||
task = Task(
|
task = Task(
|
||||||
description="What is 2 times 6? Ignore correctness and just return the result of the multiplication tool, you must use the tool.",
|
description="What is 2 times 6? Ignore correctness and just return the result of the multiplication tool, you must use the tool.",
|
||||||
|
|||||||
2571
tests/cassettes/test_crew_kickoff_streaming_usage_metrics.yaml
Normal file
2571
tests/cassettes/test_crew_kickoff_streaming_usage_metrics.yaml
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from concurrent.futures import Future
|
from concurrent.futures import Future
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
@@ -35,6 +36,11 @@ from crewai.utilities.events.crew_events import (
|
|||||||
from crewai.utilities.rpm_controller import RPMController
|
from crewai.utilities.rpm_controller import RPMController
|
||||||
from crewai.utilities.task_output_storage_handler import TaskOutputStorageHandler
|
from crewai.utilities.task_output_storage_handler import TaskOutputStorageHandler
|
||||||
|
|
||||||
|
# Skip streaming tests when running in CI/CD environments
|
||||||
|
skip_streaming_in_ci = pytest.mark.skipif(
|
||||||
|
os.getenv("CI") is not None, reason="Skipping streaming tests in CI/CD environments"
|
||||||
|
)
|
||||||
|
|
||||||
ceo = Agent(
|
ceo = Agent(
|
||||||
role="CEO",
|
role="CEO",
|
||||||
goal="Make sure the writers in your company produce amazing content.",
|
goal="Make sure the writers in your company produce amazing content.",
|
||||||
@@ -948,6 +954,7 @@ def test_api_calls_throttling(capsys):
|
|||||||
moveon.assert_called()
|
moveon.assert_called()
|
||||||
|
|
||||||
|
|
||||||
|
@skip_streaming_in_ci
|
||||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
def test_crew_kickoff_usage_metrics():
|
def test_crew_kickoff_usage_metrics():
|
||||||
inputs = [
|
inputs = [
|
||||||
@@ -960,6 +967,7 @@ def test_crew_kickoff_usage_metrics():
|
|||||||
role="{topic} Researcher",
|
role="{topic} Researcher",
|
||||||
goal="Express hot takes on {topic}.",
|
goal="Express hot takes on {topic}.",
|
||||||
backstory="You have a lot of experience with {topic}.",
|
backstory="You have a lot of experience with {topic}.",
|
||||||
|
llm=LLM(model="gpt-4o"),
|
||||||
)
|
)
|
||||||
|
|
||||||
task = Task(
|
task = Task(
|
||||||
@@ -968,12 +976,50 @@ def test_crew_kickoff_usage_metrics():
|
|||||||
agent=agent,
|
agent=agent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Use real LLM calls instead of mocking
|
||||||
crew = Crew(agents=[agent], tasks=[task])
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
results = crew.kickoff_for_each(inputs=inputs)
|
results = crew.kickoff_for_each(inputs=inputs)
|
||||||
|
|
||||||
assert len(results) == len(inputs)
|
assert len(results) == len(inputs)
|
||||||
for result in results:
|
for result in results:
|
||||||
# Assert that all required keys are in usage_metrics and their values are not None
|
# Assert that all required keys are in usage_metrics and their values are greater than 0
|
||||||
|
assert result.token_usage.total_tokens > 0
|
||||||
|
assert result.token_usage.prompt_tokens > 0
|
||||||
|
assert result.token_usage.completion_tokens > 0
|
||||||
|
assert result.token_usage.successful_requests > 0
|
||||||
|
assert result.token_usage.cached_prompt_tokens == 0
|
||||||
|
|
||||||
|
|
||||||
|
@skip_streaming_in_ci
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_crew_kickoff_streaming_usage_metrics():
|
||||||
|
inputs = [
|
||||||
|
{"topic": "dog"},
|
||||||
|
{"topic": "cat"},
|
||||||
|
{"topic": "apple"},
|
||||||
|
]
|
||||||
|
|
||||||
|
agent = Agent(
|
||||||
|
role="{topic} Researcher",
|
||||||
|
goal="Express hot takes on {topic}.",
|
||||||
|
backstory="You have a lot of experience with {topic}.",
|
||||||
|
llm=LLM(model="gpt-4o", stream=True),
|
||||||
|
max_iter=3,
|
||||||
|
)
|
||||||
|
|
||||||
|
task = Task(
|
||||||
|
description="Give me an analysis around {topic}.",
|
||||||
|
expected_output="1 bullet point about {topic} that's under 15 words.",
|
||||||
|
agent=agent,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Use real LLM calls instead of mocking
|
||||||
|
crew = Crew(agents=[agent], tasks=[task])
|
||||||
|
results = crew.kickoff_for_each(inputs=inputs)
|
||||||
|
|
||||||
|
assert len(results) == len(inputs)
|
||||||
|
for result in results:
|
||||||
|
# Assert that all required keys are in usage_metrics and their values are greater than 0
|
||||||
assert result.token_usage.total_tokens > 0
|
assert result.token_usage.total_tokens > 0
|
||||||
assert result.token_usage.prompt_tokens > 0
|
assert result.token_usage.prompt_tokens > 0
|
||||||
assert result.token_usage.completion_tokens > 0
|
assert result.token_usage.completion_tokens > 0
|
||||||
@@ -3973,3 +4019,5 @@ def test_crew_with_knowledge_sources_works_with_copy():
|
|||||||
assert crew_copy.knowledge_sources == crew.knowledge_sources
|
assert crew_copy.knowledge_sources == crew.knowledge_sources
|
||||||
assert len(crew_copy.agents) == len(crew.agents)
|
assert len(crew_copy.agents) == len(crew.agents)
|
||||||
assert len(crew_copy.tasks) == len(crew.tasks)
|
assert len(crew_copy.tasks) == len(crew.tasks)
|
||||||
|
|
||||||
|
assert len(crew_copy.tasks) == len(crew.tasks)
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ def test_get_custom_llm_provider_gemini():
|
|||||||
|
|
||||||
def test_get_custom_llm_provider_openai():
|
def test_get_custom_llm_provider_openai():
|
||||||
llm = LLM(model="gpt-4")
|
llm = LLM(model="gpt-4")
|
||||||
assert llm._get_custom_llm_provider() == "openai"
|
assert llm._get_custom_llm_provider() == None
|
||||||
|
|
||||||
|
|
||||||
def test_validate_call_params_supported():
|
def test_validate_call_params_supported():
|
||||||
@@ -285,6 +285,7 @@ def test_o3_mini_reasoning_effort_medium():
|
|||||||
assert isinstance(result, str)
|
assert isinstance(result, str)
|
||||||
assert "Paris" in result
|
assert "Paris" in result
|
||||||
|
|
||||||
|
|
||||||
def test_context_window_validation():
|
def test_context_window_validation():
|
||||||
"""Test that context window validation works correctly."""
|
"""Test that context window validation works correctly."""
|
||||||
# Test valid window size
|
# Test valid window size
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"messages": [{"role": "user", "content": "Tell me a short joke"}], "model":
|
||||||
|
"gpt-3.5-turbo", "stop": [], "stream": true}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '121'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
cookie:
|
||||||
|
- _cfuvid=IY8ppO70AMHr2skDSUsGh71zqHHdCQCZ3OvkPi26NBc-1740424913267-0.0.1.1-604800000
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.65.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.65.1
|
||||||
|
x-stainless-raw-response:
|
||||||
|
- 'true'
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600.0'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.12.8
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: 'data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"role":"assistant","content":"","refusal":null},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"Why"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
couldn"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"''t"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
the"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
bicycle"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
stand"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
up"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
by"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
itself"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"?"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
Because"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
it"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
was"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"
|
||||||
|
two"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"-t"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"ired"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{"content":"!"},"logprobs":null,"finish_reason":null}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: {"id":"chatcmpl-B74aE2TDl9ZbKx2fXoVatoMDnErNm","object":"chat.completion.chunk","created":1741025614,"model":"gpt-3.5-turbo-0125","service_tier":"default","system_fingerprint":null,"choices":[{"index":0,"delta":{},"logprobs":null,"finish_reason":"stop"}]}
|
||||||
|
|
||||||
|
|
||||||
|
data: [DONE]
|
||||||
|
|
||||||
|
|
||||||
|
'
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 91ab1bcbad95bcda-ATL
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Type:
|
||||||
|
- text/event-stream; charset=utf-8
|
||||||
|
Date:
|
||||||
|
- Mon, 03 Mar 2025 18:13:34 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=Jydtg8l0yjWRI2vKmejdq.C1W.sasIwEbTrV2rUt6V0-1741025614-1.0.1.1-Af3gmq.j2ecn9QEa3aCVY09QU4VqoW2GTk9AjvzPA.jyAZlwhJd4paniSt3kSusH0tryW03iC8uaX826hb2xzapgcfSm6Jdh_eWh_BMCh_8;
|
||||||
|
path=/; expires=Mon, 03-Mar-25 18:43:34 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=5wzaJSCvT1p1Eazad55wDvp1JsgxrlghhmmU9tx0fMs-1741025614868-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:
|
||||||
|
- '127'
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
strict-transport-security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '10000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '50000000'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '9999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '49999978'
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 6ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_2a2a04977ace88fdd64cf570f80c0202
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
interactions:
|
||||||
|
- request:
|
||||||
|
body: '{"messages": [{"role": "user", "content": "Tell me a short joke"}], "model":
|
||||||
|
"gpt-4o", "stop": [], "stream": false}'
|
||||||
|
headers:
|
||||||
|
accept:
|
||||||
|
- application/json
|
||||||
|
accept-encoding:
|
||||||
|
- gzip, deflate, zstd
|
||||||
|
connection:
|
||||||
|
- keep-alive
|
||||||
|
content-length:
|
||||||
|
- '115'
|
||||||
|
content-type:
|
||||||
|
- application/json
|
||||||
|
host:
|
||||||
|
- api.openai.com
|
||||||
|
user-agent:
|
||||||
|
- OpenAI/Python 1.65.1
|
||||||
|
x-stainless-arch:
|
||||||
|
- arm64
|
||||||
|
x-stainless-async:
|
||||||
|
- 'false'
|
||||||
|
x-stainless-lang:
|
||||||
|
- python
|
||||||
|
x-stainless-os:
|
||||||
|
- MacOS
|
||||||
|
x-stainless-package-version:
|
||||||
|
- 1.65.1
|
||||||
|
x-stainless-raw-response:
|
||||||
|
- 'true'
|
||||||
|
x-stainless-read-timeout:
|
||||||
|
- '600.0'
|
||||||
|
x-stainless-retry-count:
|
||||||
|
- '0'
|
||||||
|
x-stainless-runtime:
|
||||||
|
- CPython
|
||||||
|
x-stainless-runtime-version:
|
||||||
|
- 3.12.8
|
||||||
|
method: POST
|
||||||
|
uri: https://api.openai.com/v1/chat/completions
|
||||||
|
response:
|
||||||
|
body:
|
||||||
|
string: !!binary |
|
||||||
|
H4sIAAAAAAAAAwAAAP//jFJBbtswELzrFVteerEKSZbrxpcCDuBTUfSUtigCgSZXEhuKJLirNEbg
|
||||||
|
vxeSHMtBXSAXHmZ2BjPLfU4AhNFiA0K1klUXbLpde/X1tvtW/tnfrW6//Lzb7UraLn8s2+xpJxaD
|
||||||
|
wu9/o+IX1Qflu2CRjXcTrSJKxsE1X5d5kRWrdT4SnddoB1kTOC19WmRFmWaf0uzjSdh6o5DEBn4l
|
||||||
|
AADP4ztEdBqfxAayxQvSIZFsUGzOQwAiejsgQhIZYulYLGZSecfoxtTf2wNo794zkDLo2BATcOyJ
|
||||||
|
QbLv6DNsUcmeELjFA3TyAaEPgI8YD9wa17y7NI5Y9ySHXq639oQfz0mtb0L0ezrxZ7w2zlBbRZTk
|
||||||
|
3ZCK2AcxsscE4H7cSP+qpAjRd4Er9g/oBsO8mOzE/AVXSPYs7YwX5eKKW6WRpbF0sVGhpGpRz8p5
|
||||||
|
/bLXxl8QyUXnf8Nc8556G9e8xX4mlMLAqKsQURv1uvA8FnE40P+NnXc8BhaE8dEorNhgHP5BYy17
|
||||||
|
O92OoAMxdlVtXIMxRDMdUB2qWt3UuV5ny5VIjslfAAAA//8DADx20t9JAwAA
|
||||||
|
headers:
|
||||||
|
CF-RAY:
|
||||||
|
- 91bbfc033e461d6e-ATL
|
||||||
|
Connection:
|
||||||
|
- keep-alive
|
||||||
|
Content-Encoding:
|
||||||
|
- gzip
|
||||||
|
Content-Type:
|
||||||
|
- application/json
|
||||||
|
Date:
|
||||||
|
- Wed, 05 Mar 2025 19:22:51 GMT
|
||||||
|
Server:
|
||||||
|
- cloudflare
|
||||||
|
Set-Cookie:
|
||||||
|
- __cf_bm=LecfSlhN6VGr4kTlMiMCqRPInNb1m8zOikTZxtsE_WM-1741202571-1.0.1.1-T8nh2g1PcqyLIV97_HH9Q_nSUyCtaiFAOzvMxlswn6XjJCcSLJhi_fmkbylwppwoRPTxgs4S6VsVH0mp4ZcDTABBbtemKj7vS8QRDpRrmsU;
|
||||||
|
path=/; expires=Wed, 05-Mar-25 19:52:51 GMT; domain=.api.openai.com; HttpOnly;
|
||||||
|
Secure; SameSite=None
|
||||||
|
- _cfuvid=wyMrJP5k5bgWyD8rsK4JPvAJ78JWrsrT0lyV9DP4WZM-1741202571727-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:
|
||||||
|
- '416'
|
||||||
|
openai-version:
|
||||||
|
- '2020-10-01'
|
||||||
|
strict-transport-security:
|
||||||
|
- max-age=31536000; includeSubDomains; preload
|
||||||
|
x-ratelimit-limit-requests:
|
||||||
|
- '10000'
|
||||||
|
x-ratelimit-limit-tokens:
|
||||||
|
- '30000000'
|
||||||
|
x-ratelimit-remaining-requests:
|
||||||
|
- '9999'
|
||||||
|
x-ratelimit-remaining-tokens:
|
||||||
|
- '29999978'
|
||||||
|
x-ratelimit-reset-requests:
|
||||||
|
- 6ms
|
||||||
|
x-ratelimit-reset-tokens:
|
||||||
|
- 0s
|
||||||
|
x-request-id:
|
||||||
|
- req_f42504d00bda0a492dced0ba3cf302d8
|
||||||
|
status:
|
||||||
|
code: 200
|
||||||
|
message: OK
|
||||||
|
version: 1
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import os
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from unittest.mock import Mock, patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
@@ -38,6 +39,7 @@ from crewai.utilities.events.llm_events import (
|
|||||||
LLMCallFailedEvent,
|
LLMCallFailedEvent,
|
||||||
LLMCallStartedEvent,
|
LLMCallStartedEvent,
|
||||||
LLMCallType,
|
LLMCallType,
|
||||||
|
LLMStreamChunkEvent,
|
||||||
)
|
)
|
||||||
from crewai.utilities.events.task_events import (
|
from crewai.utilities.events.task_events import (
|
||||||
TaskCompletedEvent,
|
TaskCompletedEvent,
|
||||||
@@ -48,6 +50,11 @@ from crewai.utilities.events.tool_usage_events import (
|
|||||||
ToolUsageErrorEvent,
|
ToolUsageErrorEvent,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Skip streaming tests when running in CI/CD environments
|
||||||
|
skip_streaming_in_ci = pytest.mark.skipif(
|
||||||
|
os.getenv("CI") is not None, reason="Skipping streaming tests in CI/CD environments"
|
||||||
|
)
|
||||||
|
|
||||||
base_agent = Agent(
|
base_agent = Agent(
|
||||||
role="base_agent",
|
role="base_agent",
|
||||||
llm="gpt-4o-mini",
|
llm="gpt-4o-mini",
|
||||||
@@ -615,3 +622,152 @@ def test_llm_emits_call_failed_event():
|
|||||||
assert len(received_events) == 1
|
assert len(received_events) == 1
|
||||||
assert received_events[0].type == "llm_call_failed"
|
assert received_events[0].type == "llm_call_failed"
|
||||||
assert received_events[0].error == error_message
|
assert received_events[0].error == error_message
|
||||||
|
|
||||||
|
|
||||||
|
@skip_streaming_in_ci
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_llm_emits_stream_chunk_events():
|
||||||
|
"""Test that LLM emits stream chunk events when streaming is enabled."""
|
||||||
|
received_chunks = []
|
||||||
|
|
||||||
|
with crewai_event_bus.scoped_handlers():
|
||||||
|
|
||||||
|
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||||
|
def handle_stream_chunk(source, event):
|
||||||
|
received_chunks.append(event.chunk)
|
||||||
|
|
||||||
|
# Create an LLM with streaming enabled
|
||||||
|
llm = LLM(model="gpt-4o", stream=True)
|
||||||
|
|
||||||
|
# Call the LLM with a simple message
|
||||||
|
response = llm.call("Tell me a short joke")
|
||||||
|
|
||||||
|
# Verify that we received chunks
|
||||||
|
assert len(received_chunks) > 0
|
||||||
|
|
||||||
|
# Verify that concatenating all chunks equals the final response
|
||||||
|
assert "".join(received_chunks) == response
|
||||||
|
|
||||||
|
|
||||||
|
@skip_streaming_in_ci
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_llm_no_stream_chunks_when_streaming_disabled():
|
||||||
|
"""Test that LLM doesn't emit stream chunk events when streaming is disabled."""
|
||||||
|
received_chunks = []
|
||||||
|
|
||||||
|
with crewai_event_bus.scoped_handlers():
|
||||||
|
|
||||||
|
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||||
|
def handle_stream_chunk(source, event):
|
||||||
|
received_chunks.append(event.chunk)
|
||||||
|
|
||||||
|
# Create an LLM with streaming disabled
|
||||||
|
llm = LLM(model="gpt-4o", stream=False)
|
||||||
|
|
||||||
|
# Call the LLM with a simple message
|
||||||
|
response = llm.call("Tell me a short joke")
|
||||||
|
|
||||||
|
# Verify that we didn't receive any chunks
|
||||||
|
assert len(received_chunks) == 0
|
||||||
|
|
||||||
|
# Verify we got a response
|
||||||
|
assert response and isinstance(response, str)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_streaming_fallback_to_non_streaming():
|
||||||
|
"""Test that streaming falls back to non-streaming when there's an error."""
|
||||||
|
received_chunks = []
|
||||||
|
fallback_called = False
|
||||||
|
|
||||||
|
with crewai_event_bus.scoped_handlers():
|
||||||
|
|
||||||
|
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||||
|
def handle_stream_chunk(source, event):
|
||||||
|
received_chunks.append(event.chunk)
|
||||||
|
|
||||||
|
# Create an LLM with streaming enabled
|
||||||
|
llm = LLM(model="gpt-4o", stream=True)
|
||||||
|
|
||||||
|
# Store original methods
|
||||||
|
original_call = llm.call
|
||||||
|
|
||||||
|
# Create a mock call method that handles the streaming error
|
||||||
|
def mock_call(messages, tools=None, callbacks=None, available_functions=None):
|
||||||
|
nonlocal fallback_called
|
||||||
|
# Emit a couple of chunks to simulate partial streaming
|
||||||
|
crewai_event_bus.emit(llm, event=LLMStreamChunkEvent(chunk="Test chunk 1"))
|
||||||
|
crewai_event_bus.emit(llm, event=LLMStreamChunkEvent(chunk="Test chunk 2"))
|
||||||
|
|
||||||
|
# Mark that fallback would be called
|
||||||
|
fallback_called = True
|
||||||
|
|
||||||
|
# Return a response as if fallback succeeded
|
||||||
|
return "Fallback response after streaming error"
|
||||||
|
|
||||||
|
# Replace the call method with our mock
|
||||||
|
llm.call = mock_call
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Call the LLM
|
||||||
|
response = llm.call("Tell me a short joke")
|
||||||
|
|
||||||
|
# Verify that we received some chunks
|
||||||
|
assert len(received_chunks) == 2
|
||||||
|
assert received_chunks[0] == "Test chunk 1"
|
||||||
|
assert received_chunks[1] == "Test chunk 2"
|
||||||
|
|
||||||
|
# Verify fallback was triggered
|
||||||
|
assert fallback_called
|
||||||
|
|
||||||
|
# Verify we got the fallback response
|
||||||
|
assert response == "Fallback response after streaming error"
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore the original method
|
||||||
|
llm.call = original_call
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
|
def test_streaming_empty_response_handling():
|
||||||
|
"""Test that streaming handles empty responses correctly."""
|
||||||
|
received_chunks = []
|
||||||
|
|
||||||
|
with crewai_event_bus.scoped_handlers():
|
||||||
|
|
||||||
|
@crewai_event_bus.on(LLMStreamChunkEvent)
|
||||||
|
def handle_stream_chunk(source, event):
|
||||||
|
received_chunks.append(event.chunk)
|
||||||
|
|
||||||
|
# Create an LLM with streaming enabled
|
||||||
|
llm = LLM(model="gpt-3.5-turbo", stream=True)
|
||||||
|
|
||||||
|
# Store original methods
|
||||||
|
original_call = llm.call
|
||||||
|
|
||||||
|
# Create a mock call method that simulates empty chunks
|
||||||
|
def mock_call(messages, tools=None, callbacks=None, available_functions=None):
|
||||||
|
# Emit a few empty chunks
|
||||||
|
for _ in range(3):
|
||||||
|
crewai_event_bus.emit(llm, event=LLMStreamChunkEvent(chunk=""))
|
||||||
|
|
||||||
|
# Return the default message for empty responses
|
||||||
|
return "I apologize, but I couldn't generate a proper response. Please try again or rephrase your request."
|
||||||
|
|
||||||
|
# Replace the call method with our mock
|
||||||
|
llm.call = mock_call
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Call the LLM - this should handle empty response
|
||||||
|
response = llm.call("Tell me a short joke")
|
||||||
|
|
||||||
|
# Verify that we received empty chunks
|
||||||
|
assert len(received_chunks) == 3
|
||||||
|
assert all(chunk == "" for chunk in received_chunks)
|
||||||
|
|
||||||
|
# Verify the response is the default message for empty responses
|
||||||
|
assert "I apologize" in response and "couldn't generate" in response
|
||||||
|
|
||||||
|
finally:
|
||||||
|
# Restore the original method
|
||||||
|
llm.call = original_call
|
||||||
|
|||||||
10
uv.lock
generated
10
uv.lock
generated
@@ -619,7 +619,7 @@ wheels = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crewai"
|
name = "crewai"
|
||||||
version = "0.102.0"
|
version = "0.105.0"
|
||||||
source = { editable = "." }
|
source = { editable = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "appdirs" },
|
{ name = "appdirs" },
|
||||||
@@ -703,7 +703,7 @@ requires-dist = [
|
|||||||
{ name = "blinker", specifier = ">=1.9.0" },
|
{ name = "blinker", specifier = ">=1.9.0" },
|
||||||
{ name = "chromadb", specifier = ">=0.5.23" },
|
{ name = "chromadb", specifier = ">=0.5.23" },
|
||||||
{ name = "click", specifier = ">=8.1.7" },
|
{ name = "click", specifier = ">=8.1.7" },
|
||||||
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = ">=0.36.0" },
|
{ name = "crewai-tools", marker = "extra == 'tools'", specifier = ">=0.37.0" },
|
||||||
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" },
|
{ name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" },
|
||||||
{ name = "fastembed", marker = "extra == 'fastembed'", specifier = ">=0.4.1" },
|
{ name = "fastembed", marker = "extra == 'fastembed'", specifier = ">=0.4.1" },
|
||||||
{ name = "instructor", specifier = ">=1.3.3" },
|
{ name = "instructor", specifier = ">=1.3.3" },
|
||||||
@@ -752,7 +752,7 @@ dev = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crewai-tools"
|
name = "crewai-tools"
|
||||||
version = "0.36.0"
|
version = "0.37.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "chromadb" },
|
{ name = "chromadb" },
|
||||||
@@ -767,9 +767,9 @@ dependencies = [
|
|||||||
{ name = "pytube" },
|
{ name = "pytube" },
|
||||||
{ name = "requests" },
|
{ name = "requests" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/4d/e1/d65778cf4aea106f3f60a4208521f04bc7f1d26be4e34eeb63cae6297d50/crewai_tools-0.36.0.tar.gz", hash = "sha256:761b396ee6a4019a988720dd6a14e1409f5de9d0cdc2a8662b487d87efb1a6bf", size = 900178 }
|
sdist = { url = "https://files.pythonhosted.org/packages/ef/a9/813ef7b721d11ac962c2a3cf4c98196d3ca8bca5bb0fa5e01da0af51ac23/crewai_tools-0.37.0.tar.gz", hash = "sha256:23c8428761809e30d164be32c2a02850c4648e4371e9934eb58842590bca9659", size = 722104 }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/bd/b6/533632a6c2a2e623fc4a1677458aff3539413a196fb220a7fece4ead3f71/crewai_tools-0.36.0-py3-none-any.whl", hash = "sha256:dbd0d95a080acfb281e105f4376e1e98576dae6d53d94f7b883c57af893668b3", size = 545937 },
|
{ url = "https://files.pythonhosted.org/packages/f4/b3/6bf9b066f628875c383689ab72d21968e1108ebece887491dbf051ee39c5/crewai_tools-0.37.0-py3-none-any.whl", hash = "sha256:df5c9efade5c1f4fcfdf6ac8af13c422be7127a3083a5cda75d8f314c652bb10", size = 548490 },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
Reference in New Issue
Block a user