mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-03-05 11:28:14 +00:00
Compare commits
15 Commits
joaomdmour
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cebc52694e | ||
|
|
53df41989a | ||
|
|
ea70976a5d | ||
|
|
3cc6516ae5 | ||
|
|
ad82e52d39 | ||
|
|
9336702ebc | ||
|
|
030f6d6c43 | ||
|
|
95d51db29f | ||
|
|
a8f51419f6 | ||
|
|
e7f17d2284 | ||
|
|
5d0811258f | ||
|
|
7972192d55 | ||
|
|
b3f8a42321 | ||
|
|
21224f2bc5 | ||
|
|
b76022c1e7 |
@@ -12,6 +12,7 @@ from dotenv import load_dotenv
|
||||
import pytest
|
||||
from vcr.request import Request # type: ignore[import-untyped]
|
||||
|
||||
|
||||
try:
|
||||
import vcr.stubs.httpx_stubs as httpx_stubs # type: ignore[import-untyped]
|
||||
except ModuleNotFoundError:
|
||||
|
||||
1372
docs/docs.json
1372
docs/docs.json
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,38 @@ description: "Product updates, improvements, and bug fixes for CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="Mar 04, 2026">
|
||||
## v1.10.1
|
||||
|
||||
[View release on GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1)
|
||||
|
||||
## What's Changed
|
||||
|
||||
### Features
|
||||
- Upgrade Gemini GenAI
|
||||
|
||||
### Bug Fixes
|
||||
- Adjust executor listener value to avoid recursion
|
||||
- Group parallel function response parts in a single Content object in Gemini
|
||||
- Surface thought output from thinking models in Gemini
|
||||
- Load MCP and platform tools when agent tools are None
|
||||
- Support Jupyter environments with running event loops in A2A
|
||||
- Use anonymous ID for ephemeral traces
|
||||
- Conditionally pass plus header
|
||||
- Skip signal handler registration in non-main threads for telemetry
|
||||
- Inject tool errors as observations and resolve name collisions
|
||||
- Upgrade pypdf from 4.x to 6.7.4 to resolve Dependabot alerts
|
||||
- Resolve critical and high Dependabot security alerts
|
||||
|
||||
### Documentation
|
||||
- Sync Composio tool documentation across locales
|
||||
|
||||
## Contributors
|
||||
|
||||
@giulio-leone, @greysonlalonde, @haxzie, @joaomdmoura, @lorenzejay, @mattatcha, @mplachta, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="Feb 27, 2026">
|
||||
## v1.10.1a1
|
||||
|
||||
|
||||
518
docs/en/guides/migration/migrating-from-langgraph.mdx
Normal file
518
docs/en/guides/migration/migrating-from-langgraph.mdx
Normal file
@@ -0,0 +1,518 @@
|
||||
---
|
||||
title: "Moving from LangGraph to CrewAI: A Practical Guide for Engineers"
|
||||
description: If you already have built with LangGraph, learn how to quickly port your projects to CrewAI
|
||||
icon: switch
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
You've built agents with LangGraph. You've wrestled with `StateGraph`, wired up conditional edges, and debugged state dictionaries at 2 AM. It works — but somewhere along the way, you started wondering if there's a better path to production.
|
||||
|
||||
There is. **CrewAI Flows** gives you the same power — event-driven orchestration, conditional routing, shared state — with dramatically less boilerplate and a mental model that maps cleanly to how you actually think about multi-step AI workflows.
|
||||
|
||||
This article walks through the core concepts side by side, shows real code comparisons, and demonstrates why CrewAI Flows is the framework you'll want to reach for next.
|
||||
|
||||
---
|
||||
|
||||
## The Mental Model Shift
|
||||
|
||||
LangGraph asks you to think in **graphs**: nodes, edges, and state dictionaries. Every workflow is a directed graph where you explicitly wire transitions between computation steps. It's powerful, but the abstraction carries overhead — especially when your workflow is fundamentally sequential with a few decision points.
|
||||
|
||||
CrewAI Flows asks you to think in **events**: methods that start things, methods that listen for results, and methods that route execution. The topology of your workflow emerges from decorator annotations rather than explicit graph construction. This isn't just syntactic sugar — it changes how you design, read, and maintain your pipelines.
|
||||
|
||||
Here's the core mapping:
|
||||
|
||||
| LangGraph Concept | CrewAI Flows Equivalent |
|
||||
| --- | --- |
|
||||
| `StateGraph` class | `Flow` class |
|
||||
| `add_node()` | Methods decorated with `@start`, `@listen` |
|
||||
| `add_edge()` / `add_conditional_edges()` | `@listen()` / `@router()` decorators |
|
||||
| `TypedDict` state | Pydantic `BaseModel` state |
|
||||
| `START` / `END` constants | `@start()` decorator / natural method return |
|
||||
| `graph.compile()` | `flow.kickoff()` |
|
||||
| Checkpointer / persistence | Built-in memory (LanceDB-backed) |
|
||||
|
||||
Let's see what this looks like in practice.
|
||||
|
||||
---
|
||||
|
||||
## Demo 1: A Simple Sequential Pipeline
|
||||
|
||||
Imagine you're building a pipeline that takes a topic, researches it, writes a summary, and formats the output. Here's how each framework handles it.
|
||||
|
||||
### LangGraph Approach
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ResearchState(TypedDict):
|
||||
topic: str
|
||||
raw_research: str
|
||||
summary: str
|
||||
formatted_output: str
|
||||
|
||||
def research_topic(state: ResearchState) -> dict:
|
||||
# Call an LLM or search API
|
||||
result = llm.invoke(f"Research the topic: {state['topic']}")
|
||||
return {"raw_research": result}
|
||||
|
||||
def write_summary(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Summarize this research:\n{state['raw_research']}"
|
||||
)
|
||||
return {"summary": result}
|
||||
|
||||
def format_output(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Format this summary as a polished article section:\n{state['summary']}"
|
||||
)
|
||||
return {"formatted_output": result}
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ResearchState)
|
||||
graph.add_node("research", research_topic)
|
||||
graph.add_node("summarize", write_summary)
|
||||
graph.add_node("format", format_output)
|
||||
|
||||
graph.add_edge(START, "research")
|
||||
graph.add_edge("research", "summarize")
|
||||
graph.add_edge("summarize", "format")
|
||||
graph.add_edge("format", END)
|
||||
|
||||
# Compile and run
|
||||
app = graph.compile()
|
||||
result = app.invoke({"topic": "quantum computing advances in 2026"})
|
||||
print(result["formatted_output"])
|
||||
```
|
||||
|
||||
You define functions, register them as nodes, and manually wire every transition. For a simple sequence like this, there's a lot of ceremony.
|
||||
|
||||
### CrewAI Flows Approach
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent, Crew, Process, Task
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ResearchState(BaseModel):
|
||||
topic: str = ""
|
||||
raw_research: str = ""
|
||||
summary: str = ""
|
||||
formatted_output: str = ""
|
||||
|
||||
class ResearchFlow(Flow[ResearchState]):
|
||||
@start()
|
||||
def research_topic(self):
|
||||
# Option 1: Direct LLM call
|
||||
result = llm.call(f"Research the topic: {self.state.topic}")
|
||||
self.state.raw_research = result
|
||||
return result
|
||||
|
||||
@listen(research_topic)
|
||||
def write_summary(self, research_output):
|
||||
# Option 2: A single agent
|
||||
summarizer = Agent(
|
||||
role="Research Summarizer",
|
||||
goal="Produce concise, accurate summaries of research content",
|
||||
backstory="You are an expert at distilling complex research into clear, "
|
||||
"digestible summaries.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
result = summarizer.kickoff(
|
||||
f"Summarize this research:\n{self.state.raw_research}"
|
||||
)
|
||||
self.state.summary = str(result)
|
||||
return self.state.summary
|
||||
|
||||
@listen(write_summary)
|
||||
def format_output(self, summary_output):
|
||||
# Option 3: a complete crew (with one or more agents)
|
||||
formatter = Agent(
|
||||
role="Content Formatter",
|
||||
goal="Transform research summaries into polished, publication-ready article sections",
|
||||
backstory="You are a skilled editor with expertise in structuring and "
|
||||
"presenting technical content for a general audience.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
format_task = Task(
|
||||
description=f"Format this summary as a polished article section:\n{self.state.summary}",
|
||||
expected_output="A well-structured, polished article section ready for publication.",
|
||||
agent=formatter,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[formatter],
|
||||
tasks=[format_task],
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
result = crew.kickoff()
|
||||
self.state.formatted_output = str(result)
|
||||
return self.state.formatted_output
|
||||
|
||||
# Run the flow
|
||||
flow = ResearchFlow()
|
||||
flow.state.topic = "quantum computing advances in 2026"
|
||||
result = flow.kickoff()
|
||||
print(flow.state.formatted_output)
|
||||
|
||||
```
|
||||
|
||||
Notice what's different: no graph construction, no edge wiring, no compile step. The execution order is declared right where the logic lives. `@start()` marks the entry point, and `@listen(method_name)` chains steps together. The state is a proper Pydantic model with type safety, validation, and IDE auto-completion.
|
||||
|
||||
---
|
||||
|
||||
## Demo 2: Conditional Routing
|
||||
|
||||
This is where things get interesting. Say you're building a content pipeline that routes to different processing paths based on the type of content detected.
|
||||
|
||||
### LangGraph Approach
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Literal
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ContentState(TypedDict):
|
||||
input_text: str
|
||||
content_type: str
|
||||
result: str
|
||||
|
||||
def classify_content(state: ContentState) -> dict:
|
||||
content_type = llm.invoke(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n{state['input_text']}"
|
||||
)
|
||||
return {"content_type": content_type.strip().lower()}
|
||||
|
||||
def process_technical(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as technical doc:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_creative(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as creative writing:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_business(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as business content:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
# Routing function
|
||||
def route_content(state: ContentState) -> Literal["technical", "creative", "business"]:
|
||||
return state["content_type"]
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ContentState)
|
||||
graph.add_node("classify", classify_content)
|
||||
graph.add_node("technical", process_technical)
|
||||
graph.add_node("creative", process_creative)
|
||||
graph.add_node("business", process_business)
|
||||
|
||||
graph.add_edge(START, "classify")
|
||||
graph.add_conditional_edges(
|
||||
"classify",
|
||||
route_content,
|
||||
{
|
||||
"technical": "technical",
|
||||
"creative": "creative",
|
||||
"business": "business",
|
||||
}
|
||||
)
|
||||
graph.add_edge("technical", END)
|
||||
graph.add_edge("creative", END)
|
||||
graph.add_edge("business", END)
|
||||
|
||||
app = graph.compile()
|
||||
result = app.invoke({"input_text": "Explain how TCP handshakes work"})
|
||||
```
|
||||
|
||||
You need a separate routing function, explicit conditional edge mapping, and termination edges for every branch. The routing logic is decoupled from the node that produces the routing decision.
|
||||
|
||||
### CrewAI Flows Approach
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent
|
||||
from crewai.flow.flow import Flow, listen, router, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ContentState(BaseModel):
|
||||
input_text: str = ""
|
||||
content_type: str = ""
|
||||
result: str = ""
|
||||
|
||||
class ContentFlow(Flow[ContentState]):
|
||||
@start()
|
||||
def classify_content(self):
|
||||
self.state.content_type = (
|
||||
llm.call(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n"
|
||||
f"{self.state.input_text}"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
return self.state.content_type
|
||||
|
||||
@router(classify_content)
|
||||
def route_content(self, classification):
|
||||
if classification == "technical":
|
||||
return "process_technical"
|
||||
elif classification == "creative":
|
||||
return "process_creative"
|
||||
else:
|
||||
return "process_business"
|
||||
|
||||
@listen("process_technical")
|
||||
def handle_technical(self):
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Produce clear, accurate technical documentation",
|
||||
backstory="You are an expert technical writer who specializes in "
|
||||
"explaining complex technical concepts precisely.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as technical doc:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_creative")
|
||||
def handle_creative(self):
|
||||
agent = Agent(
|
||||
role="Creative Writer",
|
||||
goal="Craft engaging and imaginative creative content",
|
||||
backstory="You are a talented creative writer with a flair for "
|
||||
"compelling storytelling and vivid expression.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as creative writing:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_business")
|
||||
def handle_business(self):
|
||||
agent = Agent(
|
||||
role="Business Writer",
|
||||
goal="Produce professional, results-oriented business content",
|
||||
backstory="You are an experienced business writer who communicates "
|
||||
"strategy and value clearly to professional audiences.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as business content:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
flow = ContentFlow()
|
||||
flow.state.input_text = "Explain how TCP handshakes work"
|
||||
flow.kickoff()
|
||||
print(flow.state.result)
|
||||
|
||||
```
|
||||
|
||||
The `@router()` decorator turns a method into a decision point. It returns a string that matches a listener — no mapping dictionaries, no separate routing functions. The branching logic reads like a Python `if` statement because it *is* one.
|
||||
|
||||
---
|
||||
|
||||
## Demo 3: Integrating AI Agent Crews into Flows
|
||||
|
||||
Here's where CrewAI's real power shines. Flows aren't just for chaining LLM calls — they orchestrate full **Crews** of autonomous agents. This is something LangGraph simply doesn't have a native equivalent for.
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ArticleState(BaseModel):
|
||||
topic: str = ""
|
||||
research: str = ""
|
||||
draft: str = ""
|
||||
final_article: str = ""
|
||||
|
||||
class ArticleFlow(Flow[ArticleState]):
|
||||
|
||||
@start()
|
||||
def run_research_crew(self):
|
||||
"""A full Crew of agents handles research."""
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal=f"Produce comprehensive research on: {self.state.topic}",
|
||||
backstory="You're a veteran analyst known for thorough, "
|
||||
"well-sourced research reports.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
research_task = Task(
|
||||
description=f"Research '{self.state.topic}' thoroughly. "
|
||||
"Cover key trends, data points, and expert opinions.",
|
||||
expected_output="A detailed research brief with sources.",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
crew = Crew(agents=[researcher], tasks=[research_task])
|
||||
result = crew.kickoff()
|
||||
self.state.research = result.raw
|
||||
return result.raw
|
||||
|
||||
@listen(run_research_crew)
|
||||
def run_writing_crew(self, research_output):
|
||||
"""A different Crew handles writing."""
|
||||
writer = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write a compelling article based on provided research.",
|
||||
backstory="You turn complex research into engaging, clear prose.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
editor = Agent(
|
||||
role="Senior Editor",
|
||||
goal="Review and polish articles for publication quality.",
|
||||
backstory="20 years of editorial experience at top tech publications.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
write_task = Task(
|
||||
description=f"Write an article based on this research:\n{self.state.research}",
|
||||
expected_output="A well-structured draft article.",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
edit_task = Task(
|
||||
description="Review, fact-check, and polish the draft article.",
|
||||
expected_output="A publication-ready article.",
|
||||
agent=editor
|
||||
)
|
||||
|
||||
crew = Crew(agents=[writer, editor], tasks=[write_task, edit_task])
|
||||
result = crew.kickoff()
|
||||
self.state.final_article = result.raw
|
||||
return result.raw
|
||||
|
||||
# Run the full pipeline
|
||||
flow = ArticleFlow()
|
||||
flow.state.topic = "The Future of Edge AI"
|
||||
flow.kickoff()
|
||||
print(flow.state.final_article)
|
||||
```
|
||||
|
||||
This is the key insight: **Flows provide the orchestration layer, and Crews provide the intelligence layer.** Each step in a Flow can spin up a full team of collaborating agents, each with their own roles, goals, and tools. You get structured, predictable control flow *and* autonomous agent collaboration — the best of both worlds.
|
||||
|
||||
In LangGraph, achieving something similar means manually implementing agent communication protocols, tool-calling loops, and delegation logic inside your node functions. It's possible, but it's plumbing you're building from scratch every time.
|
||||
|
||||
---
|
||||
|
||||
## Demo 4: Parallel Execution and Synchronization
|
||||
|
||||
Real-world pipelines often need to fan out work and join the results. CrewAI Flows handles this elegantly with `and_` and `or_` operators.
|
||||
|
||||
```python
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, and_, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class AnalysisState(BaseModel):
|
||||
topic: str = ""
|
||||
market_data: str = ""
|
||||
tech_analysis: str = ""
|
||||
competitor_intel: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ParallelAnalysisFlow(Flow[AnalysisState]):
|
||||
@start()
|
||||
def start_method(self):
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_market_data(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def run_tech_analysis(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_competitor_intel(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(and_(gather_market_data, run_tech_analysis, gather_competitor_intel))
|
||||
def synthesize_report(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
flow = ParallelAnalysisFlow()
|
||||
flow.state.topic = "AI-powered developer tools"
|
||||
flow.kickoff()
|
||||
|
||||
```
|
||||
|
||||
Multiple `@start()` decorators fire in parallel. The `and_()` combinator on the `@listen` decorator ensures `synthesize_report` only executes after *all three* upstream methods complete. There's also `or_()` for when you want to proceed as soon as *any* upstream task finishes.
|
||||
|
||||
In LangGraph, you'd need to build a fan-out/fan-in pattern with parallel branches, a synchronization node, and careful state merging — all wired explicitly through edges.
|
||||
|
||||
---
|
||||
|
||||
## Why CrewAI Flows for Production
|
||||
|
||||
Beyond cleaner syntax, Flows deliver several production-critical advantages:
|
||||
|
||||
**Built-in state persistence.** Flow state is backed by LanceDB, meaning your workflows can survive crashes, be resumed, and accumulate knowledge across runs. LangGraph requires you to configure a separate checkpointer.
|
||||
|
||||
**Type-safe state management.** Pydantic models give you validation, serialization, and IDE support out of the box. LangGraph's `TypedDict` states don't validate at runtime.
|
||||
|
||||
**First-class agent orchestration.** Crews are a native primitive. You define agents with roles, goals, backstories, and tools — and they collaborate autonomously within the structured envelope of a Flow. No need to reinvent multi-agent coordination.
|
||||
|
||||
**Simpler mental model.** Decorators declare intent. `@start` means "begin here." `@listen(x)` means "run after x." `@router(x)` means "decide where to go after x." The code reads like the workflow it describes.
|
||||
|
||||
**CLI integration.** Run flows with `crewai run`. No separate compilation step, no graph serialization. Your Flow is a Python class, and it runs like one.
|
||||
|
||||
---
|
||||
|
||||
## Migration Cheat Sheet
|
||||
|
||||
If you're sitting on a LangGraph codebase and want to move to CrewAI Flows, here's a practical conversion guide:
|
||||
|
||||
1. **Map your state.** Convert your `TypedDict` to a Pydantic `BaseModel`. Add default values for all fields.
|
||||
2. **Convert nodes to methods.** Each `add_node` function becomes a method on your `Flow` subclass. Replace `state["field"]` reads with `self.state.field`.
|
||||
3. **Replace edges with decorators.** Your `add_edge(START, "first_node")` becomes `@start()` on the first method. Sequential `add_edge("a", "b")` becomes `@listen(a)` on method `b`.
|
||||
4. **Replace conditional edges with `@router`.** Your routing function and `add_conditional_edges()` mapping become a single `@router()` method that returns a route string.
|
||||
5. **Replace compile + invoke with kickoff.** Drop `graph.compile()`. Call `flow.kickoff()` instead.
|
||||
6. **Consider where Crews fit.** Any node where you have complex multi-step agent logic is a candidate for extraction into a Crew. This is where you'll see the biggest quality improvement.
|
||||
|
||||
---
|
||||
|
||||
## Getting Started
|
||||
|
||||
Install CrewAI and scaffold a new Flow project:
|
||||
|
||||
```bash
|
||||
pip install crewai
|
||||
crewai create flow my_first_flow
|
||||
cd my_first_flow
|
||||
```
|
||||
|
||||
This generates a project structure with a ready-to-edit Flow class, configuration files, and a `pyproject.toml` with `type = "flow"` already set. Run it with:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
From there, add your agents, wire up your listeners, and ship it.
|
||||
|
||||
---
|
||||
|
||||
## Final Thoughts
|
||||
|
||||
LangGraph taught the ecosystem that AI workflows need structure. That was an important lesson. But CrewAI Flows takes that lesson and delivers it in a form that's faster to write, easier to read, and more powerful in production — especially when your workflows involve multiple collaborating agents.
|
||||
|
||||
If you're building anything beyond a single-agent chain, give Flows a serious look. The decorator-driven model, native Crew integration, and built-in state management mean you'll spend less time on plumbing and more time on the problems that matter.
|
||||
|
||||
Start with `crewai create flow`. You won't look back.
|
||||
@@ -4,6 +4,38 @@ description: "CrewAI의 제품 업데이트, 개선 사항 및 버그 수정"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="2026년 3월 4일">
|
||||
## v1.10.1
|
||||
|
||||
[GitHub 릴리스 보기](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1)
|
||||
|
||||
## 변경 사항
|
||||
|
||||
### 기능
|
||||
- Gemini GenAI 업그레이드
|
||||
|
||||
### 버그 수정
|
||||
- 재귀를 피하기 위해 실행기 리스너 값을 조정
|
||||
- Gemini에서 병렬 함수 응답 부분을 단일 Content 객체로 그룹화
|
||||
- Gemini에서 사고 모델의 사고 출력을 표시
|
||||
- 에이전트 도구가 None일 때 MCP 및 플랫폼 도구 로드
|
||||
- A2A에서 실행 이벤트 루프가 있는 Jupyter 환경 지원
|
||||
- 일시적인 추적을 위해 익명 ID 사용
|
||||
- 조건부로 플러스 헤더 전달
|
||||
- 원격 측정을 위해 비주 스레드에서 신호 처리기 등록 건너뛰기
|
||||
- 도구 오류를 관찰로 주입하고 이름 충돌 해결
|
||||
- Dependabot 경고를 해결하기 위해 pypdf를 4.x에서 6.7.4로 업그레이드
|
||||
- 심각 및 높은 Dependabot 보안 경고 해결
|
||||
|
||||
### 문서
|
||||
- Composio 도구 문서를 지역별로 동기화
|
||||
|
||||
## 기여자
|
||||
|
||||
@giulio-leone, @greysonlalonde, @haxzie, @joaomdmoura, @lorenzejay, @mattatcha, @mplachta, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="2026년 2월 27일">
|
||||
## v1.10.1a1
|
||||
|
||||
|
||||
518
docs/ko/guides/migration/migrating-from-langgraph.mdx
Normal file
518
docs/ko/guides/migration/migrating-from-langgraph.mdx
Normal file
@@ -0,0 +1,518 @@
|
||||
---
|
||||
title: "LangGraph에서 CrewAI로 옮기기: 엔지니어를 위한 실전 가이드"
|
||||
description: LangGraph로 이미 구축했다면, 프로젝트를 CrewAI로 빠르게 옮기는 방법을 알아보세요
|
||||
icon: switch
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
LangGraph로 에이전트를 구축해 왔습니다. `StateGraph`와 씨름하고, 조건부 에지를 연결하고, 새벽 2시에 상태 딕셔너리를 디버깅해 본 적도 있죠. 동작은 하지만 — 어느 순간부터 프로덕션으로 가는 더 나은 길이 없을까 고민하게 됩니다.
|
||||
|
||||
있습니다. **CrewAI Flows**는 이벤트 기반 오케스트레이션, 조건부 라우팅, 공유 상태라는 동일한 힘을 훨씬 적은 보일러플레이트와 실제로 다단계 AI 워크플로우를 생각하는 방식에 잘 맞는 정신적 모델로 제공합니다.
|
||||
|
||||
이 글은 핵심 개념을 나란히 비교하고 실제 코드 비교를 보여주며, 다음으로 손이 갈 프레임워크가 왜 CrewAI Flows인지 설명합니다.
|
||||
|
||||
---
|
||||
|
||||
## 정신적 모델의 전환
|
||||
|
||||
LangGraph는 **그래프**로 생각하라고 요구합니다: 노드, 에지, 그리고 상태 딕셔너리. 모든 워크플로우는 계산 단계 사이의 전이를 명시적으로 연결하는 방향 그래프입니다. 강력하지만, 특히 워크플로우가 몇 개의 결정 지점이 있는 순차적 흐름일 때 이 추상화는 오버헤드를 가져옵니다.
|
||||
|
||||
CrewAI Flows는 **이벤트**로 생각하라고 요구합니다: 시작하는 메서드, 결과를 듣는 메서드, 실행을 라우팅하는 메서드. 워크플로우의 토폴로지는 명시적 그래프 구성 대신 데코레이터 어노테이션에서 드러납니다. 이것은 단순한 문법 설탕이 아니라 — 파이프라인을 설계하고 읽고 유지하는 방식을 바꿉니다.
|
||||
|
||||
핵심 매핑은 다음과 같습니다:
|
||||
|
||||
| LangGraph 개념 | CrewAI Flows 대응 |
|
||||
| --- | --- |
|
||||
| `StateGraph` class | `Flow` class |
|
||||
| `add_node()` | Methods decorated with `@start`, `@listen` |
|
||||
| `add_edge()` / `add_conditional_edges()` | `@listen()` / `@router()` decorators |
|
||||
| `TypedDict` state | Pydantic `BaseModel` state |
|
||||
| `START` / `END` constants | `@start()` decorator / natural method return |
|
||||
| `graph.compile()` | `flow.kickoff()` |
|
||||
| Checkpointer / persistence | Built-in memory (LanceDB-backed) |
|
||||
|
||||
실제로 어떻게 보이는지 살펴보겠습니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 1: 간단한 순차 파이프라인
|
||||
|
||||
주제를 받아 조사하고, 요약을 작성한 뒤, 결과를 포맷팅하는 파이프라인을 만든다고 해봅시다. 각 프레임워크는 이렇게 처리합니다.
|
||||
|
||||
### LangGraph 방식
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ResearchState(TypedDict):
|
||||
topic: str
|
||||
raw_research: str
|
||||
summary: str
|
||||
formatted_output: str
|
||||
|
||||
def research_topic(state: ResearchState) -> dict:
|
||||
# Call an LLM or search API
|
||||
result = llm.invoke(f"Research the topic: {state['topic']}")
|
||||
return {"raw_research": result}
|
||||
|
||||
def write_summary(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Summarize this research:\n{state['raw_research']}"
|
||||
)
|
||||
return {"summary": result}
|
||||
|
||||
def format_output(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Format this summary as a polished article section:\n{state['summary']}"
|
||||
)
|
||||
return {"formatted_output": result}
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ResearchState)
|
||||
graph.add_node("research", research_topic)
|
||||
graph.add_node("summarize", write_summary)
|
||||
graph.add_node("format", format_output)
|
||||
|
||||
graph.add_edge(START, "research")
|
||||
graph.add_edge("research", "summarize")
|
||||
graph.add_edge("summarize", "format")
|
||||
graph.add_edge("format", END)
|
||||
|
||||
# Compile and run
|
||||
app = graph.compile()
|
||||
result = app.invoke({"topic": "quantum computing advances in 2026"})
|
||||
print(result["formatted_output"])
|
||||
```
|
||||
|
||||
함수를 정의하고 노드로 등록한 다음, 모든 전이를 수동으로 연결합니다. 이렇게 단순한 순서인데도 의례처럼 해야 할 작업이 많습니다.
|
||||
|
||||
### CrewAI Flows 방식
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent, Crew, Process, Task
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ResearchState(BaseModel):
|
||||
topic: str = ""
|
||||
raw_research: str = ""
|
||||
summary: str = ""
|
||||
formatted_output: str = ""
|
||||
|
||||
class ResearchFlow(Flow[ResearchState]):
|
||||
@start()
|
||||
def research_topic(self):
|
||||
# Option 1: Direct LLM call
|
||||
result = llm.call(f"Research the topic: {self.state.topic}")
|
||||
self.state.raw_research = result
|
||||
return result
|
||||
|
||||
@listen(research_topic)
|
||||
def write_summary(self, research_output):
|
||||
# Option 2: A single agent
|
||||
summarizer = Agent(
|
||||
role="Research Summarizer",
|
||||
goal="Produce concise, accurate summaries of research content",
|
||||
backstory="You are an expert at distilling complex research into clear, "
|
||||
"digestible summaries.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
result = summarizer.kickoff(
|
||||
f"Summarize this research:\n{self.state.raw_research}"
|
||||
)
|
||||
self.state.summary = str(result)
|
||||
return self.state.summary
|
||||
|
||||
@listen(write_summary)
|
||||
def format_output(self, summary_output):
|
||||
# Option 3: a complete crew (with one or more agents)
|
||||
formatter = Agent(
|
||||
role="Content Formatter",
|
||||
goal="Transform research summaries into polished, publication-ready article sections",
|
||||
backstory="You are a skilled editor with expertise in structuring and "
|
||||
"presenting technical content for a general audience.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
format_task = Task(
|
||||
description=f"Format this summary as a polished article section:\n{self.state.summary}",
|
||||
expected_output="A well-structured, polished article section ready for publication.",
|
||||
agent=formatter,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[formatter],
|
||||
tasks=[format_task],
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
result = crew.kickoff()
|
||||
self.state.formatted_output = str(result)
|
||||
return self.state.formatted_output
|
||||
|
||||
# Run the flow
|
||||
flow = ResearchFlow()
|
||||
flow.state.topic = "quantum computing advances in 2026"
|
||||
result = flow.kickoff()
|
||||
print(flow.state.formatted_output)
|
||||
|
||||
```
|
||||
|
||||
눈에 띄는 차이점이 있습니다: 그래프 구성 없음, 에지 연결 없음, 컴파일 단계 없음. 실행 순서는 로직이 있는 곳에서 바로 선언됩니다. `@start()`는 진입점을 표시하고, `@listen(method_name)`은 단계들을 연결합니다. 상태는 타입 안전성, 검증, IDE 자동 완성까지 제공하는 제대로 된 Pydantic 모델입니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 2: 조건부 라우팅
|
||||
|
||||
여기서 흥미로워집니다. 콘텐츠 유형에 따라 서로 다른 처리 경로로 라우팅하는 파이프라인을 만든다고 해봅시다.
|
||||
|
||||
### LangGraph 방식
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Literal
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ContentState(TypedDict):
|
||||
input_text: str
|
||||
content_type: str
|
||||
result: str
|
||||
|
||||
def classify_content(state: ContentState) -> dict:
|
||||
content_type = llm.invoke(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n{state['input_text']}"
|
||||
)
|
||||
return {"content_type": content_type.strip().lower()}
|
||||
|
||||
def process_technical(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as technical doc:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_creative(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as creative writing:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_business(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as business content:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
# Routing function
|
||||
def route_content(state: ContentState) -> Literal["technical", "creative", "business"]:
|
||||
return state["content_type"]
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ContentState)
|
||||
graph.add_node("classify", classify_content)
|
||||
graph.add_node("technical", process_technical)
|
||||
graph.add_node("creative", process_creative)
|
||||
graph.add_node("business", process_business)
|
||||
|
||||
graph.add_edge(START, "classify")
|
||||
graph.add_conditional_edges(
|
||||
"classify",
|
||||
route_content,
|
||||
{
|
||||
"technical": "technical",
|
||||
"creative": "creative",
|
||||
"business": "business",
|
||||
}
|
||||
)
|
||||
graph.add_edge("technical", END)
|
||||
graph.add_edge("creative", END)
|
||||
graph.add_edge("business", END)
|
||||
|
||||
app = graph.compile()
|
||||
result = app.invoke({"input_text": "Explain how TCP handshakes work"})
|
||||
```
|
||||
|
||||
별도의 라우팅 함수, 명시적 조건부 에지 매핑, 그리고 모든 분기에 대한 종료 에지가 필요합니다. 라우팅 결정 로직이 그 결정을 만들어 내는 노드와 분리됩니다.
|
||||
|
||||
### CrewAI Flows 방식
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent
|
||||
from crewai.flow.flow import Flow, listen, router, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ContentState(BaseModel):
|
||||
input_text: str = ""
|
||||
content_type: str = ""
|
||||
result: str = ""
|
||||
|
||||
class ContentFlow(Flow[ContentState]):
|
||||
@start()
|
||||
def classify_content(self):
|
||||
self.state.content_type = (
|
||||
llm.call(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n"
|
||||
f"{self.state.input_text}"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
return self.state.content_type
|
||||
|
||||
@router(classify_content)
|
||||
def route_content(self, classification):
|
||||
if classification == "technical":
|
||||
return "process_technical"
|
||||
elif classification == "creative":
|
||||
return "process_creative"
|
||||
else:
|
||||
return "process_business"
|
||||
|
||||
@listen("process_technical")
|
||||
def handle_technical(self):
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Produce clear, accurate technical documentation",
|
||||
backstory="You are an expert technical writer who specializes in "
|
||||
"explaining complex technical concepts precisely.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as technical doc:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_creative")
|
||||
def handle_creative(self):
|
||||
agent = Agent(
|
||||
role="Creative Writer",
|
||||
goal="Craft engaging and imaginative creative content",
|
||||
backstory="You are a talented creative writer with a flair for "
|
||||
"compelling storytelling and vivid expression.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as creative writing:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_business")
|
||||
def handle_business(self):
|
||||
agent = Agent(
|
||||
role="Business Writer",
|
||||
goal="Produce professional, results-oriented business content",
|
||||
backstory="You are an experienced business writer who communicates "
|
||||
"strategy and value clearly to professional audiences.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as business content:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
flow = ContentFlow()
|
||||
flow.state.input_text = "Explain how TCP handshakes work"
|
||||
flow.kickoff()
|
||||
print(flow.state.result)
|
||||
|
||||
```
|
||||
|
||||
`@router()` 데코레이터는 메서드를 결정 지점으로 만듭니다. 리스너와 매칭되는 문자열을 반환하므로, 매핑 딕셔너리도, 별도의 라우팅 함수도 필요 없습니다. 분기 로직이 Python `if` 문처럼 읽히는 이유는, 실제로 `if` 문이기 때문입니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 3: AI 에이전트 Crew를 Flow에 통합하기
|
||||
|
||||
여기서 CrewAI의 진짜 힘이 드러납니다. Flows는 LLM 호출을 연결하는 것에 그치지 않고 자율적인 에이전트 **Crew** 전체를 오케스트레이션합니다. 이는 LangGraph에 기본으로 대응되는 개념이 없습니다.
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ArticleState(BaseModel):
|
||||
topic: str = ""
|
||||
research: str = ""
|
||||
draft: str = ""
|
||||
final_article: str = ""
|
||||
|
||||
class ArticleFlow(Flow[ArticleState]):
|
||||
|
||||
@start()
|
||||
def run_research_crew(self):
|
||||
"""A full Crew of agents handles research."""
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal=f"Produce comprehensive research on: {self.state.topic}",
|
||||
backstory="You're a veteran analyst known for thorough, "
|
||||
"well-sourced research reports.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
research_task = Task(
|
||||
description=f"Research '{self.state.topic}' thoroughly. "
|
||||
"Cover key trends, data points, and expert opinions.",
|
||||
expected_output="A detailed research brief with sources.",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
crew = Crew(agents=[researcher], tasks=[research_task])
|
||||
result = crew.kickoff()
|
||||
self.state.research = result.raw
|
||||
return result.raw
|
||||
|
||||
@listen(run_research_crew)
|
||||
def run_writing_crew(self, research_output):
|
||||
"""A different Crew handles writing."""
|
||||
writer = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write a compelling article based on provided research.",
|
||||
backstory="You turn complex research into engaging, clear prose.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
editor = Agent(
|
||||
role="Senior Editor",
|
||||
goal="Review and polish articles for publication quality.",
|
||||
backstory="20 years of editorial experience at top tech publications.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
write_task = Task(
|
||||
description=f"Write an article based on this research:\n{self.state.research}",
|
||||
expected_output="A well-structured draft article.",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
edit_task = Task(
|
||||
description="Review, fact-check, and polish the draft article.",
|
||||
expected_output="A publication-ready article.",
|
||||
agent=editor
|
||||
)
|
||||
|
||||
crew = Crew(agents=[writer, editor], tasks=[write_task, edit_task])
|
||||
result = crew.kickoff()
|
||||
self.state.final_article = result.raw
|
||||
return result.raw
|
||||
|
||||
# Run the full pipeline
|
||||
flow = ArticleFlow()
|
||||
flow.state.topic = "The Future of Edge AI"
|
||||
flow.kickoff()
|
||||
print(flow.state.final_article)
|
||||
```
|
||||
|
||||
핵심 인사이트는 다음과 같습니다: **Flows는 오케스트레이션 레이어를, Crews는 지능 레이어를 제공합니다.** Flow의 각 단계는 각자의 역할, 목표, 도구를 가진 협업 에이전트 팀을 띄울 수 있습니다. 구조화되고 예측 가능한 제어 흐름 *그리고* 자율적 에이전트 협업 — 두 세계의 장점을 모두 얻습니다.
|
||||
|
||||
LangGraph에서 비슷한 것을 하려면 노드 함수 안에 에이전트 통신 프로토콜, 도구 호출 루프, 위임 로직을 직접 구현해야 합니다. 가능하긴 하지만, 매번 처음부터 배관을 만드는 셈입니다.
|
||||
|
||||
---
|
||||
|
||||
## 데모 4: 병렬 실행과 동기화
|
||||
|
||||
실제 파이프라인은 종종 작업을 병렬로 분기하고 결과를 합쳐야 합니다. CrewAI Flows는 `and_`와 `or_` 연산자로 이를 우아하게 처리합니다.
|
||||
|
||||
```python
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, and_, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class AnalysisState(BaseModel):
|
||||
topic: str = ""
|
||||
market_data: str = ""
|
||||
tech_analysis: str = ""
|
||||
competitor_intel: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ParallelAnalysisFlow(Flow[AnalysisState]):
|
||||
@start()
|
||||
def start_method(self):
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_market_data(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def run_tech_analysis(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_competitor_intel(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(and_(gather_market_data, run_tech_analysis, gather_competitor_intel))
|
||||
def synthesize_report(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
flow = ParallelAnalysisFlow()
|
||||
flow.state.topic = "AI-powered developer tools"
|
||||
flow.kickoff()
|
||||
|
||||
```
|
||||
|
||||
여러 `@start()` 데코레이터는 병렬로 실행됩니다. `@listen` 데코레이터의 `and_()` 결합자는 `synthesize_report`가 *세 가지* 상위 메서드가 모두 완료된 뒤에만 실행되도록 보장합니다. *어떤* 상위 작업이든 끝나는 즉시 진행하고 싶다면 `or_()`도 사용할 수 있습니다.
|
||||
|
||||
LangGraph에서는 병렬 분기, 동기화 노드, 신중한 상태 병합이 포함된 fan-out/fan-in 패턴을 만들어야 하며 — 모든 것을 에지로 명시적으로 연결해야 합니다.
|
||||
|
||||
---
|
||||
|
||||
## 프로덕션에서 CrewAI Flows를 쓰는 이유
|
||||
|
||||
깔끔한 문법을 넘어, Flows는 여러 프로덕션 핵심 이점을 제공합니다:
|
||||
|
||||
**내장 상태 지속성.** Flow 상태는 LanceDB에 의해 백업되므로 워크플로우가 크래시에서 살아남고, 재개될 수 있으며, 실행 간에 지식을 축적할 수 있습니다. LangGraph는 별도의 체크포인터를 구성해야 합니다.
|
||||
|
||||
**타입 안전한 상태 관리.** Pydantic 모델은 즉시 검증, 직렬화, IDE 지원을 제공합니다. LangGraph의 `TypedDict` 상태는 런타임 검증을 하지 않습니다.
|
||||
|
||||
**일급 에이전트 오케스트레이션.** Crews는 기본 프리미티브입니다. 역할, 목표, 배경, 도구를 가진 에이전트를 정의하고, Flow의 구조적 틀 안에서 자율적으로 협업하게 합니다. 다중 에이전트 조율을 다시 만들 필요가 없습니다.
|
||||
|
||||
**더 단순한 정신적 모델.** 데코레이터는 의도를 선언합니다. `@start`는 "여기서 시작", `@listen(x)`는 "x 이후 실행", `@router(x)`는 "x 이후 어디로 갈지 결정"을 의미합니다. 코드는 자신이 설명하는 워크플로우처럼 읽힙니다.
|
||||
|
||||
**CLI 통합.** `crewai run`으로 Flows를 실행합니다. 별도의 컴파일 단계나 그래프 직렬화가 없습니다. Flow는 Python 클래스이며, 그대로 실행됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 마이그레이션 치트 시트
|
||||
|
||||
LangGraph 코드베이스를 CrewAI Flows로 옮기고 싶다면, 다음의 실전 변환 가이드를 참고하세요:
|
||||
|
||||
1. **상태를 매핑하세요.** `TypedDict`를 Pydantic `BaseModel`로 변환하고 모든 필드에 기본값을 추가하세요.
|
||||
2. **노드를 메서드로 변환하세요.** 각 `add_node` 함수는 `Flow` 서브클래스의 메서드가 됩니다. `state["field"]` 읽기는 `self.state.field`로 바꾸세요.
|
||||
3. **에지를 데코레이터로 교체하세요.** `add_edge(START, "first_node")`는 첫 메서드의 `@start()`가 됩니다. 순차적인 `add_edge("a", "b")`는 `b` 메서드의 `@listen(a)`가 됩니다.
|
||||
4. **조건부 에지는 `@router`로 교체하세요.** 라우팅 함수와 `add_conditional_edges()` 매핑은 하나의 `@router()` 메서드로 통합하고, 라우트 문자열을 반환하세요.
|
||||
5. **compile + invoke를 kickoff으로 교체하세요.** `graph.compile()`를 제거하고 `flow.kickoff()`를 호출하세요.
|
||||
6. **Crew가 들어갈 지점을 고려하세요.** 복잡한 다단계 에이전트 로직이 있는 노드는 Crew로 분리할 후보입니다. 이 부분에서 가장 큰 품질 향상을 체감할 수 있습니다.
|
||||
|
||||
---
|
||||
|
||||
## 시작하기
|
||||
|
||||
CrewAI를 설치하고 새 Flow 프로젝트를 스캐폴딩하세요:
|
||||
|
||||
```bash
|
||||
pip install crewai
|
||||
crewai create flow my_first_flow
|
||||
cd my_first_flow
|
||||
```
|
||||
|
||||
이렇게 하면 바로 편집 가능한 Flow 클래스, 설정 파일, 그리고 `type = "flow"`가 이미 설정된 `pyproject.toml`이 포함된 프로젝트 구조가 생성됩니다. 다음으로 실행하세요:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
그 다음부터는 에이전트를 추가하고 리스너를 연결한 뒤, 배포하면 됩니다.
|
||||
|
||||
---
|
||||
|
||||
## 마무리
|
||||
|
||||
LangGraph는 AI 워크플로우에 구조가 필요하다는 사실을 생태계에 일깨워 주었습니다. 중요한 교훈이었습니다. 하지만 CrewAI Flows는 그 교훈을 더 빠르게 쓰고, 더 쉽게 읽으며, 프로덕션에서 더 강력한 형태로 제공합니다 — 특히 워크플로우에 여러 에이전트의 협업이 포함될 때 그렇습니다.
|
||||
|
||||
단일 에이전트 체인을 넘는 무엇인가를 만들고 있다면, Flows를 진지하게 검토해 보세요. 데코레이터 기반 모델, Crews의 네이티브 통합, 내장 상태 관리를 통해 배관 작업에 쓰는 시간을 줄이고, 중요한 문제에 더 많은 시간을 쓸 수 있습니다.
|
||||
|
||||
`crewai create flow`로 시작하세요. 후회하지 않을 겁니다.
|
||||
@@ -4,6 +4,38 @@ description: "Atualizações de produto, melhorias e correções do CrewAI"
|
||||
icon: "clock"
|
||||
mode: "wide"
|
||||
---
|
||||
<Update label="04 mar 2026">
|
||||
## v1.10.1
|
||||
|
||||
[Ver release no GitHub](https://github.com/crewAIInc/crewAI/releases/tag/1.10.1)
|
||||
|
||||
## O que mudou
|
||||
|
||||
### Recursos
|
||||
- Atualizar Gemini GenAI
|
||||
|
||||
### Correções de Bugs
|
||||
- Ajustar o valor do listener do executor para evitar recursão
|
||||
- Agrupar partes da resposta da função paralela em um único objeto Content no Gemini
|
||||
- Exibir a saída de pensamento dos modelos de pensamento no Gemini
|
||||
- Carregar ferramentas MCP e da plataforma quando as ferramentas do agente forem None
|
||||
- Suportar ambientes Jupyter com loops de eventos em A2A
|
||||
- Usar ID anônimo para rastreamentos efêmeros
|
||||
- Passar condicionalmente o cabeçalho plus
|
||||
- Ignorar o registro do manipulador de sinal em threads não principais para telemetria
|
||||
- Injetar erros de ferramentas como observações e resolver colisões de nomes
|
||||
- Atualizar pypdf de 4.x para 6.7.4 para resolver alertas do Dependabot
|
||||
- Resolver alertas de segurança críticos e altos do Dependabot
|
||||
|
||||
### Documentação
|
||||
- Sincronizar a documentação da ferramenta Composio entre locais
|
||||
|
||||
## Contribuidores
|
||||
|
||||
@giulio-leone, @greysonlalonde, @haxzie, @joaomdmoura, @lorenzejay, @mattatcha, @mplachta, @nicoferdi96
|
||||
|
||||
</Update>
|
||||
|
||||
<Update label="27 fev 2026">
|
||||
## v1.10.1a1
|
||||
|
||||
|
||||
518
docs/pt-BR/guides/migration/migrating-from-langgraph.mdx
Normal file
518
docs/pt-BR/guides/migration/migrating-from-langgraph.mdx
Normal file
@@ -0,0 +1,518 @@
|
||||
---
|
||||
title: "Migrando do LangGraph para o CrewAI: um guia prático para engenheiros"
|
||||
description: Se você já construiu com LangGraph, saiba como portar rapidamente seus projetos para o CrewAI
|
||||
icon: switch
|
||||
mode: "wide"
|
||||
---
|
||||
|
||||
Você construiu agentes com LangGraph. Já lutou com o `StateGraph`, ligou arestas condicionais e depurou dicionários de estado às 2 da manhã. Funciona — mas, em algum momento, você começou a se perguntar se existe um caminho melhor para produção.
|
||||
|
||||
Existe. **CrewAI Flows** entrega o mesmo poder — orquestração orientada a eventos, roteamento condicional, estado compartilhado — com muito menos boilerplate e um modelo mental que se alinha a como você realmente pensa sobre fluxos de trabalho de IA em múltiplas etapas.
|
||||
|
||||
Este artigo apresenta os conceitos principais lado a lado, mostra comparações reais de código e demonstra por que o CrewAI Flows é o framework que você vai querer usar a seguir.
|
||||
|
||||
---
|
||||
|
||||
## A Mudança de Modelo Mental
|
||||
|
||||
LangGraph pede que você pense em **grafos**: nós, arestas e dicionários de estado. Todo workflow é um grafo direcionado em que você conecta explicitamente as transições entre as etapas de computação. É poderoso, mas a abstração traz overhead — especialmente quando o seu fluxo é fundamentalmente sequencial com alguns pontos de decisão.
|
||||
|
||||
CrewAI Flows pede que você pense em **eventos**: métodos que iniciam, métodos que escutam resultados e métodos que roteiam a execução. A topologia do workflow emerge de anotações com decorators, em vez de construção explícita do grafo. Isso não é apenas açúcar sintático — muda como você projeta, lê e mantém seus pipelines.
|
||||
|
||||
Veja o mapeamento principal:
|
||||
|
||||
| Conceito no LangGraph | Equivalente no CrewAI Flows |
|
||||
| --- | --- |
|
||||
| `StateGraph` class | `Flow` class |
|
||||
| `add_node()` | Methods decorated with `@start`, `@listen` |
|
||||
| `add_edge()` / `add_conditional_edges()` | `@listen()` / `@router()` decorators |
|
||||
| `TypedDict` state | Pydantic `BaseModel` state |
|
||||
| `START` / `END` constants | `@start()` decorator / natural method return |
|
||||
| `graph.compile()` | `flow.kickoff()` |
|
||||
| Checkpointer / persistence | Built-in memory (LanceDB-backed) |
|
||||
|
||||
Vamos ver como isso fica na prática.
|
||||
|
||||
---
|
||||
|
||||
## Demo 1: Um Pipeline Sequencial Simples
|
||||
|
||||
Imagine que você está construindo um pipeline que recebe um tema, pesquisa, escreve um resumo e formata a saída. Veja como cada framework lida com isso.
|
||||
|
||||
### Abordagem com LangGraph
|
||||
|
||||
```python
|
||||
from typing import TypedDict
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ResearchState(TypedDict):
|
||||
topic: str
|
||||
raw_research: str
|
||||
summary: str
|
||||
formatted_output: str
|
||||
|
||||
def research_topic(state: ResearchState) -> dict:
|
||||
# Call an LLM or search API
|
||||
result = llm.invoke(f"Research the topic: {state['topic']}")
|
||||
return {"raw_research": result}
|
||||
|
||||
def write_summary(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Summarize this research:\n{state['raw_research']}"
|
||||
)
|
||||
return {"summary": result}
|
||||
|
||||
def format_output(state: ResearchState) -> dict:
|
||||
result = llm.invoke(
|
||||
f"Format this summary as a polished article section:\n{state['summary']}"
|
||||
)
|
||||
return {"formatted_output": result}
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ResearchState)
|
||||
graph.add_node("research", research_topic)
|
||||
graph.add_node("summarize", write_summary)
|
||||
graph.add_node("format", format_output)
|
||||
|
||||
graph.add_edge(START, "research")
|
||||
graph.add_edge("research", "summarize")
|
||||
graph.add_edge("summarize", "format")
|
||||
graph.add_edge("format", END)
|
||||
|
||||
# Compile and run
|
||||
app = graph.compile()
|
||||
result = app.invoke({"topic": "quantum computing advances in 2026"})
|
||||
print(result["formatted_output"])
|
||||
```
|
||||
|
||||
Você define funções, registra-as como nós e conecta manualmente cada transição. Para uma sequência simples como essa, há muita cerimônia.
|
||||
|
||||
### Abordagem com CrewAI Flows
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent, Crew, Process, Task
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ResearchState(BaseModel):
|
||||
topic: str = ""
|
||||
raw_research: str = ""
|
||||
summary: str = ""
|
||||
formatted_output: str = ""
|
||||
|
||||
class ResearchFlow(Flow[ResearchState]):
|
||||
@start()
|
||||
def research_topic(self):
|
||||
# Option 1: Direct LLM call
|
||||
result = llm.call(f"Research the topic: {self.state.topic}")
|
||||
self.state.raw_research = result
|
||||
return result
|
||||
|
||||
@listen(research_topic)
|
||||
def write_summary(self, research_output):
|
||||
# Option 2: A single agent
|
||||
summarizer = Agent(
|
||||
role="Research Summarizer",
|
||||
goal="Produce concise, accurate summaries of research content",
|
||||
backstory="You are an expert at distilling complex research into clear, "
|
||||
"digestible summaries.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
result = summarizer.kickoff(
|
||||
f"Summarize this research:\n{self.state.raw_research}"
|
||||
)
|
||||
self.state.summary = str(result)
|
||||
return self.state.summary
|
||||
|
||||
@listen(write_summary)
|
||||
def format_output(self, summary_output):
|
||||
# Option 3: a complete crew (with one or more agents)
|
||||
formatter = Agent(
|
||||
role="Content Formatter",
|
||||
goal="Transform research summaries into polished, publication-ready article sections",
|
||||
backstory="You are a skilled editor with expertise in structuring and "
|
||||
"presenting technical content for a general audience.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
format_task = Task(
|
||||
description=f"Format this summary as a polished article section:\n{self.state.summary}",
|
||||
expected_output="A well-structured, polished article section ready for publication.",
|
||||
agent=formatter,
|
||||
)
|
||||
crew = Crew(
|
||||
agents=[formatter],
|
||||
tasks=[format_task],
|
||||
process=Process.sequential,
|
||||
verbose=True,
|
||||
)
|
||||
result = crew.kickoff()
|
||||
self.state.formatted_output = str(result)
|
||||
return self.state.formatted_output
|
||||
|
||||
# Run the flow
|
||||
flow = ResearchFlow()
|
||||
flow.state.topic = "quantum computing advances in 2026"
|
||||
result = flow.kickoff()
|
||||
print(flow.state.formatted_output)
|
||||
|
||||
```
|
||||
|
||||
Repare a diferença: nada de construção de grafo, de ligação de arestas, nem de etapa de compilação. A ordem de execução é declarada exatamente onde a lógica vive. `@start()` marca o ponto de entrada, e `@listen(method_name)` encadeia as etapas. O estado é um modelo Pydantic de verdade, com segurança de tipos, validação e auto-complete na IDE.
|
||||
|
||||
---
|
||||
|
||||
## Demo 2: Roteamento Condicional
|
||||
|
||||
Aqui é que fica interessante. Digamos que você está construindo um pipeline de conteúdo que roteia para diferentes caminhos de processamento com base no tipo de conteúdo detectado.
|
||||
|
||||
### Abordagem com LangGraph
|
||||
|
||||
```python
|
||||
from typing import TypedDict, Literal
|
||||
from langgraph.graph import StateGraph, START, END
|
||||
|
||||
class ContentState(TypedDict):
|
||||
input_text: str
|
||||
content_type: str
|
||||
result: str
|
||||
|
||||
def classify_content(state: ContentState) -> dict:
|
||||
content_type = llm.invoke(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n{state['input_text']}"
|
||||
)
|
||||
return {"content_type": content_type.strip().lower()}
|
||||
|
||||
def process_technical(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as technical doc:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_creative(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as creative writing:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
def process_business(state: ContentState) -> dict:
|
||||
result = llm.invoke(f"Process as business content:\n{state['input_text']}")
|
||||
return {"result": result}
|
||||
|
||||
# Routing function
|
||||
def route_content(state: ContentState) -> Literal["technical", "creative", "business"]:
|
||||
return state["content_type"]
|
||||
|
||||
# Build the graph
|
||||
graph = StateGraph(ContentState)
|
||||
graph.add_node("classify", classify_content)
|
||||
graph.add_node("technical", process_technical)
|
||||
graph.add_node("creative", process_creative)
|
||||
graph.add_node("business", process_business)
|
||||
|
||||
graph.add_edge(START, "classify")
|
||||
graph.add_conditional_edges(
|
||||
"classify",
|
||||
route_content,
|
||||
{
|
||||
"technical": "technical",
|
||||
"creative": "creative",
|
||||
"business": "business",
|
||||
}
|
||||
)
|
||||
graph.add_edge("technical", END)
|
||||
graph.add_edge("creative", END)
|
||||
graph.add_edge("business", END)
|
||||
|
||||
app = graph.compile()
|
||||
result = app.invoke({"input_text": "Explain how TCP handshakes work"})
|
||||
```
|
||||
|
||||
Você precisa de uma função de roteamento separada, de um mapeamento explícito de arestas condicionais e de arestas de término para cada ramificação. A lógica de roteamento fica desacoplada do nó que produz a decisão.
|
||||
|
||||
### Abordagem com CrewAI Flows
|
||||
|
||||
```python
|
||||
from crewai import LLM, Agent
|
||||
from crewai.flow.flow import Flow, listen, router, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class ContentState(BaseModel):
|
||||
input_text: str = ""
|
||||
content_type: str = ""
|
||||
result: str = ""
|
||||
|
||||
class ContentFlow(Flow[ContentState]):
|
||||
@start()
|
||||
def classify_content(self):
|
||||
self.state.content_type = (
|
||||
llm.call(
|
||||
f"Classify this content as 'technical', 'creative', or 'business':\n"
|
||||
f"{self.state.input_text}"
|
||||
)
|
||||
.strip()
|
||||
.lower()
|
||||
)
|
||||
return self.state.content_type
|
||||
|
||||
@router(classify_content)
|
||||
def route_content(self, classification):
|
||||
if classification == "technical":
|
||||
return "process_technical"
|
||||
elif classification == "creative":
|
||||
return "process_creative"
|
||||
else:
|
||||
return "process_business"
|
||||
|
||||
@listen("process_technical")
|
||||
def handle_technical(self):
|
||||
agent = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Produce clear, accurate technical documentation",
|
||||
backstory="You are an expert technical writer who specializes in "
|
||||
"explaining complex technical concepts precisely.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as technical doc:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_creative")
|
||||
def handle_creative(self):
|
||||
agent = Agent(
|
||||
role="Creative Writer",
|
||||
goal="Craft engaging and imaginative creative content",
|
||||
backstory="You are a talented creative writer with a flair for "
|
||||
"compelling storytelling and vivid expression.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as creative writing:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
@listen("process_business")
|
||||
def handle_business(self):
|
||||
agent = Agent(
|
||||
role="Business Writer",
|
||||
goal="Produce professional, results-oriented business content",
|
||||
backstory="You are an experienced business writer who communicates "
|
||||
"strategy and value clearly to professional audiences.",
|
||||
llm=llm,
|
||||
verbose=True,
|
||||
)
|
||||
self.state.result = str(
|
||||
agent.kickoff(f"Process as business content:\n{self.state.input_text}")
|
||||
)
|
||||
|
||||
flow = ContentFlow()
|
||||
flow.state.input_text = "Explain how TCP handshakes work"
|
||||
flow.kickoff()
|
||||
print(flow.state.result)
|
||||
|
||||
```
|
||||
|
||||
O decorator `@router()` transforma um método em um ponto de decisão. Ele retorna uma string que corresponde a um listener — sem dicionários de mapeamento, sem funções de roteamento separadas. A lógica de ramificação parece um `if` em Python porque *é* um.
|
||||
|
||||
---
|
||||
|
||||
## Demo 3: Integrando Crews de Agentes de IA em Flows
|
||||
|
||||
É aqui que o verdadeiro poder do CrewAI aparece. Flows não servem apenas para encadear chamadas de LLM — elas orquestram **Crews** completas de agentes autônomos. Isso é algo para o qual o LangGraph simplesmente não tem um equivalente nativo.
|
||||
|
||||
```python
|
||||
from crewai import Agent, Task, Crew
|
||||
from crewai.flow.flow import Flow, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
class ArticleState(BaseModel):
|
||||
topic: str = ""
|
||||
research: str = ""
|
||||
draft: str = ""
|
||||
final_article: str = ""
|
||||
|
||||
class ArticleFlow(Flow[ArticleState]):
|
||||
|
||||
@start()
|
||||
def run_research_crew(self):
|
||||
"""A full Crew of agents handles research."""
|
||||
researcher = Agent(
|
||||
role="Senior Research Analyst",
|
||||
goal=f"Produce comprehensive research on: {self.state.topic}",
|
||||
backstory="You're a veteran analyst known for thorough, "
|
||||
"well-sourced research reports.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
research_task = Task(
|
||||
description=f"Research '{self.state.topic}' thoroughly. "
|
||||
"Cover key trends, data points, and expert opinions.",
|
||||
expected_output="A detailed research brief with sources.",
|
||||
agent=researcher
|
||||
)
|
||||
|
||||
crew = Crew(agents=[researcher], tasks=[research_task])
|
||||
result = crew.kickoff()
|
||||
self.state.research = result.raw
|
||||
return result.raw
|
||||
|
||||
@listen(run_research_crew)
|
||||
def run_writing_crew(self, research_output):
|
||||
"""A different Crew handles writing."""
|
||||
writer = Agent(
|
||||
role="Technical Writer",
|
||||
goal="Write a compelling article based on provided research.",
|
||||
backstory="You turn complex research into engaging, clear prose.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
editor = Agent(
|
||||
role="Senior Editor",
|
||||
goal="Review and polish articles for publication quality.",
|
||||
backstory="20 years of editorial experience at top tech publications.",
|
||||
llm="gpt-4o"
|
||||
)
|
||||
|
||||
write_task = Task(
|
||||
description=f"Write an article based on this research:\n{self.state.research}",
|
||||
expected_output="A well-structured draft article.",
|
||||
agent=writer
|
||||
)
|
||||
|
||||
edit_task = Task(
|
||||
description="Review, fact-check, and polish the draft article.",
|
||||
expected_output="A publication-ready article.",
|
||||
agent=editor
|
||||
)
|
||||
|
||||
crew = Crew(agents=[writer, editor], tasks=[write_task, edit_task])
|
||||
result = crew.kickoff()
|
||||
self.state.final_article = result.raw
|
||||
return result.raw
|
||||
|
||||
# Run the full pipeline
|
||||
flow = ArticleFlow()
|
||||
flow.state.topic = "The Future of Edge AI"
|
||||
flow.kickoff()
|
||||
print(flow.state.final_article)
|
||||
```
|
||||
|
||||
Este é o insight-chave: **Flows fornecem a camada de orquestração, e Crews fornecem a camada de inteligência.** Cada etapa em um Flow pode subir uma equipe completa de agentes colaborativos, cada um com seus próprios papéis, objetivos e ferramentas. Você obtém fluxo de controle estruturado e previsível *e* colaboração autônoma de agentes — o melhor dos dois mundos.
|
||||
|
||||
No LangGraph, alcançar algo similar significa implementar manualmente protocolos de comunicação entre agentes, loops de chamada de ferramentas e lógica de delegação dentro das funções dos nós. É possível, mas é encanamento que você constrói do zero todas as vezes.
|
||||
|
||||
---
|
||||
|
||||
## Demo 4: Execução Paralela e Sincronização
|
||||
|
||||
Pipelines do mundo real frequentemente precisam dividir o trabalho e juntar os resultados. O CrewAI Flows lida com isso de forma elegante com os operadores `and_` e `or_`.
|
||||
|
||||
```python
|
||||
from crewai import LLM
|
||||
from crewai.flow.flow import Flow, and_, listen, start
|
||||
from pydantic import BaseModel
|
||||
|
||||
llm = LLM(model="openai/gpt-5.2")
|
||||
|
||||
class AnalysisState(BaseModel):
|
||||
topic: str = ""
|
||||
market_data: str = ""
|
||||
tech_analysis: str = ""
|
||||
competitor_intel: str = ""
|
||||
final_report: str = ""
|
||||
|
||||
class ParallelAnalysisFlow(Flow[AnalysisState]):
|
||||
@start()
|
||||
def start_method(self):
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_market_data(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def run_tech_analysis(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(start_method)
|
||||
def gather_competitor_intel(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
@listen(and_(gather_market_data, run_tech_analysis, gather_competitor_intel))
|
||||
def synthesize_report(self):
|
||||
# Your agentic or deterministic code
|
||||
pass
|
||||
|
||||
flow = ParallelAnalysisFlow()
|
||||
flow.state.topic = "AI-powered developer tools"
|
||||
flow.kickoff()
|
||||
|
||||
```
|
||||
|
||||
Vários decorators `@start()` disparam em paralelo. O combinador `and_()` no decorator `@listen` garante que `synthesize_report` só execute depois que *todos os três* métodos upstream forem concluídos. Também existe `or_()` para quando você quer prosseguir assim que *qualquer* tarefa upstream terminar.
|
||||
|
||||
No LangGraph, você precisaria construir um padrão fan-out/fan-in com ramificações paralelas, um nó de sincronização e uma mesclagem de estado cuidadosa — tudo conectado explicitamente por arestas.
|
||||
|
||||
---
|
||||
|
||||
## Por que CrewAI Flows em Produção
|
||||
|
||||
Além de uma sintaxe mais limpa, Flows entrega várias vantagens críticas para produção:
|
||||
|
||||
**Persistência de estado integrada.** O estado do Flow é respaldado pelo LanceDB, o que significa que seus workflows podem sobreviver a falhas, ser retomados e acumular conhecimento entre execuções. No LangGraph, você precisa configurar um checkpointer separado.
|
||||
|
||||
**Gerenciamento de estado com segurança de tipos.** Modelos Pydantic oferecem validação, serialização e suporte de IDE prontos para uso. Estados `TypedDict` do LangGraph não validam em runtime.
|
||||
|
||||
**Orquestração de agentes de primeira classe.** Crews são um primitivo nativo. Você define agentes com papéis, objetivos, histórias e ferramentas — e eles colaboram de forma autônoma dentro do envelope estruturado de um Flow. Não é preciso reinventar a coordenação multiagente.
|
||||
|
||||
**Modelo mental mais simples.** Decorators declaram intenção. `@start` significa "comece aqui". `@listen(x)` significa "execute depois de x". `@router(x)` significa "decida para onde ir depois de x". O código lê como o workflow que ele descreve.
|
||||
|
||||
**Integração com CLI.** Execute flows com `crewai run`. Sem etapa de compilação separada, sem serialização de grafo. Seu Flow é uma classe Python, e ele roda como tal.
|
||||
|
||||
---
|
||||
|
||||
## Cheat Sheet de Migração
|
||||
|
||||
Se você está com uma base de código LangGraph e quer migrar para o CrewAI Flows, aqui vai um guia prático de conversão:
|
||||
|
||||
1. **Mapeie seu estado.** Converta seu `TypedDict` para um `BaseModel` do Pydantic. Adicione valores padrão para todos os campos.
|
||||
2. **Converta nós em métodos.** Cada função de `add_node` vira um método na sua subclasse de `Flow`. Substitua leituras `state["field"]` por `self.state.field`.
|
||||
3. **Substitua arestas por decorators.** `add_edge(START, "first_node")` vira `@start()` no primeiro método. A sequência `add_edge("a", "b")` vira `@listen(a)` no método `b`.
|
||||
4. **Substitua arestas condicionais por `@router`.** A função de roteamento e o mapeamento do `add_conditional_edges()` viram um único método `@router()` que retorna a string de rota.
|
||||
5. **Troque compile + invoke por kickoff.** Remova `graph.compile()`. Chame `flow.kickoff()`.
|
||||
6. **Considere onde as Crews se encaixam.** Qualquer nó com lógica complexa de agentes em múltiplas etapas é um candidato a extração para uma Crew. É aqui que você verá a maior melhoria de qualidade.
|
||||
|
||||
---
|
||||
|
||||
## Primeiros Passos
|
||||
|
||||
Instale o CrewAI e crie o scaffold de um novo projeto Flow:
|
||||
|
||||
```bash
|
||||
pip install crewai
|
||||
crewai create flow my_first_flow
|
||||
cd my_first_flow
|
||||
```
|
||||
|
||||
Isso gera uma estrutura de projeto com uma classe Flow pronta para edição, arquivos de configuração e um `pyproject.toml` com `type = "flow"` já definido. Execute com:
|
||||
|
||||
```bash
|
||||
crewai run
|
||||
```
|
||||
|
||||
A partir daí, adicione seus agentes, conecte seus listeners e publique.
|
||||
|
||||
---
|
||||
|
||||
## Considerações Finais
|
||||
|
||||
O LangGraph ensinou ao ecossistema que workflows de IA precisam de estrutura. Essa foi uma lição importante. Mas o CrewAI Flows pega essa lição e a entrega de um jeito mais rápido de escrever, mais fácil de ler e mais poderoso em produção — especialmente quando seus workflows envolvem múltiplos agentes colaborando.
|
||||
|
||||
Se você está construindo algo além de uma cadeia de agente único, dê uma olhada séria no Flows. O modelo baseado em decorators, a integração nativa com Crews e o gerenciamento de estado embutido significam menos tempo com encanamento e mais tempo nos problemas que importam.
|
||||
|
||||
Comece com `crewai create flow`. Você não vai olhar para trás.
|
||||
@@ -9,7 +9,7 @@ authors = [
|
||||
requires-python = ">=3.10, <3.14"
|
||||
dependencies = [
|
||||
"Pillow~=12.1.1",
|
||||
"pypdf~=6.7.4",
|
||||
"pypdf~=6.7.5",
|
||||
"python-magic>=0.4.27",
|
||||
"aiocache~=0.12.3",
|
||||
"aiofiles~=24.1.0",
|
||||
|
||||
@@ -152,4 +152,4 @@ __all__ = [
|
||||
"wrap_file_source",
|
||||
]
|
||||
|
||||
__version__ = "1.10.1a1"
|
||||
__version__ = "1.10.1"
|
||||
|
||||
@@ -11,7 +11,7 @@ dependencies = [
|
||||
"pytube~=15.0.0",
|
||||
"requests~=2.32.5",
|
||||
"docker~=7.1.0",
|
||||
"crewai==1.10.1a1",
|
||||
"crewai==1.10.1",
|
||||
"tiktoken~=0.8.0",
|
||||
"beautifulsoup4~=4.13.4",
|
||||
"python-docx~=1.2.0",
|
||||
|
||||
@@ -291,4 +291,4 @@ __all__ = [
|
||||
"ZapierActionTools",
|
||||
]
|
||||
|
||||
__version__ = "1.10.1a1"
|
||||
__version__ = "1.10.1"
|
||||
|
||||
@@ -10,6 +10,7 @@ from pydantic import BaseModel, Field
|
||||
from pydantic.types import StringConstraints
|
||||
import requests
|
||||
|
||||
|
||||
load_dotenv()
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
|
||||
from crewai import Agent, Crew, Task
|
||||
from multion_tool import MultiOnTool # type: ignore[import-not-found]
|
||||
from multion_tool import MultiOnTool # type: ignore[import-not-found]
|
||||
|
||||
|
||||
os.environ["OPENAI_API_KEY"] = "Your Key"
|
||||
|
||||
@@ -17,11 +17,11 @@ Usage:
|
||||
|
||||
import os
|
||||
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai.utilities.printer import Printer
|
||||
from dotenv import load_dotenv
|
||||
from stagehand.schemas import AvailableModel # type: ignore[import-untyped]
|
||||
|
||||
from crewai import Agent, Crew, Process, Task
|
||||
from crewai_tools import StagehandTool
|
||||
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ dependencies = [
|
||||
"opentelemetry-exporter-otlp-proto-http~=1.34.0",
|
||||
# Data Handling
|
||||
"chromadb~=1.1.0",
|
||||
"tokenizers~=0.20.3",
|
||||
"tokenizers>=0.21,<1",
|
||||
"openpyxl~=3.1.5",
|
||||
# Authentication and Security
|
||||
"python-dotenv~=1.1.1",
|
||||
@@ -53,7 +53,7 @@ Repository = "https://github.com/crewAIInc/crewAI"
|
||||
|
||||
[project.optional-dependencies]
|
||||
tools = [
|
||||
"crewai-tools==1.10.1a1",
|
||||
"crewai-tools==1.10.1",
|
||||
]
|
||||
embeddings = [
|
||||
"tiktoken~=0.8.0"
|
||||
@@ -88,7 +88,7 @@ bedrock = [
|
||||
"boto3~=1.40.45",
|
||||
]
|
||||
google-genai = [
|
||||
"google-genai~=1.49.0",
|
||||
"google-genai~=1.65.0",
|
||||
]
|
||||
azure-ai-inference = [
|
||||
"azure-ai-inference~=1.0.0b9",
|
||||
|
||||
@@ -40,7 +40,7 @@ def _suppress_pydantic_deprecation_warnings() -> None:
|
||||
|
||||
_suppress_pydantic_deprecation_warnings()
|
||||
|
||||
__version__ = "1.10.1a1"
|
||||
__version__ = "1.10.1"
|
||||
_telemetry_submitted = False
|
||||
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import MutableMapping
|
||||
import concurrent.futures
|
||||
from functools import lru_cache
|
||||
import ssl
|
||||
import time
|
||||
@@ -138,14 +139,17 @@ def fetch_agent_card(
|
||||
ttl_hash = int(time.time() // cache_ttl)
|
||||
return _fetch_agent_card_cached(endpoint, auth_hash, timeout, ttl_hash)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
coro = afetch_agent_card(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
afetch_agent_card(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
asyncio.get_running_loop()
|
||||
has_running_loop = True
|
||||
except RuntimeError:
|
||||
has_running_loop = False
|
||||
|
||||
if has_running_loop:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
return pool.submit(asyncio.run, coro).result()
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
async def afetch_agent_card(
|
||||
@@ -203,14 +207,17 @@ def _fetch_agent_card_cached(
|
||||
"""Cached sync version of fetch_agent_card."""
|
||||
auth = _auth_store.get(auth_hash)
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
coro = _afetch_agent_card_impl(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
_afetch_agent_card_impl(endpoint=endpoint, auth=auth, timeout=timeout)
|
||||
)
|
||||
finally:
|
||||
loop.close()
|
||||
asyncio.get_running_loop()
|
||||
has_running_loop = True
|
||||
except RuntimeError:
|
||||
has_running_loop = False
|
||||
|
||||
if has_running_loop:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
return pool.submit(asyncio.run, coro).result()
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
@cached(ttl=300, serializer=PickleSerializer()) # type: ignore[untyped-decorator]
|
||||
|
||||
@@ -5,6 +5,7 @@ from __future__ import annotations
|
||||
import asyncio
|
||||
import base64
|
||||
from collections.abc import AsyncIterator, Callable, MutableMapping
|
||||
import concurrent.futures
|
||||
from contextlib import asynccontextmanager
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Final, Literal
|
||||
@@ -194,56 +195,43 @@ def execute_a2a_delegation(
|
||||
|
||||
Returns:
|
||||
TaskStateResult with status, result/error, history, and agent_card.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If called from an async context with a running event loop.
|
||||
"""
|
||||
coro = aexecute_a2a_delegation(
|
||||
endpoint=endpoint,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
task_description=task_description,
|
||||
context=context,
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=metadata,
|
||||
extensions=extensions,
|
||||
conversation_history=conversation_history,
|
||||
agent_id=agent_id,
|
||||
agent_role=agent_role,
|
||||
agent_branch=agent_branch,
|
||||
response_model=response_model,
|
||||
turn_number=turn_number,
|
||||
updates=updates,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
skill_id=skill_id,
|
||||
client_extensions=client_extensions,
|
||||
transport=transport,
|
||||
accepted_output_modes=accepted_output_modes,
|
||||
input_files=input_files,
|
||||
)
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
raise RuntimeError(
|
||||
"execute_a2a_delegation() cannot be called from an async context. "
|
||||
"Use 'await aexecute_a2a_delegation()' instead."
|
||||
)
|
||||
except RuntimeError as e:
|
||||
if "no running event loop" not in str(e).lower():
|
||||
raise
|
||||
has_running_loop = True
|
||||
except RuntimeError:
|
||||
has_running_loop = False
|
||||
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
try:
|
||||
return loop.run_until_complete(
|
||||
aexecute_a2a_delegation(
|
||||
endpoint=endpoint,
|
||||
auth=auth,
|
||||
timeout=timeout,
|
||||
task_description=task_description,
|
||||
context=context,
|
||||
context_id=context_id,
|
||||
task_id=task_id,
|
||||
reference_task_ids=reference_task_ids,
|
||||
metadata=metadata,
|
||||
extensions=extensions,
|
||||
conversation_history=conversation_history,
|
||||
agent_id=agent_id,
|
||||
agent_role=agent_role,
|
||||
agent_branch=agent_branch,
|
||||
response_model=response_model,
|
||||
turn_number=turn_number,
|
||||
updates=updates,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
skill_id=skill_id,
|
||||
client_extensions=client_extensions,
|
||||
transport=transport,
|
||||
accepted_output_modes=accepted_output_modes,
|
||||
input_files=input_files,
|
||||
)
|
||||
)
|
||||
finally:
|
||||
try:
|
||||
loop.run_until_complete(loop.shutdown_asyncgens())
|
||||
finally:
|
||||
loop.close()
|
||||
if has_running_loop:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool:
|
||||
return pool.submit(asyncio.run, coro).result()
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
async def aexecute_a2a_delegation(
|
||||
|
||||
@@ -1156,11 +1156,15 @@ class Agent(BaseAgent):
|
||||
# Process platform apps and MCP tools
|
||||
if self.apps:
|
||||
platform_tools = self.get_platform_tools(self.apps)
|
||||
if platform_tools and self.tools is not None:
|
||||
if platform_tools:
|
||||
if self.tools is None:
|
||||
self.tools = []
|
||||
self.tools.extend(platform_tools)
|
||||
if self.mcps:
|
||||
mcps = self.get_mcp_tools(self.mcps)
|
||||
if mcps and self.tools is not None:
|
||||
if mcps:
|
||||
if self.tools is None:
|
||||
self.tools = []
|
||||
self.tools.extend(mcps)
|
||||
|
||||
# Prepare tools
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from crewai.agents.cache.cache_handler import CacheHandler
|
||||
|
||||
|
||||
|
||||
__all__ = ["CacheHandler"]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from crewai.cli.authentication.main import AuthenticationCommand
|
||||
|
||||
|
||||
|
||||
__all__ = ["AuthenticationCommand"]
|
||||
|
||||
@@ -143,7 +143,7 @@ def create_folder_structure(
|
||||
(folder_path / "src" / folder_name).mkdir(parents=True)
|
||||
(folder_path / "src" / folder_name / "tools").mkdir(parents=True)
|
||||
(folder_path / "src" / folder_name / "config").mkdir(parents=True)
|
||||
|
||||
|
||||
# Copy AGENTS.md to project root (top-level projects only)
|
||||
package_dir = Path(__file__).parent
|
||||
agents_md_src = package_dir / "templates" / "AGENTS.md"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
|
||||
import click
|
||||
|
||||
|
||||
@@ -22,14 +22,15 @@ class PlusAPI:
|
||||
EPHEMERAL_TRACING_RESOURCE = "/crewai_plus/api/v1/tracing/ephemeral"
|
||||
INTEGRATIONS_RESOURCE = "/crewai_plus/api/v1/integrations"
|
||||
|
||||
def __init__(self, api_key: str) -> None:
|
||||
def __init__(self, api_key: str | None = None) -> None:
|
||||
self.api_key = api_key
|
||||
self.headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"User-Agent": f"CrewAI-CLI/{get_crewai_version()}",
|
||||
"X-Crewai-Version": get_crewai_version(),
|
||||
}
|
||||
if api_key:
|
||||
self.headers["Authorization"] = f"Bearer {api_key}"
|
||||
settings = Settings()
|
||||
if settings.org_uuid:
|
||||
self.headers["X-Crewai-Organization-Id"] = settings.org_uuid
|
||||
@@ -48,8 +49,13 @@ class PlusAPI:
|
||||
with httpx.Client(trust_env=False, verify=verify) as client:
|
||||
return client.request(method, url, headers=self.headers, **kwargs)
|
||||
|
||||
def login_to_tool_repository(self) -> httpx.Response:
|
||||
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login")
|
||||
def login_to_tool_repository(
|
||||
self, user_identifier: str | None = None
|
||||
) -> httpx.Response:
|
||||
payload = {}
|
||||
if user_identifier:
|
||||
payload["user_identifier"] = user_identifier
|
||||
return self._make_request("POST", f"{self.TOOLS_RESOURCE}/login", json=payload)
|
||||
|
||||
def get_tool(self, handle: str) -> httpx.Response:
|
||||
return self._make_request("GET", f"{self.TOOLS_RESOURCE}/{handle}")
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.1a1"
|
||||
"crewai[tools]==1.10.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "{{name}} using crewAI"
|
||||
authors = [{ name = "Your Name", email = "you@example.com" }]
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.1a1"
|
||||
"crewai[tools]==1.10.1"
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
@@ -5,7 +5,7 @@ description = "Power up your crews with {{folder_name}}"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10,<3.14"
|
||||
dependencies = [
|
||||
"crewai[tools]==1.10.1a1"
|
||||
"crewai[tools]==1.10.1"
|
||||
]
|
||||
|
||||
[tool.crewai]
|
||||
|
||||
@@ -23,6 +23,7 @@ from crewai.cli.utils import (
|
||||
tree_copy,
|
||||
tree_find_and_replace,
|
||||
)
|
||||
from crewai.events.listeners.tracing.utils import get_user_id
|
||||
|
||||
|
||||
console = Console()
|
||||
@@ -169,7 +170,9 @@ class ToolCommand(BaseCommand, PlusAPIMixin):
|
||||
console.print(f"Successfully installed {handle}", style="bold green")
|
||||
|
||||
def login(self) -> None:
|
||||
login_response = self.plus_api_client.login_to_tool_repository()
|
||||
login_response = self.plus_api_client.login_to_tool_repository(
|
||||
user_identifier=get_user_id()
|
||||
)
|
||||
|
||||
if login_response.status_code != 200:
|
||||
console.print(
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from crewai.crews.crew_output import CrewOutput
|
||||
|
||||
|
||||
|
||||
__all__ = ["CrewOutput"]
|
||||
|
||||
@@ -23,4 +23,3 @@ class BaseEventListener(ABC):
|
||||
Args:
|
||||
crewai_event_bus: The event bus to register listeners on.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -15,6 +15,7 @@ from crewai.cli.plus_api import PlusAPI
|
||||
from crewai.cli.version import get_crewai_version
|
||||
from crewai.events.listeners.tracing.types import TraceEvent
|
||||
from crewai.events.listeners.tracing.utils import (
|
||||
get_user_id,
|
||||
is_tracing_enabled_in_context,
|
||||
should_auto_collect_first_time_traces,
|
||||
)
|
||||
@@ -67,7 +68,7 @@ class TraceBatchManager:
|
||||
api_key=get_auth_token(),
|
||||
)
|
||||
except AuthError:
|
||||
self.plus_api = PlusAPI(api_key="")
|
||||
self.plus_api = PlusAPI()
|
||||
self.ephemeral_trace_url = None
|
||||
|
||||
def initialize_batch(
|
||||
@@ -120,7 +121,6 @@ class TraceBatchManager:
|
||||
payload = {
|
||||
"trace_id": self.current_batch.batch_id,
|
||||
"execution_type": execution_metadata.get("execution_type", "crew"),
|
||||
"user_identifier": execution_metadata.get("user_context", None),
|
||||
"execution_context": {
|
||||
"crew_fingerprint": execution_metadata.get("crew_fingerprint"),
|
||||
"crew_name": execution_metadata.get("crew_name", None),
|
||||
@@ -140,6 +140,7 @@ class TraceBatchManager:
|
||||
}
|
||||
if use_ephemeral:
|
||||
payload["ephemeral_trace_id"] = self.current_batch.batch_id
|
||||
payload["user_identifier"] = get_user_id()
|
||||
|
||||
response = (
|
||||
self.plus_api.initialize_ephemeral_trace_batch(payload)
|
||||
|
||||
@@ -86,3 +86,11 @@ class LLMStreamChunkEvent(LLMEventBase):
|
||||
tool_call: ToolCall | None = None
|
||||
call_type: LLMCallType | None = None
|
||||
response_id: str | None = None
|
||||
|
||||
|
||||
class LLMThinkingChunkEvent(LLMEventBase):
|
||||
"""Event emitted when a thinking/reasoning chunk is received from a thinking model"""
|
||||
|
||||
type: str = "llm_thinking_chunk"
|
||||
chunk: str
|
||||
response_id: str | None = None
|
||||
|
||||
@@ -302,6 +302,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin):
|
||||
super().__init__(
|
||||
suppress_flow_events=True,
|
||||
tracing=current_tracing if current_tracing else None,
|
||||
max_method_calls=self.max_iter * 10,
|
||||
)
|
||||
self._flow_initialized = True
|
||||
|
||||
@@ -403,7 +404,7 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin):
|
||||
self._setup_native_tools()
|
||||
return "initialized"
|
||||
|
||||
@listen("force_final_answer")
|
||||
@listen("max_iterations_exceeded")
|
||||
def force_final_answer(self) -> Literal["agent_finished"]:
|
||||
"""Force agent to provide final answer when max iterations exceeded."""
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
@@ -655,11 +656,11 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin):
|
||||
return "tool_result_is_final"
|
||||
|
||||
reasoning_prompt = self._i18n.slice("post_tool_reasoning")
|
||||
reasoning_message: LLMMessage = {
|
||||
reasoning_message_post: LLMMessage = {
|
||||
"role": "user",
|
||||
"content": reasoning_prompt,
|
||||
}
|
||||
self.state.messages.append(reasoning_message)
|
||||
self.state.messages.append(reasoning_message_post)
|
||||
|
||||
return "tool_completed"
|
||||
|
||||
@@ -886,9 +887,10 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin):
|
||||
call_id, func_name, func_args = info
|
||||
|
||||
# Parse arguments
|
||||
args_dict, parse_error = parse_tool_call_args(func_args, func_name, call_id)
|
||||
parsed_args, parse_error = parse_tool_call_args(func_args, func_name, call_id)
|
||||
if parse_error is not None:
|
||||
return parse_error
|
||||
args_dict: dict[str, Any] = parsed_args or {}
|
||||
|
||||
# Get agent_key for event tracking
|
||||
agent_key = getattr(self.agent, "key", "unknown") if self.agent else "unknown"
|
||||
@@ -1107,11 +1109,11 @@ class AgentExecutor(Flow[AgentReActState], CrewAgentExecutorMixin):
|
||||
def check_max_iterations(
|
||||
self,
|
||||
) -> Literal[
|
||||
"force_final_answer", "continue_reasoning", "continue_reasoning_native"
|
||||
"max_iterations_exceeded", "continue_reasoning", "continue_reasoning_native"
|
||||
]:
|
||||
"""Check if max iterations reached before proceeding with reasoning."""
|
||||
if has_reached_max_iterations(self.state.iterations, self.max_iter):
|
||||
return "force_final_answer"
|
||||
return "max_iterations_exceeded"
|
||||
if self.state.use_native_tools:
|
||||
return "continue_reasoning_native"
|
||||
return "continue_reasoning"
|
||||
|
||||
@@ -692,6 +692,7 @@ class FlowMeta(type):
|
||||
condition_type = getattr(
|
||||
attr_value, "__condition_type__", OR_CONDITION
|
||||
)
|
||||
|
||||
if (
|
||||
hasattr(attr_value, "__trigger_condition__")
|
||||
and attr_value.__trigger_condition__ is not None
|
||||
@@ -769,6 +770,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
persistence: FlowPersistence | None = None,
|
||||
tracing: bool | None = None,
|
||||
suppress_flow_events: bool = False,
|
||||
max_method_calls: int = 100,
|
||||
**kwargs: Any,
|
||||
) -> None:
|
||||
"""Initialize a new Flow instance.
|
||||
@@ -777,6 +779,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
persistence: Optional persistence backend for storing flow states
|
||||
tracing: Whether to enable tracing. True=always enable, False=always disable, None=check environment/user settings
|
||||
suppress_flow_events: Whether to suppress flow event emissions (internal use)
|
||||
max_method_calls: Maximum times a single method can be called per execution before raising RecursionError
|
||||
**kwargs: Additional state values to initialize or override
|
||||
"""
|
||||
# Initialize basic instance attributes
|
||||
@@ -792,6 +795,8 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self._completed_methods: set[FlowMethodName] = (
|
||||
set()
|
||||
) # Track completed methods for reload
|
||||
self._method_call_counts: dict[FlowMethodName, int] = {}
|
||||
self._max_method_calls = max_method_calls
|
||||
self._persistence: FlowPersistence | None = persistence
|
||||
self._is_execution_resuming: bool = False
|
||||
self._event_futures: list[Future[None]] = []
|
||||
@@ -1828,6 +1833,7 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
self._method_outputs.clear()
|
||||
self._pending_and_listeners.clear()
|
||||
self._clear_or_listeners()
|
||||
self._method_call_counts.clear()
|
||||
else:
|
||||
# Only enter resumption mode if there are completed methods to
|
||||
# replay. When _completed_methods is empty (e.g. a pure
|
||||
@@ -2569,6 +2575,16 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
||||
- Skips execution if method was already completed (e.g., after reload)
|
||||
- Catches and logs any exceptions during execution, preventing individual listener failures from breaking the entire flow
|
||||
"""
|
||||
count = self._method_call_counts.get(listener_name, 0) + 1
|
||||
if count > self._max_method_calls:
|
||||
raise RecursionError(
|
||||
f"Method '{listener_name}' has been called {self._max_method_calls} times in "
|
||||
f"this flow execution, which indicates an infinite loop. "
|
||||
f"This commonly happens when a @listen label matches the "
|
||||
f"method's own name."
|
||||
)
|
||||
self._method_call_counts[listener_name] = count
|
||||
|
||||
if listener_name in self._completed_methods:
|
||||
if self._is_execution_resuming:
|
||||
# During resumption, skip execution but continue listeners
|
||||
|
||||
@@ -69,9 +69,7 @@ from crewai.llms.base_llm import BaseLLM
|
||||
from crewai.tools.base_tool import BaseTool
|
||||
from crewai.tools.structured_tool import CrewStructuredTool
|
||||
from crewai.utilities.agent_utils import (
|
||||
convert_tools_to_openai_schema,
|
||||
enforce_rpm_limit,
|
||||
extract_tool_call_info,
|
||||
format_message_for_llm,
|
||||
get_llm_response,
|
||||
get_tool_names,
|
||||
@@ -82,7 +80,6 @@ from crewai.utilities.agent_utils import (
|
||||
handle_unknown_error,
|
||||
has_reached_max_iterations,
|
||||
is_context_length_exceeded,
|
||||
parse_tool_call_args,
|
||||
parse_tools,
|
||||
process_llm_response,
|
||||
render_text_description_and_args,
|
||||
@@ -91,7 +88,6 @@ from crewai.utilities.converter import (
|
||||
Converter,
|
||||
ConverterError,
|
||||
)
|
||||
from crewai.utilities.string_utils import sanitize_tool_name
|
||||
from crewai.utilities.guardrail import process_guardrail
|
||||
from crewai.utilities.guardrail_types import GuardrailCallable, GuardrailType
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
@@ -278,7 +274,6 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
_printer: Printer = PrivateAttr(default_factory=Printer)
|
||||
_guardrail: GuardrailCallable | None = PrivateAttr(default=None)
|
||||
_guardrail_retry_count: int = PrivateAttr(default=0)
|
||||
_use_native_tools: bool = PrivateAttr(default=False)
|
||||
_callbacks: list[TokenCalcHandler] = PrivateAttr(default_factory=list)
|
||||
_before_llm_call_hooks: list[BeforeLLMCallHookType | BeforeLLMCallHookCallable] = (
|
||||
PrivateAttr(default_factory=get_before_llm_call_hooks)
|
||||
@@ -522,16 +517,6 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
self._iterations = 0
|
||||
self.tools_results = []
|
||||
|
||||
# Determine execution mode before building the system prompt so
|
||||
# native mode gets a clean prompt without ReAct format instructions.
|
||||
llm = cast(LLM, self.llm)
|
||||
self._use_native_tools = bool(
|
||||
hasattr(llm, "supports_function_calling")
|
||||
and callable(getattr(llm, "supports_function_calling", None))
|
||||
and llm.supports_function_calling()
|
||||
and self._parsed_tools
|
||||
)
|
||||
|
||||
# Format messages for the LLM
|
||||
self._messages = self._format_messages(
|
||||
messages, response_format=response_format, input_files=input_files
|
||||
@@ -808,18 +793,9 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
response_format: Optional response format to use instead of self.response_format
|
||||
"""
|
||||
base_prompt = ""
|
||||
if self._parsed_tools and self._use_native_tools:
|
||||
base_prompt = self.i18n.slice(
|
||||
"lite_agent_system_prompt_native_tools"
|
||||
).format(
|
||||
role=self.role,
|
||||
backstory=self.backstory,
|
||||
goal=self.goal,
|
||||
)
|
||||
elif self._parsed_tools:
|
||||
base_prompt = self.i18n.slice(
|
||||
"lite_agent_system_prompt_with_tools"
|
||||
).format(
|
||||
if self._parsed_tools:
|
||||
# Use the prompt template for agents with tools
|
||||
base_prompt = self.i18n.slice("lite_agent_system_prompt_with_tools").format(
|
||||
role=self.role,
|
||||
backstory=self.backstory,
|
||||
goal=self.goal,
|
||||
@@ -827,6 +803,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
tool_names=get_tool_names(self._parsed_tools),
|
||||
)
|
||||
else:
|
||||
# Use the prompt template for agents without tools
|
||||
base_prompt = self.i18n.slice(
|
||||
"lite_agent_system_prompt_without_tools"
|
||||
).format(
|
||||
@@ -883,501 +860,8 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
def _invoke_loop(
|
||||
self, response_model: type[BaseModel] | None = None
|
||||
) -> AgentFinish:
|
||||
"""Run the agent's thought process until it reaches a conclusion or max iterations.
|
||||
|
||||
Checks if the LLM supports native function calling and uses that
|
||||
approach if available, otherwise falls back to the ReAct text pattern.
|
||||
|
||||
Args:
|
||||
response_model: Optional Pydantic model for native structured output.
|
||||
|
||||
Returns:
|
||||
AgentFinish: The final result of the agent execution.
|
||||
"""
|
||||
if self._use_native_tools:
|
||||
return self._invoke_loop_native_tools(response_model=response_model)
|
||||
|
||||
return self._invoke_loop_react(response_model=response_model)
|
||||
|
||||
def _invoke_loop_native_tools(
|
||||
self, response_model: type[BaseModel] | None = None
|
||||
) -> AgentFinish:
|
||||
"""Execute agent loop using native function calling.
|
||||
|
||||
Uses the LLM's native tool/function calling capability instead of the
|
||||
text-based ReAct pattern. The LLM directly returns structured tool
|
||||
calls which are executed and results fed back.
|
||||
|
||||
Args:
|
||||
response_model: Optional Pydantic model for native structured output.
|
||||
|
||||
Returns:
|
||||
AgentFinish: The final result of the agent execution.
|
||||
"""
|
||||
openai_tools, available_functions, original_tools_by_name = (
|
||||
convert_tools_to_openai_schema(self.tools)
|
||||
)
|
||||
|
||||
while True:
|
||||
try:
|
||||
if has_reached_max_iterations(self._iterations, self.max_iterations):
|
||||
formatted_answer = handle_max_iterations_exceeded(
|
||||
None,
|
||||
printer=self._printer,
|
||||
i18n=self.i18n,
|
||||
messages=self._messages,
|
||||
llm=cast(LLM, self.llm),
|
||||
callbacks=self._callbacks,
|
||||
verbose=self.verbose,
|
||||
)
|
||||
self._show_logs(formatted_answer)
|
||||
return formatted_answer
|
||||
|
||||
enforce_rpm_limit(self.request_within_rpm_limit)
|
||||
|
||||
answer = get_llm_response(
|
||||
llm=cast(LLM, self.llm),
|
||||
messages=self._messages,
|
||||
callbacks=self._callbacks,
|
||||
printer=self._printer,
|
||||
tools=openai_tools,
|
||||
available_functions=None,
|
||||
from_agent=self,
|
||||
executor_context=self,
|
||||
response_model=response_model,
|
||||
verbose=self.verbose,
|
||||
)
|
||||
|
||||
if (
|
||||
isinstance(answer, list)
|
||||
and answer
|
||||
and self._is_tool_call_list(answer)
|
||||
):
|
||||
tool_finish = self._handle_native_tool_calls(
|
||||
answer, available_functions, original_tools_by_name
|
||||
)
|
||||
if tool_finish is not None:
|
||||
return tool_finish
|
||||
continue
|
||||
|
||||
if isinstance(answer, BaseModel):
|
||||
output_json = answer.model_dump_json()
|
||||
formatted_answer = AgentFinish(
|
||||
thought="", output=answer, text=output_json
|
||||
)
|
||||
self._append_message(output_json)
|
||||
self._show_logs(formatted_answer)
|
||||
return formatted_answer
|
||||
|
||||
answer_str = str(answer) if not isinstance(answer, str) else answer
|
||||
formatted_answer = AgentFinish(
|
||||
thought="", output=answer_str, text=answer_str
|
||||
)
|
||||
self._append_message(answer_str)
|
||||
self._show_logs(formatted_answer)
|
||||
return formatted_answer
|
||||
|
||||
except Exception as e:
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
raise e
|
||||
if is_context_length_exceeded(e):
|
||||
handle_context_length(
|
||||
respect_context_window=self.respect_context_window,
|
||||
printer=self._printer,
|
||||
messages=self._messages,
|
||||
llm=cast(LLM, self.llm),
|
||||
callbacks=self._callbacks,
|
||||
i18n=self.i18n,
|
||||
verbose=self.verbose,
|
||||
)
|
||||
continue
|
||||
handle_unknown_error(self._printer, e, verbose=self.verbose)
|
||||
raise e
|
||||
finally:
|
||||
self._iterations += 1
|
||||
|
||||
@staticmethod
|
||||
def _is_tool_call_list(response: list[Any]) -> bool:
|
||||
"""Check if a response is a list of native tool calls.
|
||||
|
||||
Supports OpenAI, Anthropic, Bedrock, and Gemini formats.
|
||||
"""
|
||||
if not response:
|
||||
return False
|
||||
first_item = response[0]
|
||||
if hasattr(first_item, "function") or (
|
||||
isinstance(first_item, dict) and "function" in first_item
|
||||
):
|
||||
return True
|
||||
if (
|
||||
hasattr(first_item, "type")
|
||||
and getattr(first_item, "type", None) == "tool_use"
|
||||
):
|
||||
return True
|
||||
if hasattr(first_item, "name") and hasattr(first_item, "input"):
|
||||
return True
|
||||
if (
|
||||
isinstance(first_item, dict)
|
||||
and "name" in first_item
|
||||
and "input" in first_item
|
||||
):
|
||||
return True
|
||||
if hasattr(first_item, "function_call") and first_item.function_call:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _handle_native_tool_calls(
|
||||
self,
|
||||
tool_calls: list[Any],
|
||||
available_functions: dict[str, Callable[..., Any]],
|
||||
original_tools_by_name: dict[str, BaseTool],
|
||||
) -> AgentFinish | None:
|
||||
"""Execute native tool calls and feed results back into message history.
|
||||
|
||||
Uses parallel execution via ``ThreadPoolExecutor`` when safe (no
|
||||
``result_as_answer`` or ``max_usage_count`` tools in the batch).
|
||||
Falls back to sequential execution otherwise.
|
||||
|
||||
Args:
|
||||
tool_calls: Tool call objects from the LLM response.
|
||||
available_functions: Mapping of sanitized tool names to callables.
|
||||
original_tools_by_name: Mapping of sanitized tool names to original
|
||||
BaseTool instances.
|
||||
|
||||
Returns:
|
||||
AgentFinish if a tool with result_as_answer=True was called,
|
||||
None otherwise (loop continues).
|
||||
"""
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
|
||||
parsed_calls = [
|
||||
parsed
|
||||
for tc in tool_calls
|
||||
if (parsed := extract_tool_call_info(tc)) is not None
|
||||
]
|
||||
if not parsed_calls:
|
||||
return None
|
||||
|
||||
# Single assistant message with all tool calls (matches OpenAI API spec)
|
||||
self._messages.append({
|
||||
"role": "assistant",
|
||||
"content": None,
|
||||
"tool_calls": [
|
||||
{
|
||||
"id": call_id,
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": func_name,
|
||||
"arguments": func_args
|
||||
if isinstance(func_args, str)
|
||||
else json.dumps(func_args),
|
||||
},
|
||||
}
|
||||
for call_id, func_name, func_args in parsed_calls
|
||||
],
|
||||
})
|
||||
|
||||
# Determine if parallel execution is safe for this batch.
|
||||
# Usage counters are not thread-safe, and result_as_answer requires
|
||||
# immediate return, so both force sequential execution.
|
||||
can_parallelize = len(parsed_calls) > 1 and not any(
|
||||
(
|
||||
original_tools_by_name.get(fn)
|
||||
and (
|
||||
getattr(original_tools_by_name.get(fn), "result_as_answer", False)
|
||||
or getattr(original_tools_by_name.get(fn), "max_usage_count", None)
|
||||
is not None
|
||||
)
|
||||
)
|
||||
for _, fn, _ in parsed_calls
|
||||
)
|
||||
|
||||
if can_parallelize:
|
||||
execution_plan = [
|
||||
(cid, fn, fa, original_tools_by_name.get(fn))
|
||||
for cid, fn, fa in parsed_calls
|
||||
]
|
||||
max_workers = min(8, len(execution_plan))
|
||||
ordered_results: list[dict[str, Any] | None] = [None] * len(
|
||||
execution_plan
|
||||
)
|
||||
with ThreadPoolExecutor(max_workers=max_workers) as pool:
|
||||
futures = {
|
||||
pool.submit(
|
||||
self._execute_native_tool_call,
|
||||
call_id=cid,
|
||||
func_name=fn,
|
||||
func_args=fa,
|
||||
available_functions=available_functions,
|
||||
original_tool=ot,
|
||||
): idx
|
||||
for idx, (cid, fn, fa, ot) in enumerate(execution_plan)
|
||||
}
|
||||
for future in as_completed(futures):
|
||||
ordered_results[futures[future]] = future.result()
|
||||
|
||||
for exec_result in ordered_results:
|
||||
if exec_result is None:
|
||||
continue
|
||||
self._messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": exec_result["call_id"],
|
||||
"name": exec_result["func_name"],
|
||||
"content": exec_result["result"],
|
||||
})
|
||||
if self.verbose:
|
||||
cache_tag = " (from cache)" if exec_result["from_cache"] else ""
|
||||
self._printer.print(
|
||||
content=f"Tool {exec_result['func_name']} executed{cache_tag}: {exec_result['result'][:200]}",
|
||||
color="green",
|
||||
)
|
||||
orig = original_tools_by_name.get(exec_result["func_name"])
|
||||
if orig and getattr(orig, "result_as_answer", False):
|
||||
finished = AgentFinish(
|
||||
thought="", output=exec_result["result"], text=exec_result["result"]
|
||||
)
|
||||
self._show_logs(finished)
|
||||
return finished
|
||||
else:
|
||||
# Sequential execution: process each call one at a time.
|
||||
for call_id, func_name, func_args in parsed_calls:
|
||||
exec_result = self._execute_native_tool_call(
|
||||
call_id=call_id,
|
||||
func_name=func_name,
|
||||
func_args=func_args,
|
||||
available_functions=available_functions,
|
||||
original_tool=original_tools_by_name.get(func_name),
|
||||
)
|
||||
|
||||
self._messages.append({
|
||||
"role": "tool",
|
||||
"tool_call_id": exec_result["call_id"],
|
||||
"name": exec_result["func_name"],
|
||||
"content": exec_result["result"],
|
||||
})
|
||||
if self.verbose:
|
||||
cache_tag = " (from cache)" if exec_result["from_cache"] else ""
|
||||
self._printer.print(
|
||||
content=f"Tool {exec_result['func_name']} executed{cache_tag}: {exec_result['result'][:200]}",
|
||||
color="green",
|
||||
)
|
||||
|
||||
original_tool = original_tools_by_name.get(func_name)
|
||||
if original_tool and getattr(original_tool, "result_as_answer", False):
|
||||
finished = AgentFinish(
|
||||
thought="", output=exec_result["result"], text=exec_result["result"]
|
||||
)
|
||||
self._show_logs(finished)
|
||||
return finished
|
||||
|
||||
reasoning_prompt = self.i18n.slice("post_tool_reasoning")
|
||||
self._messages.append({"role": "user", "content": reasoning_prompt})
|
||||
return None
|
||||
|
||||
def _execute_native_tool_call(
|
||||
self,
|
||||
*,
|
||||
call_id: str,
|
||||
func_name: str,
|
||||
func_args: str | dict[str, Any],
|
||||
available_functions: dict[str, Callable[..., Any]],
|
||||
original_tool: BaseTool | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Execute a single native tool call.
|
||||
|
||||
Handles argument parsing, usage-limit checks, caching, and hook
|
||||
invocation.
|
||||
|
||||
Args:
|
||||
call_id: The tool call ID from the LLM.
|
||||
func_name: Sanitized tool function name.
|
||||
func_args: Raw arguments (JSON string or dict).
|
||||
available_functions: Mapping of tool names to callables.
|
||||
original_tool: The original BaseTool instance, if available.
|
||||
|
||||
Returns:
|
||||
Dict with keys ``call_id``, ``func_name``, ``result``,
|
||||
``from_cache``, and ``original_tool``.
|
||||
"""
|
||||
from datetime import datetime
|
||||
|
||||
from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageErrorEvent,
|
||||
ToolUsageFinishedEvent,
|
||||
ToolUsageStartedEvent,
|
||||
)
|
||||
from crewai.hooks.tool_hooks import (
|
||||
ToolCallHookContext,
|
||||
get_after_tool_call_hooks,
|
||||
get_before_tool_call_hooks,
|
||||
)
|
||||
|
||||
args_dict, parse_error = parse_tool_call_args(
|
||||
func_args, func_name, call_id, original_tool
|
||||
)
|
||||
if parse_error is not None:
|
||||
return {
|
||||
"call_id": call_id,
|
||||
"func_name": func_name,
|
||||
"result": cast(str, parse_error["result"]),
|
||||
"from_cache": False,
|
||||
"original_tool": original_tool,
|
||||
}
|
||||
|
||||
if (
|
||||
original_tool
|
||||
and getattr(original_tool, "max_usage_count", None) is not None
|
||||
and getattr(original_tool, "current_usage_count", 0)
|
||||
>= original_tool.max_usage_count
|
||||
):
|
||||
return {
|
||||
"call_id": call_id,
|
||||
"func_name": func_name,
|
||||
"result": (
|
||||
f"Tool '{func_name}' has reached its usage limit of "
|
||||
f"{original_tool.max_usage_count} times and cannot be used anymore."
|
||||
),
|
||||
"from_cache": False,
|
||||
"original_tool": original_tool,
|
||||
}
|
||||
|
||||
from_cache = False
|
||||
result: str = f"Tool '{func_name}' not found"
|
||||
input_str = json.dumps(args_dict) if args_dict else ""
|
||||
|
||||
if self._cache_handler:
|
||||
cached = self._cache_handler.read(tool=func_name, input=input_str)
|
||||
if cached is not None:
|
||||
result = str(cached) if not isinstance(cached, str) else cached
|
||||
from_cache = True
|
||||
|
||||
started_at = datetime.now()
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageStartedEvent(
|
||||
tool_name=func_name,
|
||||
tool_args=args_dict,
|
||||
from_agent=self,
|
||||
agent_key=self.key,
|
||||
),
|
||||
)
|
||||
|
||||
structured_tool: CrewStructuredTool | None = next(
|
||||
(t for t in self._parsed_tools if sanitize_tool_name(t.name) == func_name),
|
||||
None,
|
||||
)
|
||||
|
||||
hook_blocked = False
|
||||
before_ctx = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
tool_input=args_dict,
|
||||
tool=structured_tool, # type: ignore[arg-type]
|
||||
agent=self,
|
||||
task=None,
|
||||
crew=None,
|
||||
)
|
||||
try:
|
||||
for hook in get_before_tool_call_hooks():
|
||||
if hook(before_ctx) is False:
|
||||
hook_blocked = True
|
||||
break
|
||||
except Exception as hook_err:
|
||||
if self.verbose:
|
||||
self._printer.print(
|
||||
content=f"Error in before_tool_call hook: {hook_err}",
|
||||
color="red",
|
||||
)
|
||||
|
||||
error_event_emitted = False
|
||||
if hook_blocked:
|
||||
result = f"Tool execution blocked by hook. Tool: {func_name}"
|
||||
elif not from_cache and func_name in available_functions:
|
||||
try:
|
||||
raw_result = available_functions[func_name](**(args_dict or {}))
|
||||
result = str(raw_result) if not isinstance(raw_result, str) else raw_result
|
||||
|
||||
if self._cache_handler:
|
||||
should_cache = True
|
||||
if (
|
||||
original_tool
|
||||
and hasattr(original_tool, "cache_function")
|
||||
and callable(original_tool.cache_function)
|
||||
):
|
||||
should_cache = original_tool.cache_function(args_dict, raw_result)
|
||||
if should_cache:
|
||||
self._cache_handler.add(
|
||||
tool=func_name, input=input_str, output=raw_result
|
||||
)
|
||||
except Exception as e:
|
||||
result = f"Error executing tool '{func_name}': {e}"
|
||||
error_event_emitted = True
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageErrorEvent(
|
||||
tool_name=func_name,
|
||||
tool_args=args_dict,
|
||||
from_agent=self,
|
||||
agent_key=self.key,
|
||||
error=e,
|
||||
),
|
||||
)
|
||||
|
||||
after_ctx = ToolCallHookContext(
|
||||
tool_name=func_name,
|
||||
tool_input=args_dict,
|
||||
tool=structured_tool, # type: ignore[arg-type]
|
||||
agent=self,
|
||||
task=None,
|
||||
crew=None,
|
||||
tool_result=result,
|
||||
)
|
||||
try:
|
||||
for after_hook in get_after_tool_call_hooks():
|
||||
after_result = after_hook(after_ctx)
|
||||
if after_result is not None:
|
||||
result = after_result
|
||||
after_ctx.tool_result = result
|
||||
except Exception as hook_err:
|
||||
if self.verbose:
|
||||
self._printer.print(
|
||||
content=f"Error in after_tool_call hook: {hook_err}",
|
||||
color="red",
|
||||
)
|
||||
|
||||
if not error_event_emitted:
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=ToolUsageFinishedEvent(
|
||||
output=result,
|
||||
tool_name=func_name,
|
||||
tool_args=args_dict,
|
||||
from_agent=self,
|
||||
agent_key=self.key,
|
||||
started_at=started_at,
|
||||
finished_at=datetime.now(),
|
||||
),
|
||||
)
|
||||
|
||||
self.tools_results.append({
|
||||
"result": result,
|
||||
"tool_name": func_name,
|
||||
"tool_args": args_dict,
|
||||
})
|
||||
|
||||
return {
|
||||
"call_id": call_id,
|
||||
"func_name": func_name,
|
||||
"result": result,
|
||||
"from_cache": from_cache,
|
||||
"original_tool": original_tool,
|
||||
}
|
||||
|
||||
def _invoke_loop_react(
|
||||
self, response_model: type[BaseModel] | None = None
|
||||
) -> AgentFinish:
|
||||
"""Execute agent loop using the ReAct text-based pattern.
|
||||
|
||||
This is the fallback when the LLM does not support native function calling.
|
||||
"""
|
||||
Run the agent's thought process until it reaches a conclusion or max iterations.
|
||||
|
||||
Args:
|
||||
response_model: Optional Pydantic model for native structured output.
|
||||
@@ -1385,6 +869,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
Returns:
|
||||
AgentFinish: The final result of the agent execution.
|
||||
"""
|
||||
# Execute the agent loop
|
||||
formatted_answer: AgentAction | AgentFinish | None = None
|
||||
while not isinstance(formatted_answer, AgentFinish):
|
||||
try:
|
||||
@@ -1464,6 +949,7 @@ class LiteAgent(FlowTrackable, BaseModel):
|
||||
|
||||
except Exception as e:
|
||||
if e.__class__.__module__.startswith("litellm"):
|
||||
# Do not retry on litellm errors
|
||||
raise e
|
||||
if is_context_length_exceeded(e):
|
||||
handle_context_length(
|
||||
|
||||
@@ -26,6 +26,7 @@ from crewai.events.types.llm_events import (
|
||||
LLMCallStartedEvent,
|
||||
LLMCallType,
|
||||
LLMStreamChunkEvent,
|
||||
LLMThinkingChunkEvent,
|
||||
)
|
||||
from crewai.events.types.tool_usage_events import (
|
||||
ToolUsageErrorEvent,
|
||||
@@ -368,9 +369,6 @@ class BaseLLM(ABC):
|
||||
"""Emit LLM call started event."""
|
||||
from crewai.utilities.serialization import to_serializable
|
||||
|
||||
if not hasattr(crewai_event_bus, "emit"):
|
||||
raise ValueError("crewai_event_bus does not have an emit method") from None
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMCallStartedEvent(
|
||||
@@ -416,9 +414,6 @@ class BaseLLM(ABC):
|
||||
from_agent: Agent | None = None,
|
||||
) -> None:
|
||||
"""Emit LLM call failed event."""
|
||||
if not hasattr(crewai_event_bus, "emit"):
|
||||
raise ValueError("crewai_event_bus does not have an emit method") from None
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMCallFailedEvent(
|
||||
@@ -449,9 +444,6 @@ class BaseLLM(ABC):
|
||||
call_type: The type of LLM call (LLM_CALL or TOOL_CALL).
|
||||
response_id: Unique ID for a particular LLM response, chunks have same response_id.
|
||||
"""
|
||||
if not hasattr(crewai_event_bus, "emit"):
|
||||
raise ValueError("crewai_event_bus does not have an emit method") from None
|
||||
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMStreamChunkEvent(
|
||||
@@ -465,6 +457,32 @@ class BaseLLM(ABC):
|
||||
),
|
||||
)
|
||||
|
||||
def _emit_thinking_chunk_event(
|
||||
self,
|
||||
chunk: str,
|
||||
from_task: Task | None = None,
|
||||
from_agent: Agent | None = None,
|
||||
response_id: str | None = None,
|
||||
) -> None:
|
||||
"""Emit thinking/reasoning chunk event from a thinking model.
|
||||
|
||||
Args:
|
||||
chunk: The thinking text content.
|
||||
from_task: The task that initiated the call.
|
||||
from_agent: The agent that initiated the call.
|
||||
response_id: Unique ID for a particular LLM response.
|
||||
"""
|
||||
crewai_event_bus.emit(
|
||||
self,
|
||||
event=LLMThinkingChunkEvent(
|
||||
chunk=chunk,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_id=response_id,
|
||||
call_id=get_current_call_id(),
|
||||
),
|
||||
)
|
||||
|
||||
def _handle_tool_execution(
|
||||
self,
|
||||
function_name: str,
|
||||
|
||||
@@ -61,6 +61,7 @@ class GeminiCompletion(BaseLLM):
|
||||
interceptor: BaseInterceptor[Any, Any] | None = None,
|
||||
use_vertexai: bool | None = None,
|
||||
response_format: type[BaseModel] | None = None,
|
||||
thinking_config: types.ThinkingConfig | None = None,
|
||||
**kwargs: Any,
|
||||
):
|
||||
"""Initialize Google Gemini chat completion client.
|
||||
@@ -93,6 +94,10 @@ class GeminiCompletion(BaseLLM):
|
||||
api_version="v1" is automatically configured.
|
||||
response_format: Pydantic model for structured output. Used as default when
|
||||
response_model is not passed to call()/acall() methods.
|
||||
thinking_config: ThinkingConfig for thinking models (gemini-2.5+, gemini-3+).
|
||||
Controls thought output via include_thoughts, thinking_budget,
|
||||
and thinking_level. When None, thinking models automatically
|
||||
get include_thoughts=True so thought content is surfaced.
|
||||
**kwargs: Additional parameters
|
||||
"""
|
||||
if interceptor is not None:
|
||||
@@ -139,6 +144,14 @@ class GeminiCompletion(BaseLLM):
|
||||
version_match and float(version_match.group(1)) >= 2.0
|
||||
)
|
||||
|
||||
self.thinking_config = thinking_config
|
||||
if (
|
||||
self.thinking_config is None
|
||||
and version_match
|
||||
and float(version_match.group(1)) >= 2.5
|
||||
):
|
||||
self.thinking_config = types.ThinkingConfig(include_thoughts=True)
|
||||
|
||||
@property
|
||||
def stop(self) -> list[str]:
|
||||
"""Get stop sequences sent to the API."""
|
||||
@@ -520,6 +533,9 @@ class GeminiCompletion(BaseLLM):
|
||||
if self.safety_settings:
|
||||
config_params["safety_settings"] = self.safety_settings
|
||||
|
||||
if self.thinking_config is not None:
|
||||
config_params["thinking_config"] = self.thinking_config
|
||||
|
||||
return types.GenerateContentConfig(**config_params)
|
||||
|
||||
def _convert_tools_for_interference( # type: ignore[override]
|
||||
@@ -618,9 +634,17 @@ class GeminiCompletion(BaseLLM):
|
||||
function_response_part = types.Part.from_function_response(
|
||||
name=tool_name, response=response_data
|
||||
)
|
||||
contents.append(
|
||||
types.Content(role="user", parts=[function_response_part])
|
||||
)
|
||||
if (
|
||||
contents
|
||||
and contents[-1].role == "user"
|
||||
and contents[-1].parts
|
||||
and contents[-1].parts[-1].function_response is not None
|
||||
):
|
||||
contents[-1].parts.append(function_response_part)
|
||||
else:
|
||||
contents.append(
|
||||
types.Content(role="user", parts=[function_response_part])
|
||||
)
|
||||
elif role == "assistant" and message.get("tool_calls"):
|
||||
raw_parts: list[Any] | None = message.get("raw_tool_call_parts")
|
||||
if raw_parts and all(isinstance(p, types.Part) for p in raw_parts):
|
||||
@@ -931,15 +955,6 @@ class GeminiCompletion(BaseLLM):
|
||||
if chunk.usage_metadata:
|
||||
usage_data = self._extract_token_usage(chunk)
|
||||
|
||||
if chunk.text:
|
||||
full_response += chunk.text
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=chunk.text,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_id=response_id,
|
||||
)
|
||||
|
||||
if chunk.candidates:
|
||||
candidate = chunk.candidates[0]
|
||||
if candidate.content and candidate.content.parts:
|
||||
@@ -976,6 +991,21 @@ class GeminiCompletion(BaseLLM):
|
||||
call_type=LLMCallType.TOOL_CALL,
|
||||
response_id=response_id,
|
||||
)
|
||||
elif part.thought and part.text:
|
||||
self._emit_thinking_chunk_event(
|
||||
chunk=part.text,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_id=response_id,
|
||||
)
|
||||
elif part.text:
|
||||
full_response += part.text
|
||||
self._emit_stream_chunk_event(
|
||||
chunk=part.text,
|
||||
from_task=from_task,
|
||||
from_agent=from_agent,
|
||||
response_id=response_id,
|
||||
)
|
||||
|
||||
return full_response, function_calls, usage_data
|
||||
|
||||
@@ -1329,7 +1359,7 @@ class GeminiCompletion(BaseLLM):
|
||||
text_parts = [
|
||||
part.text
|
||||
for part in candidate.content.parts
|
||||
if hasattr(part, "text") and part.text
|
||||
if part.text and not part.thought
|
||||
]
|
||||
|
||||
return "".join(text_parts)
|
||||
|
||||
@@ -19,6 +19,7 @@ from crewai.memory.types import (
|
||||
embed_texts,
|
||||
)
|
||||
|
||||
|
||||
_LAZY_IMPORTS: dict[str, tuple[str, str]] = {
|
||||
"Memory": ("crewai.memory.unified_memory", "Memory"),
|
||||
"EncodingFlow": ("crewai.memory.encoding_flow", "EncodingFlow"),
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
Implements adaptive-depth retrieval with:
|
||||
- LLM query distillation into targeted sub-queries
|
||||
- Keyword-driven category filtering
|
||||
- Time-based filtering from temporal hints
|
||||
- Parallel multi-query, multi-scope search
|
||||
- Confidence-based routing with iterative deepening (budget loop)
|
||||
@@ -81,11 +80,8 @@ class RecallFlow(Flow[RecallState]):
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _merged_categories(self) -> list[str] | None:
|
||||
"""Merge caller-supplied and LLM-inferred categories."""
|
||||
merged = list(
|
||||
set((self.state.categories or []))
|
||||
)
|
||||
return merged or None
|
||||
"""Return caller-supplied categories, or None if empty."""
|
||||
return self.state.categories or None
|
||||
|
||||
def _do_search(self) -> list[dict[str, Any]]:
|
||||
"""Run parallel search across (embeddings x scopes) with filters.
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from crewai.telemetry.telemetry import Telemetry
|
||||
|
||||
|
||||
|
||||
__all__ = ["Telemetry"]
|
||||
|
||||
@@ -173,6 +173,12 @@ class Telemetry:
|
||||
|
||||
self._original_handlers: dict[int, Any] = {}
|
||||
|
||||
if threading.current_thread() is not threading.main_thread():
|
||||
logger.debug(
|
||||
"Skipping signal handler registration: not running in main thread"
|
||||
)
|
||||
return
|
||||
|
||||
self._register_signal_handler(signal.SIGTERM, SigTermEvent, shutdown=True)
|
||||
self._register_signal_handler(signal.SIGINT, SigIntEvent, shutdown=True)
|
||||
if hasattr(signal, "SIGHUP"):
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from crewai.tools.base_tool import BaseTool, EnvVar, tool
|
||||
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BaseTool",
|
||||
"EnvVar",
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
"conversation_history_instruction": "You are a member of a crew collaborating to achieve a common goal. Your task is a specific action that contributes to this larger objective. For additional context, please review the conversation history between you and the user that led to the initiation of this crew. Use any relevant information or feedback from the conversation to inform your task execution and ensure your response aligns with both the immediate task and the crew's overall goals.",
|
||||
"feedback_instructions": "User feedback: {feedback}\nInstructions: Use this feedback to enhance the next output iteration.\nNote: Do not respond or add commentary.",
|
||||
"lite_agent_system_prompt_with_tools": "You are {role}. {backstory}\nYour personal goal is: {goal}\n\nYou ONLY have access to the following tools, and should NEVER make up tools that are not listed here:\n\n{tools}\n\nIMPORTANT: Use the following format in your response:\n\n```\nThought: you should always think about what to do\nAction: the action to take, only one name of [{tool_names}], just the name, exactly as it's written.\nAction Input: the input to the action, just a simple JSON object, enclosed in curly braces, using \" to wrap keys and values.\nObservation: the result of the action\n```\n\nOnce all necessary information is gathered, return the following format:\n\n```\nThought: I now know the final answer\nFinal Answer: the final answer to the original input question\n```",
|
||||
"lite_agent_system_prompt_native_tools": "You are {role}. {backstory}\nYour personal goal is: {goal}",
|
||||
"lite_agent_system_prompt_without_tools": "You are {role}. {backstory}\nYour personal goal is: {goal}\n\nTo give my best complete final answer to the task respond using the exact following format:\n\nThought: I now can give a great answer\nFinal Answer: Your final answer must be the great and the most complete as possible, it must be outcome described.\n\nI MUST use these formats, my job depends on it!",
|
||||
"lite_agent_response_format": "Format your final answer according to the following OpenAPI schema: {response_format}\n\nIMPORTANT: Preserve the original content exactly as-is. Do NOT rewrite, paraphrase, or modify the meaning of the content. Only structure it to match the schema format.\n\nDo not include the OpenAPI schema in the final output. Ensure the final output does not include any code block markers like ```json or ```python.",
|
||||
"knowledge_search_query": "The original query is: {task_prompt}.",
|
||||
|
||||
@@ -1245,34 +1245,26 @@ def _setup_before_llm_call_hooks(
|
||||
|
||||
def _setup_after_llm_call_hooks(
|
||||
executor_context: CrewAgentExecutor | AgentExecutor | LiteAgent | None,
|
||||
answer: str | BaseModel | list[Any],
|
||||
answer: str | BaseModel,
|
||||
printer: Printer,
|
||||
verbose: bool = True,
|
||||
) -> str | BaseModel | list[Any]:
|
||||
) -> str | BaseModel:
|
||||
"""Setup and invoke after_llm_call hooks for the executor context.
|
||||
|
||||
Args:
|
||||
executor_context: The executor context to setup the hooks for.
|
||||
answer: The LLM response (string, Pydantic model, or list of native
|
||||
tool calls).
|
||||
answer: The LLM response (string or Pydantic model).
|
||||
printer: Printer instance for error logging.
|
||||
verbose: Whether to print output.
|
||||
|
||||
Returns:
|
||||
The potentially modified response. List-type answers (native tool
|
||||
calls) are always returned unchanged so that callers can rely on
|
||||
``isinstance(answer, list)`` checks.
|
||||
The potentially modified response (string or Pydantic model).
|
||||
"""
|
||||
if executor_context and executor_context.after_llm_call_hooks:
|
||||
from crewai.hooks.llm_hooks import LLMCallHookContext
|
||||
|
||||
original_messages = executor_context.messages
|
||||
|
||||
# Native tool-call lists must survive hooks unchanged. We provide a
|
||||
# stringified representation to hook context for observability but
|
||||
# always return the original list so callers can detect tool calls.
|
||||
is_tool_call_list = isinstance(answer, list)
|
||||
|
||||
# For Pydantic models, serialize to JSON for hooks
|
||||
if isinstance(answer, BaseModel):
|
||||
pydantic_answer = answer
|
||||
@@ -1311,9 +1303,6 @@ def _setup_after_llm_call_hooks(
|
||||
else:
|
||||
executor_context.messages = []
|
||||
|
||||
if is_tool_call_list:
|
||||
return answer
|
||||
|
||||
# If hooks modified the response, update answer accordingly
|
||||
if pydantic_answer is not None:
|
||||
# For Pydantic models, reparse the JSON if it was modified
|
||||
|
||||
@@ -123,7 +123,7 @@ class TestAgentExecutor:
|
||||
executor.state.iterations = 10
|
||||
|
||||
result = executor.check_max_iterations()
|
||||
assert result == "force_final_answer"
|
||||
assert result == "max_iterations_exceeded"
|
||||
|
||||
def test_route_by_answer_type_action(self, mock_dependencies):
|
||||
"""Test routing for AgentAction."""
|
||||
|
||||
@@ -1160,315 +1160,3 @@ def test_lite_agent_memory_instance_recall_and_save_called():
|
||||
mock_memory.remember_many.assert_called_once_with(
|
||||
["Fact one.", "Fact two."], agent_role="Test"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Native tool calling tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _NativeToolCallLLM(BaseLLM):
|
||||
"""Fake LLM that supports native function calling and returns tool calls."""
|
||||
|
||||
def __init__(self, tool_calls=None, final_answer="42"):
|
||||
super().__init__(model="fake-native-fc-model")
|
||||
self._tool_calls = tool_calls or []
|
||||
self._final_answer = final_answer
|
||||
self._call_index = 0
|
||||
|
||||
def call(
|
||||
self,
|
||||
messages,
|
||||
tools=None,
|
||||
callbacks=None,
|
||||
available_functions=None,
|
||||
from_task=None,
|
||||
from_agent=None,
|
||||
response_model=None,
|
||||
):
|
||||
idx = self._call_index
|
||||
self._call_index += 1
|
||||
if idx < len(self._tool_calls):
|
||||
return self._tool_calls[idx]
|
||||
return self._final_answer
|
||||
|
||||
def supports_function_calling(self) -> bool:
|
||||
return True
|
||||
|
||||
def supports_stop_words(self) -> bool:
|
||||
return False
|
||||
|
||||
def get_context_window_size(self) -> int:
|
||||
return 8192
|
||||
|
||||
|
||||
class _ReactOnlyLLM(BaseLLM):
|
||||
"""Fake LLM that does NOT support function calling."""
|
||||
|
||||
def __init__(self, response="Thought: done\nFinal Answer: hello"):
|
||||
super().__init__(model="fake-react-only-model")
|
||||
self._response = response
|
||||
|
||||
def call(self, messages, **kwargs):
|
||||
return self._response
|
||||
|
||||
def supports_function_calling(self) -> bool:
|
||||
return False
|
||||
|
||||
def supports_stop_words(self) -> bool:
|
||||
return True
|
||||
|
||||
def get_context_window_size(self) -> int:
|
||||
return 8192
|
||||
|
||||
|
||||
def test_lite_agent_native_mode_detection_with_fc_llm():
|
||||
"""LiteAgent should set _use_native_tools=True when LLM supports function calling and tools exist."""
|
||||
llm = _NativeToolCallLLM(final_answer="done")
|
||||
agent = LiteAgent(
|
||||
role="Tester", goal="Test", backstory="Test agent",
|
||||
llm=llm, tools=[SecretLookupTool()],
|
||||
)
|
||||
agent.kickoff("test")
|
||||
assert agent._use_native_tools is True
|
||||
|
||||
|
||||
def test_lite_agent_native_mode_detection_without_fc_llm():
|
||||
"""LiteAgent should set _use_native_tools=False when LLM does not support function calling."""
|
||||
llm = _ReactOnlyLLM()
|
||||
agent = LiteAgent(
|
||||
role="Tester", goal="Test", backstory="Test agent",
|
||||
llm=llm, tools=[SecretLookupTool()],
|
||||
)
|
||||
agent.kickoff("test")
|
||||
assert agent._use_native_tools is False
|
||||
|
||||
|
||||
def test_lite_agent_native_mode_detection_no_tools():
|
||||
"""LiteAgent should set _use_native_tools=False when there are no tools."""
|
||||
llm = _NativeToolCallLLM(final_answer="no tools needed")
|
||||
agent = LiteAgent(
|
||||
role="Tester", goal="Test", backstory="Test agent",
|
||||
llm=llm, tools=[],
|
||||
)
|
||||
agent.kickoff("test")
|
||||
assert agent._use_native_tools is False
|
||||
|
||||
|
||||
def test_lite_agent_native_mode_system_prompt_has_no_react_instructions():
|
||||
"""In native mode the system prompt should NOT contain ReAct Action/Action Input instructions."""
|
||||
llm = _NativeToolCallLLM(final_answer="result")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute things", backstory="A math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
agent.kickoff("What is 1+1?")
|
||||
|
||||
system_msg = agent._messages[0]
|
||||
assert system_msg["role"] == "system"
|
||||
content = system_msg["content"]
|
||||
assert "Action:" not in content
|
||||
assert "Action Input:" not in content
|
||||
assert "Observation:" not in content
|
||||
assert "Calculator" in content
|
||||
assert "Compute things" in content
|
||||
|
||||
|
||||
def test_lite_agent_react_mode_system_prompt_has_react_instructions():
|
||||
"""In ReAct mode the system prompt SHOULD contain Action/Action Input instructions."""
|
||||
llm = _ReactOnlyLLM()
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute things", backstory="A math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
agent.kickoff("What is 1+1?")
|
||||
|
||||
system_msg = agent._messages[0]
|
||||
content = system_msg["content"]
|
||||
assert "Action:" in content
|
||||
assert "Action Input:" in content
|
||||
|
||||
|
||||
def _make_openai_tool_call(call_id, name, arguments):
|
||||
"""Helper to create an OpenAI-style tool call object."""
|
||||
tc = Mock()
|
||||
tc.id = call_id
|
||||
func = Mock()
|
||||
func.name = name
|
||||
func.arguments = arguments
|
||||
tc.function = func
|
||||
return tc
|
||||
|
||||
|
||||
def test_lite_agent_native_tool_execution():
|
||||
"""Verify LiteAgent executes native tool calls and feeds results back to the LLM."""
|
||||
tool_call = [_make_openai_tool_call("call_1", "calculate", '{"expression": "6*7"}')]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_call], final_answer="The answer is 42")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
result = agent.kickoff("What is 6 * 7?")
|
||||
|
||||
assert "42" in result.raw
|
||||
assert len(agent.tools_results) == 1
|
||||
assert agent.tools_results[0]["tool_name"] == "calculate"
|
||||
|
||||
|
||||
def test_lite_agent_native_parallel_tool_calls():
|
||||
"""When LLM returns multiple tool calls, they should all be executed."""
|
||||
tool_calls = [
|
||||
_make_openai_tool_call("call_1", "calculate", '{"expression": "2+3"}'),
|
||||
_make_openai_tool_call("call_2", "calculate", '{"expression": "4+5"}'),
|
||||
]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_calls], final_answer="5 and 9")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
result = agent.kickoff("What is 2+3 and 4+5?")
|
||||
|
||||
assert len(agent.tools_results) == 2
|
||||
tool_names = [r["tool_name"] for r in agent.tools_results]
|
||||
assert tool_names == ["calculate", "calculate"]
|
||||
|
||||
tool_messages = [m for m in agent._messages if m.get("role") == "tool"]
|
||||
assert len(tool_messages) == 2
|
||||
|
||||
assistant_tc_messages = [
|
||||
m for m in agent._messages
|
||||
if m.get("role") == "assistant" and m.get("tool_calls")
|
||||
]
|
||||
assert len(assistant_tc_messages) == 1
|
||||
assert len(assistant_tc_messages[0]["tool_calls"]) == 2
|
||||
|
||||
|
||||
def test_lite_agent_native_tool_usage_count_no_double_increment():
|
||||
"""current_usage_count must increment exactly once per native tool call.
|
||||
|
||||
BaseTool.run() already increments the counter internally, so the native
|
||||
tool call handler must not add a second increment.
|
||||
"""
|
||||
tool_call = [_make_openai_tool_call("call_1", "calculate", '{"expression": "1+1"}')]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_call], final_answer="2")
|
||||
calc_tool = CalculatorTool()
|
||||
assert calc_tool.current_usage_count == 0
|
||||
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[calc_tool],
|
||||
)
|
||||
agent.kickoff("What is 1+1?")
|
||||
|
||||
assert calc_tool.current_usage_count == 1
|
||||
|
||||
|
||||
def test_lite_agent_native_tool_max_usage_count_respected():
|
||||
"""A tool with max_usage_count=1 should be usable exactly once, not blocked after 1 call."""
|
||||
call_round_1 = [_make_openai_tool_call("c1", "calculate", '{"expression": "1+1"}')]
|
||||
call_round_2 = [_make_openai_tool_call("c2", "calculate", '{"expression": "2+2"}')]
|
||||
|
||||
llm = _NativeToolCallLLM(
|
||||
tool_calls=[call_round_1, call_round_2], final_answer="done"
|
||||
)
|
||||
calc_tool = CalculatorTool()
|
||||
calc_tool.max_usage_count = 2
|
||||
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[calc_tool],
|
||||
)
|
||||
agent.kickoff("Compute 1+1 then 2+2")
|
||||
|
||||
executed = [r for r in agent.tools_results if "usage limit" not in r["result"]]
|
||||
assert len(executed) == 2
|
||||
assert calc_tool.current_usage_count == 2
|
||||
|
||||
|
||||
def test_lite_agent_native_tool_calls_with_after_llm_hook():
|
||||
"""Native tool calls must be processed even when after_llm_call hooks are active.
|
||||
|
||||
Regression test: _setup_after_llm_call_hooks was converting the list of
|
||||
tool calls to a string via str(), causing isinstance(answer, list) to fail
|
||||
in _invoke_loop_native_tools and silently returning the stringified list as
|
||||
the agent's final answer.
|
||||
"""
|
||||
hook_called = {"count": 0}
|
||||
|
||||
def after_hook(context):
|
||||
hook_called["count"] += 1
|
||||
return None
|
||||
|
||||
tool_call = [_make_openai_tool_call("call_1", "calculate", '{"expression": "6*7"}')]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_call], final_answer="The answer is 42")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
agent._after_llm_call_hooks.append(after_hook)
|
||||
|
||||
result = agent.kickoff("What is 6 * 7?")
|
||||
|
||||
assert hook_called["count"] >= 1
|
||||
assert len(agent.tools_results) == 1
|
||||
assert agent.tools_results[0]["tool_name"] == "calculate"
|
||||
assert "42" in result.raw
|
||||
|
||||
|
||||
def test_lite_agent_native_parallel_tool_calls_with_after_llm_hook():
|
||||
"""Multiple native tool calls in a single response must work with hooks active."""
|
||||
hook_called = {"count": 0}
|
||||
|
||||
def after_hook(context):
|
||||
hook_called["count"] += 1
|
||||
return None
|
||||
|
||||
tool_calls = [
|
||||
_make_openai_tool_call("call_1", "calculate", '{"expression": "2+3"}'),
|
||||
_make_openai_tool_call("call_2", "calculate", '{"expression": "4+5"}'),
|
||||
]
|
||||
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_calls], final_answer="5 and 9")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[CalculatorTool()],
|
||||
)
|
||||
agent._after_llm_call_hooks.append(after_hook)
|
||||
|
||||
result = agent.kickoff("What is 2+3 and 4+5?")
|
||||
|
||||
assert hook_called["count"] >= 1
|
||||
assert len(agent.tools_results) == 2
|
||||
tool_names = [r["tool_name"] for r in agent.tools_results]
|
||||
assert tool_names == ["calculate", "calculate"]
|
||||
|
||||
|
||||
def test_lite_agent_native_duplicate_tool_names_resolved():
|
||||
"""Two tools with the same sanitized name should both be usable via dedup suffixes.
|
||||
|
||||
convert_tools_to_openai_schema renames duplicates (e.g. calculate -> calculate_2).
|
||||
The original_tools_by_name mapping must honour these deduplicated names so
|
||||
result_as_answer, max_usage_count, and usage tracking work for every tool.
|
||||
"""
|
||||
tool_a = CalculatorTool()
|
||||
tool_a.result_as_answer = True
|
||||
|
||||
tool_b = CalculatorTool()
|
||||
|
||||
tool_call = [
|
||||
_make_openai_tool_call("c1", "calculate_2", '{"expression": "9+1"}'),
|
||||
]
|
||||
llm = _NativeToolCallLLM(tool_calls=[tool_call], final_answer="fallback")
|
||||
agent = LiteAgent(
|
||||
role="Calculator", goal="Compute", backstory="Math agent",
|
||||
llm=llm, tools=[tool_a, tool_b],
|
||||
)
|
||||
agent.kickoff("What is 9+1?")
|
||||
|
||||
assert len(agent.tools_results) == 1
|
||||
assert agent.tools_results[0]["tool_name"] == "calculate_2"
|
||||
assert "10" in agent.tools_results[0]["result"]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -28,7 +28,19 @@ class TestPlusAPI(unittest.TestCase):
|
||||
response = self.api.login_to_tool_repository()
|
||||
|
||||
mock_make_request.assert_called_once_with(
|
||||
"POST", "/crewai_plus/api/v1/tools/login"
|
||||
"POST", "/crewai_plus/api/v1/tools/login", json={}
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
@patch("crewai.cli.plus_api.PlusAPI._make_request")
|
||||
def test_login_to_tool_repository_with_user_identifier(self, mock_make_request):
|
||||
mock_response = MagicMock()
|
||||
mock_make_request.return_value = mock_response
|
||||
|
||||
response = self.api.login_to_tool_repository(user_identifier="test-hash-123")
|
||||
|
||||
mock_make_request.assert_called_once_with(
|
||||
"POST", "/crewai_plus/api/v1/tools/login", json={"user_identifier": "test-hash-123"}
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
@@ -67,7 +79,7 @@ class TestPlusAPI(unittest.TestCase):
|
||||
response = self.api.login_to_tool_repository()
|
||||
|
||||
self.assert_request_with_org_id(
|
||||
mock_client_instance, "POST", "/crewai_plus/api/v1/tools/login"
|
||||
mock_client_instance, "POST", "/crewai_plus/api/v1/tools/login", json={}
|
||||
)
|
||||
self.assertEqual(response, mock_response)
|
||||
|
||||
|
||||
@@ -121,3 +121,41 @@ def test_telemetry_singleton_pattern():
|
||||
thread.join()
|
||||
|
||||
assert all(instance is telemetry1 for instance in instances)
|
||||
|
||||
|
||||
def test_no_signal_handler_traceback_in_non_main_thread():
|
||||
"""Signal handler registration should be silently skipped in non-main threads.
|
||||
|
||||
Regression test for https://github.com/crewAIInc/crewAI/issues/4289
|
||||
"""
|
||||
errors: list[Exception] = []
|
||||
mock_holder: dict = {}
|
||||
|
||||
def init_in_thread():
|
||||
try:
|
||||
Telemetry._instance = None
|
||||
with (
|
||||
patch.dict(
|
||||
os.environ,
|
||||
{"CREWAI_DISABLE_TELEMETRY": "false", "OTEL_SDK_DISABLED": "false"},
|
||||
),
|
||||
patch("crewai.telemetry.telemetry.TracerProvider"),
|
||||
patch("signal.signal") as mock_signal,
|
||||
patch("crewai.telemetry.telemetry.logger") as mock_logger,
|
||||
):
|
||||
Telemetry()
|
||||
mock_holder["signal"] = mock_signal
|
||||
mock_holder["logger"] = mock_logger
|
||||
except Exception as exc:
|
||||
errors.append(exc)
|
||||
|
||||
thread = threading.Thread(target=init_in_thread)
|
||||
thread.start()
|
||||
thread.join()
|
||||
|
||||
assert not errors, f"Unexpected error: {errors}"
|
||||
assert mock_holder, "Thread did not execute"
|
||||
mock_holder["signal"].assert_not_called()
|
||||
mock_holder["logger"].debug.assert_any_call(
|
||||
"Skipping signal handler registration: not running in main thread"
|
||||
)
|
||||
|
||||
@@ -1843,3 +1843,53 @@ def test_cyclic_flow_works_with_persist_and_id_input():
|
||||
f"'{method}' should fire 3 times, "
|
||||
f"got {len(events)}: {execution_order}"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.timeout(5)
|
||||
def test_self_listening_method_does_not_loop():
|
||||
"""A method whose @listen label matches its own name must not loop forever.
|
||||
|
||||
Without the guard, 'process' re-triggers itself on every completion,
|
||||
running indefinitely (timeout → FAIL). The fix caps method calls
|
||||
and raises RecursionError (PASS).
|
||||
"""
|
||||
|
||||
class SelfListenFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
return "process"
|
||||
|
||||
@router(begin)
|
||||
def route(self):
|
||||
return "process"
|
||||
|
||||
@listen("process")
|
||||
def process(self):
|
||||
pass
|
||||
|
||||
flow = SelfListenFlow()
|
||||
with pytest.raises(RecursionError, match="infinite loop"):
|
||||
flow.kickoff()
|
||||
|
||||
|
||||
def test_or_condition_self_listen_fires_once():
|
||||
"""or_() with a self-referencing label only fires once due to or_() guard."""
|
||||
call_count = 0
|
||||
|
||||
class OrSelfListenFlow(Flow):
|
||||
@start()
|
||||
def begin(self):
|
||||
return "process"
|
||||
|
||||
@router(begin)
|
||||
def route(self):
|
||||
return "process"
|
||||
|
||||
@listen(or_("other_trigger", "process"))
|
||||
def process(self):
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
|
||||
flow = OrSelfListenFlow()
|
||||
flow.kickoff()
|
||||
assert call_count == 1
|
||||
|
||||
@@ -840,3 +840,87 @@ class TestTraceListenerSetup:
|
||||
mock_mark_failed.assert_called_once_with(
|
||||
"test_batch_id_12345", "Internal Server Error"
|
||||
)
|
||||
|
||||
def test_ephemeral_batch_includes_anon_id(self):
|
||||
"""Test that ephemeral batch initialization sends anon_id from get_user_id()"""
|
||||
fake_user_id = "abc123def456"
|
||||
|
||||
with (
|
||||
patch(
|
||||
"crewai.events.listeners.tracing.trace_batch_manager.is_tracing_enabled_in_context",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"crewai.events.listeners.tracing.trace_batch_manager.get_user_id",
|
||||
return_value=fake_user_id,
|
||||
),
|
||||
patch(
|
||||
"crewai.events.listeners.tracing.trace_batch_manager.should_auto_collect_first_time_traces",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
batch_manager = TraceBatchManager()
|
||||
|
||||
mock_response = MagicMock(
|
||||
status_code=201,
|
||||
json=MagicMock(return_value={
|
||||
"ephemeral_trace_id": "test-trace-id",
|
||||
"access_code": "TRACE-abc123",
|
||||
}),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
batch_manager.plus_api,
|
||||
"initialize_ephemeral_trace_batch",
|
||||
return_value=mock_response,
|
||||
) as mock_init:
|
||||
batch_manager.initialize_batch(
|
||||
user_context={"privacy_level": "standard"},
|
||||
execution_metadata={
|
||||
"execution_type": "crew",
|
||||
"crew_name": "test_crew",
|
||||
},
|
||||
use_ephemeral=True,
|
||||
)
|
||||
|
||||
mock_init.assert_called_once()
|
||||
payload = mock_init.call_args[0][0]
|
||||
assert payload["user_identifier"] == fake_user_id
|
||||
assert "ephemeral_trace_id" in payload
|
||||
|
||||
def test_non_ephemeral_batch_does_not_include_anon_id(self):
|
||||
"""Test that non-ephemeral batch initialization does not send anon_id"""
|
||||
with (
|
||||
patch(
|
||||
"crewai.events.listeners.tracing.trace_batch_manager.is_tracing_enabled_in_context",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"crewai.events.listeners.tracing.trace_batch_manager.should_auto_collect_first_time_traces",
|
||||
return_value=False,
|
||||
),
|
||||
):
|
||||
batch_manager = TraceBatchManager()
|
||||
|
||||
mock_response = MagicMock(
|
||||
status_code=201,
|
||||
json=MagicMock(return_value={"trace_id": "test-trace-id"}),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
batch_manager.plus_api,
|
||||
"initialize_trace_batch",
|
||||
return_value=mock_response,
|
||||
) as mock_init:
|
||||
batch_manager.initialize_batch(
|
||||
user_context={"privacy_level": "standard"},
|
||||
execution_metadata={
|
||||
"execution_type": "crew",
|
||||
"crew_name": "test_crew",
|
||||
},
|
||||
use_ephemeral=False,
|
||||
)
|
||||
|
||||
mock_init.assert_called_once()
|
||||
payload = mock_init.call_args[0][0]
|
||||
assert "user_identifier" not in payload
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
"""CrewAI development tools."""
|
||||
|
||||
__version__ = "1.10.1a1"
|
||||
__version__ = "1.10.1"
|
||||
|
||||
@@ -8,9 +8,9 @@ authors = [
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff==0.14.7",
|
||||
"mypy==1.19.0",
|
||||
"pre-commit==4.5.0",
|
||||
"ruff==0.15.1",
|
||||
"mypy==1.19.1",
|
||||
"pre-commit==4.5.1",
|
||||
"bandit==1.9.2",
|
||||
"pytest==8.4.2",
|
||||
"pytest-asyncio==1.3.0",
|
||||
@@ -23,9 +23,9 @@ dev = [
|
||||
"pytest-split==0.10.0",
|
||||
"types-requests~=2.31.0.6",
|
||||
"types-pyyaml==6.0.*",
|
||||
"types-regex==2024.11.6.*",
|
||||
"types-regex==2026.1.15.*",
|
||||
"types-appdirs==1.4.*",
|
||||
"boto3-stubs[bedrock-runtime]==1.40.54",
|
||||
"boto3-stubs[bedrock-runtime]==1.42.40",
|
||||
"types-psycopg2==2.9.21.20251012",
|
||||
"types-pymysql==1.1.0.20250916",
|
||||
"types-aiofiles~=25.1.0",
|
||||
@@ -153,6 +153,7 @@ override-dependencies = [
|
||||
"onnxruntime<1.24; python_version < '3.11'",
|
||||
"pillow>=12.1.1",
|
||||
"langchain-core>=0.3.80,<1",
|
||||
"urllib3>=2.6.3",
|
||||
]
|
||||
|
||||
[tool.uv.workspace]
|
||||
|
||||
323
uv.lock
generated
323
uv.lock
generated
@@ -24,14 +24,15 @@ overrides = [
|
||||
{ name = "onnxruntime", marker = "python_full_version < '3.11'", specifier = "<1.24" },
|
||||
{ name = "pillow", specifier = ">=12.1.1" },
|
||||
{ name = "rich", specifier = ">=13.7.1" },
|
||||
{ name = "urllib3", specifier = ">=2.6.3" },
|
||||
]
|
||||
|
||||
[manifest.dependency-groups]
|
||||
dev = [
|
||||
{ name = "bandit", specifier = "==1.9.2" },
|
||||
{ name = "boto3-stubs", extras = ["bedrock-runtime"], specifier = "==1.40.54" },
|
||||
{ name = "mypy", specifier = "==1.19.0" },
|
||||
{ name = "pre-commit", specifier = "==4.5.0" },
|
||||
{ name = "boto3-stubs", extras = ["bedrock-runtime"], specifier = "==1.42.40" },
|
||||
{ name = "mypy", specifier = "==1.19.1" },
|
||||
{ name = "pre-commit", specifier = "==4.5.1" },
|
||||
{ name = "pytest", specifier = "==8.4.2" },
|
||||
{ name = "pytest-asyncio", specifier = "==1.3.0" },
|
||||
{ name = "pytest-randomly", specifier = "==4.0.1" },
|
||||
@@ -40,13 +41,13 @@ dev = [
|
||||
{ name = "pytest-subprocess", specifier = "==1.5.3" },
|
||||
{ name = "pytest-timeout", specifier = "==2.4.0" },
|
||||
{ name = "pytest-xdist", specifier = "==3.8.0" },
|
||||
{ name = "ruff", specifier = "==0.14.7" },
|
||||
{ name = "ruff", specifier = "==0.15.1" },
|
||||
{ name = "types-aiofiles", specifier = "~=25.1.0" },
|
||||
{ name = "types-appdirs", specifier = "==1.4.*" },
|
||||
{ name = "types-psycopg2", specifier = "==2.9.21.20251012" },
|
||||
{ name = "types-pymysql", specifier = "==1.1.0.20250916" },
|
||||
{ name = "types-pyyaml", specifier = "==6.0.*" },
|
||||
{ name = "types-regex", specifier = "==2024.11.6.*" },
|
||||
{ name = "types-regex", specifier = "==2026.1.15.*" },
|
||||
{ name = "types-requests", specifier = "~=2.31.0.6" },
|
||||
{ name = "vcrpy", specifier = "==7.0.0" },
|
||||
]
|
||||
@@ -595,8 +596,7 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "starlette" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "uvicorn" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
@@ -621,16 +621,16 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "boto3-stubs"
|
||||
version = "1.40.54"
|
||||
version = "1.42.40"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore-stubs" },
|
||||
{ name = "types-s3transfer" },
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e2/70/245477b7f07c9e1533c47fa69e611b172814423a6fd4637004f0d2a13b73/boto3_stubs-1.40.54.tar.gz", hash = "sha256:e21a9eda979a451935eb3196de3efbe15b9470e6bf9027406d1f6d0ac08b339e", size = 100919, upload-time = "2025-10-16T19:49:17.079Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/87/190df0854bcacc31d58dab28721f855d928ddd1d20c0ca2c201731d4622b/boto3_stubs-1.42.40.tar.gz", hash = "sha256:2689e235ae0deb6878fced175f7c2701fd8c088e6764de65e8c14085c1fc1914", size = 100886, upload-time = "2026-02-02T23:19:28.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/52/ee9dadd1cc8911e16f18ca9fa036a10328e0a0d3fddd54fadcc1ca0f9143/boto3_stubs-1.40.54-py3-none-any.whl", hash = "sha256:548a4786785ba7b43ef4ef1a2a764bebbb0301525f3201091fcf412e4c8ce323", size = 69712, upload-time = "2025-10-16T19:49:12.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/09/e1d031ceae85688c13dd16d84a0e6e416def62c6b23e04f7d318837ee355/boto3_stubs-1.42.40-py3-none-any.whl", hash = "sha256:66679f1075e094b15b2032d8cfc4f070a472e066b04ee1edf61aa44884a6d2cd", size = 69782, upload-time = "2026-02-02T23:19:20.16Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -645,8 +645,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/35/c1/8c4c199ae1663feee579a15861e34f10b29da11ae6ea0ad7b6a847ef3823/botocore-1.40.70.tar.gz", hash = "sha256:61b1f2cecd54d1b28a081116fa113b97bf4e17da57c62ae2c2751fe4c528af1f", size = 14444592, upload-time = "2025-11-10T20:29:04.046Z" }
|
||||
wheels = [
|
||||
@@ -1197,7 +1196,7 @@ requires-dist = [
|
||||
{ name = "crewai-files", marker = "extra == 'file-processing'", editable = "lib/crewai-files" },
|
||||
{ name = "crewai-tools", marker = "extra == 'tools'", editable = "lib/crewai-tools" },
|
||||
{ name = "docling", marker = "extra == 'docling'", specifier = "~=2.75.0" },
|
||||
{ name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.49.0" },
|
||||
{ name = "google-genai", marker = "extra == 'google-genai'", specifier = "~=1.65.0" },
|
||||
{ name = "httpx", specifier = "~=0.28.1" },
|
||||
{ name = "httpx-auth", marker = "extra == 'a2a'", specifier = "~=0.23.1" },
|
||||
{ name = "httpx-sse", marker = "extra == 'a2a'", specifier = "~=0.4.0" },
|
||||
@@ -1227,7 +1226,7 @@ requires-dist = [
|
||||
{ name = "regex", specifier = "~=2026.1.15" },
|
||||
{ name = "textual", specifier = ">=7.5.0" },
|
||||
{ name = "tiktoken", marker = "extra == 'embeddings'", specifier = "~=0.8.0" },
|
||||
{ name = "tokenizers", specifier = "~=0.20.3" },
|
||||
{ name = "tokenizers", specifier = ">=0.21,<1" },
|
||||
{ name = "tomli", specifier = "~=2.0.2" },
|
||||
{ name = "tomli-w", specifier = "~=1.1.0" },
|
||||
{ name = "uv", specifier = "~=0.9.13" },
|
||||
@@ -1276,7 +1275,7 @@ requires-dist = [
|
||||
{ name = "aiofiles", specifier = "~=24.1.0" },
|
||||
{ name = "av", specifier = "~=13.0.0" },
|
||||
{ name = "pillow", specifier = "~=12.1.1" },
|
||||
{ name = "pypdf", specifier = "~=6.7.4" },
|
||||
{ name = "pypdf", specifier = "~=6.7.5" },
|
||||
{ name = "python-magic", specifier = ">=0.4.27" },
|
||||
{ name = "tinytag", specifier = "~=1.10.0" },
|
||||
]
|
||||
@@ -1668,8 +1667,7 @@ source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pywin32", marker = "sys_platform == 'win32'" },
|
||||
{ name = "requests" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/91/9b/4a2ea29aeba62471211598dac5d96825bb49348fa07e906ea930394a83ce/docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c", size = 117834, upload-time = "2024-05-23T11:13:57.216Z" }
|
||||
wheels = [
|
||||
@@ -2249,6 +2247,11 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/83/1d/d6466de3a5249d35e832a52834115ca9d1d0de6abc22065f049707516d47/google_auth-2.48.0-py3-none-any.whl", hash = "sha256:2e2a537873d449434252a9632c28bfc268b0adb1e53f9fb62afc5333a975903f", size = 236499, upload-time = "2026-01-26T19:22:45.099Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
requests = [
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "google-cloud-vision"
|
||||
version = "3.12.1"
|
||||
@@ -2267,21 +2270,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "google-genai"
|
||||
version = "1.49.0"
|
||||
version = "1.65.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
{ name = "google-auth" },
|
||||
{ name = "distro" },
|
||||
{ name = "google-auth", extra = ["requests"] },
|
||||
{ name = "httpx" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "requests" },
|
||||
{ name = "sniffio" },
|
||||
{ name = "tenacity" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "websockets" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/49/1a724ee3c3748fa50721d53a52d9fee88c67d0c43bb16eb2b10ee89ab239/google_genai-1.49.0.tar.gz", hash = "sha256:35eb16023b72e298571ae30e919c810694f258f2ba68fc77a2185c7c8829ad5a", size = 253493, upload-time = "2025-11-05T22:41:03.278Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/f9/cc1191c2540d6a4e24609a586c4ed45d2db57cfef47931c139ee70e5874a/google_genai-1.65.0.tar.gz", hash = "sha256:d470eb600af802d58a79c7f13342d9ea0d05d965007cae8f76c7adff3d7a4750", size = 497206, upload-time = "2026-02-26T00:20:33.824Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/d3/84a152746dc7bdebb8ba0fd7d6157263044acd1d14b2a53e8df4a307b6b7/google_genai-1.49.0-py3-none-any.whl", hash = "sha256:ad49cd5be5b63397069e7aef9a4fe0a84cbdf25fcd93408e795292308db4ef32", size = 256098, upload-time = "2025-11-05T22:41:01.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/3c/3fea4e7c91357c71782d7dcaad7a2577d636c90317e003386893c25bc62c/google_genai-1.65.0-py3-none-any.whl", hash = "sha256:68c025205856919bc03edb0155c11b4b833810b7ce17ad4b7a9eeba5158f6c44", size = 724429, upload-time = "2026-02-26T00:20:32.186Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2662,7 +2667,7 @@ dependencies = [
|
||||
{ name = "jmespath", marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "python-dateutil", marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "requests", marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", marker = "platform_python_implementation == 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/db/e913f210d66c2ad09521925f29754fb9b7240da11238a29a0186ebad4ffa/ibm_cos_sdk_core-2.14.2.tar.gz", hash = "sha256:d594b2af58f70e892aa3b0f6ae4b0fa5d412422c05beeba083d4561b5fad91b4", size = 1103504, upload-time = "2025-06-18T05:03:42.969Z" }
|
||||
|
||||
@@ -2680,7 +2685,7 @@ dependencies = [
|
||||
{ name = "jmespath", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "python-dateutil", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "requests", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/45/80c23aa1e13175a9deefe43cbf8e853a3d3bfc8dfa8b6d6fe83e5785fe21/ibm_cos_sdk_core-2.14.3.tar.gz", hash = "sha256:85dee7790c92e8db69bf39dae4c02cac211e3c1d81bb86e64fa2d1e929674623", size = 1103637, upload-time = "2025-08-01T06:35:41.645Z" }
|
||||
|
||||
@@ -2729,8 +2734,7 @@ dependencies = [
|
||||
{ name = "pandas" },
|
||||
{ name = "requests" },
|
||||
{ name = "tabulate" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/56/2e3df38a1f13062095d7bde23c87a92f3898982993a15186b1bfecbd206f/ibm_watsonx_ai-1.3.42.tar.gz", hash = "sha256:ee5be59009004245d957ce97d1227355516df95a2640189749487614fef674ff", size = 688651, upload-time = "2025-10-01T13:35:41.527Z" }
|
||||
wheels = [
|
||||
@@ -3217,8 +3221,7 @@ dependencies = [
|
||||
{ name = "requests" },
|
||||
{ name = "requests-oauthlib" },
|
||||
{ name = "six" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "websocket-client" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2c/8f/85bf51ad4150f64e8c665daf0d9dfe9787ae92005efb9a4d1cba592bd79d/kubernetes-35.0.0.tar.gz", hash = "sha256:3d00d344944239821458b9efd484d6df9f011da367ecb155dadf9513f05f09ee", size = 1094642, upload-time = "2026-01-16T01:05:27.76Z" }
|
||||
@@ -3246,8 +3249,7 @@ dependencies = [
|
||||
{ name = "pydantic" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e9/64/51622c93ec8c164483c83b68764e5e76e52286c0137a8247bc6a7fac25f4/lance_namespace_urllib3_client-0.5.2.tar.gz", hash = "sha256:8a3a238006e6eabc01fc9d385ac3de22ba933aef0ae8987558f3c3199c9b3799", size = 172578, upload-time = "2026-02-20T03:14:33.031Z" }
|
||||
wheels = [
|
||||
@@ -4145,54 +4147,54 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "mypy"
|
||||
version = "1.19.0"
|
||||
version = "1.19.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "librt" },
|
||||
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "mypy-extensions" },
|
||||
{ name = "pathspec" },
|
||||
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/b5/b58cdc25fadd424552804bf410855d52324183112aa004f0732c5f6324cf/mypy-1.19.0.tar.gz", hash = "sha256:f6b874ca77f733222641e5c46e4711648c4037ea13646fd0cdc814c2eaec2528", size = 3579025, upload-time = "2025-11-28T15:49:01.26Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/8f/55fb488c2b7dabd76e3f30c10f7ab0f6190c1fcbc3e97b1e588ec625bbe2/mypy-1.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6148ede033982a8c5ca1143de34c71836a09f105068aaa8b7d5edab2b053e6c8", size = 13093239, upload-time = "2025-11-28T15:45:11.342Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/1b/278beea978456c56b3262266274f335c3ba5ff2c8108b3b31bec1ffa4c1d/mypy-1.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a9ac09e52bb0f7fb912f5d2a783345c72441a08ef56ce3e17c1752af36340a39", size = 12156128, upload-time = "2025-11-28T15:46:02.566Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/f8/e06f951902e136ff74fd7a4dc4ef9d884faeb2f8eb9c49461235714f079f/mypy-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:11f7254c15ab3f8ed68f8e8f5cbe88757848df793e31c36aaa4d4f9783fd08ab", size = 12753508, upload-time = "2025-11-28T15:44:47.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/5a/d035c534ad86e09cee274d53cf0fd769c0b29ca6ed5b32e205be3c06878c/mypy-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318ba74f75899b0e78b847d8c50821e4c9637c79d9a59680fc1259f29338cb3e", size = 13507553, upload-time = "2025-11-28T15:44:39.26Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/17/c4a5498e00071ef29e483a01558b285d086825b61cf1fb2629fbdd019d94/mypy-1.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf7d84f497f78b682edd407f14a7b6e1a2212b433eedb054e2081380b7395aa3", size = 13792898, upload-time = "2025-11-28T15:44:31.102Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/f6/bb542422b3ee4399ae1cdc463300d2d91515ab834c6233f2fd1d52fa21e0/mypy-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:c3385246593ac2b97f155a0e9639be906e73534630f663747c71908dfbf26134", size = 10048835, upload-time = "2025-11-28T15:48:15.744Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0f/d2/010fb171ae5ac4a01cc34fbacd7544531e5ace95c35ca166dd8fd1b901d0/mypy-1.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a31e4c28e8ddb042c84c5e977e28a21195d086aaffaf08b016b78e19c9ef8106", size = 13010563, upload-time = "2025-11-28T15:48:23.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/41/6b/63f095c9f1ce584fdeb595d663d49e0980c735a1d2004720ccec252c5d47/mypy-1.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34ec1ac66d31644f194b7c163d7f8b8434f1b49719d403a5d26c87fff7e913f7", size = 12077037, upload-time = "2025-11-28T15:47:51.582Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/83/6cb93d289038d809023ec20eb0b48bbb1d80af40511fa077da78af6ff7c7/mypy-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb64b0ba5980466a0f3f9990d1c582bcab8db12e29815ecb57f1408d99b4bff7", size = 12680255, upload-time = "2025-11-28T15:46:57.628Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/db/d217815705987d2cbace2edd9100926196d6f85bcb9b5af05058d6e3c8ad/mypy-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:120cffe120cca5c23c03c77f84abc0c14c5d2e03736f6c312480020082f1994b", size = 13421472, upload-time = "2025-11-28T15:47:59.655Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4e/51/d2beaca7c497944b07594f3f8aad8d2f0e8fc53677059848ae5d6f4d193e/mypy-1.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a500ab5c444268a70565e374fc803972bfd1f09545b13418a5174e29883dab7", size = 13651823, upload-time = "2025-11-28T15:45:29.318Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/d1/7883dcf7644db3b69490f37b51029e0870aac4a7ad34d09ceae709a3df44/mypy-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:c14a98bc63fd867530e8ec82f217dae29d0550c86e70debc9667fff1ec83284e", size = 10049077, upload-time = "2025-11-28T15:45:39.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/7e/1afa8fb188b876abeaa14460dc4983f909aaacaa4bf5718c00b2c7e0b3d5/mypy-1.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0fb3115cb8fa7c5f887c8a8d81ccdcb94cff334684980d847e5a62e926910e1d", size = 13207728, upload-time = "2025-11-28T15:46:26.463Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/13/f103d04962bcbefb1644f5ccb235998b32c337d6c13145ea390b9da47f3e/mypy-1.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3e19e3b897562276bb331074d64c076dbdd3e79213f36eed4e592272dabd760", size = 12202945, upload-time = "2025-11-28T15:48:49.143Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/93/a86a5608f74a22284a8ccea8592f6e270b61f95b8588951110ad797c2ddd/mypy-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9d491295825182fba01b6ffe2c6fe4e5a49dbf4e2bb4d1217b6ced3b4797bc6", size = 12718673, upload-time = "2025-11-28T15:47:37.193Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/58/cf08fff9ced0423b858f2a7495001fda28dc058136818ee9dffc31534ea9/mypy-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6016c52ab209919b46169651b362068f632efcd5eb8ef9d1735f6f86da7853b2", size = 13608336, upload-time = "2025-11-28T15:48:32.625Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/ed/9c509105c5a6d4b73bb08733102a3ea62c25bc02c51bca85e3134bf912d3/mypy-1.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f188dcf16483b3e59f9278c4ed939ec0254aa8a60e8fc100648d9ab5ee95a431", size = 13833174, upload-time = "2025-11-28T15:45:48.091Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/71/01939b66e35c6f8cb3e6fdf0b657f0fd24de2f8ba5e523625c8e72328208/mypy-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:0e3c3d1e1d62e678c339e7ade72746a9e0325de42cd2cccc51616c7b2ed1a018", size = 10112208, upload-time = "2025-11-28T15:46:41.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/0d/a1357e6bb49e37ce26fcf7e3cc55679ce9f4ebee0cd8b6ee3a0e301a9210/mypy-1.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7686ed65dbabd24d20066f3115018d2dce030d8fa9db01aa9f0a59b6813e9f9e", size = 13191993, upload-time = "2025-11-28T15:47:22.336Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/75/8e5d492a879ec4490e6ba664b5154e48c46c85b5ac9785792a5ec6a4d58f/mypy-1.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fd4a985b2e32f23bead72e2fb4bbe5d6aceee176be471243bd831d5b2644672d", size = 12174411, upload-time = "2025-11-28T15:44:55.492Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/31/ad5dcee9bfe226e8eaba777e9d9d251c292650130f0450a280aec3485370/mypy-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc51a5b864f73a3a182584b1ac75c404396a17eced54341629d8bdcb644a5bba", size = 12727751, upload-time = "2025-11-28T15:44:14.169Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/06/b6b8994ce07405f6039701f4b66e9d23f499d0b41c6dd46ec28f96d57ec3/mypy-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37af5166f9475872034b56c5efdcf65ee25394e9e1d172907b84577120714364", size = 13593323, upload-time = "2025-11-28T15:46:34.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/b1/126e274484cccdf099a8e328d4fda1c7bdb98a5e888fa6010b00e1bbf330/mypy-1.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:510c014b722308c9bd377993bcbf9a07d7e0692e5fa8fc70e639c1eb19fc6bee", size = 13818032, upload-time = "2025-11-28T15:46:18.286Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/56/53a8f70f562dfc466c766469133a8a4909f6c0012d83993143f2a9d48d2d/mypy-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:cabbee74f29aa9cd3b444ec2f1e4fa5a9d0d746ce7567a6a609e224429781f53", size = 10120644, upload-time = "2025-11-28T15:47:43.99Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/0e/fe228ed5aeab470c6f4eb82481837fadb642a5aa95cc8215fd2214822c10/mypy-1.19.0-py3-none-any.whl", hash = "sha256:0c01c99d626380752e527d5ce8e69ffbba2046eb8a060db0329690849cf9b6f9", size = 2469714, upload-time = "2025-11-28T15:45:33.22Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mypy-boto3-bedrock-runtime"
|
||||
version = "1.40.76"
|
||||
version = "1.42.42"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.12'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/db/cc668a48a27973df31c7044a6785bd0e8691b1a0419dae001c4c29f1c98f/mypy_boto3_bedrock_runtime-1.40.76.tar.gz", hash = "sha256:52f2a2b3955eb9f4f0d075398f2d430abcc6bf56ff00815b94e3371e66030059", size = 28428, upload-time = "2025-11-18T21:42:43.41Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/bb/65dc1b2c5796a6ab5f60bdb57343bd6c3ecb82251c580eca415c8548333e/mypy_boto3_bedrock_runtime-1.42.42.tar.gz", hash = "sha256:3a4088218478b6fbbc26055c03c95bee4fc04624a801090b3cce3037e8275c8d", size = 29840, upload-time = "2026-02-04T20:53:05.999Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/05/6f/8b04729224a76952e08406eccbbbebfa75ee7df91313279d76428f13fdc2/mypy_boto3_bedrock_runtime-1.40.76-py3-none-any.whl", hash = "sha256:0347f6d78e342d640da74bbd6158b276c5cb39ef73405084a65fe490766b6dab", size = 34454, upload-time = "2025-11-18T21:42:42.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/43/7ea062f2228f47b5779dcfa14dab48d6e29f979b35d1a5102b0ba80b9c1b/mypy_boto3_bedrock_runtime-1.42.42-py3-none-any.whl", hash = "sha256:b2d16eae22607d0685f90796b3a0afc78c0b09d45872e00eafd634a31dd9358f", size = 36077, upload-time = "2026-02-04T20:53:01.768Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -5259,7 +5261,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pre-commit"
|
||||
version = "4.5.0"
|
||||
version = "4.5.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cfgv" },
|
||||
@@ -5268,9 +5270,9 @@ dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "virtualenv" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f4/9b/6a4ffb4ed980519da959e1cf3122fc6cb41211daa58dbae1c73c0e519a37/pre_commit-4.5.0.tar.gz", hash = "sha256:dc5a065e932b19fc1d4c653c6939068fe54325af8e741e74e88db4d28a4dd66b", size = 198428, upload-time = "2025-11-22T21:02:42.304Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/c4/b2d28e9d2edf4f1713eb3c29307f1a63f3d67cf09bdda29715a36a68921a/pre_commit-4.5.0-py2.py3-none-any.whl", hash = "sha256:25e2ce09595174d9c97860a95609f9f852c0614ba602de3561e267547f2335e1", size = 226429, upload-time = "2025-11-22T21:02:40.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6169,14 +6171,14 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pypdf"
|
||||
version = "6.7.4"
|
||||
version = "6.7.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/09/dc/f52deef12797ad58b88e4663f097a343f53b9361338aef6573f135ac302f/pypdf-6.7.4.tar.gz", hash = "sha256:9edd1cd47938bb35ec87795f61225fd58a07cfaf0c5699018ae1a47d6f8ab0e3", size = 5304821, upload-time = "2026-02-27T10:44:39.395Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/52/37cc0aa9e9d1bf7729a737a0d83f8b3f851c8eb137373d9f71eafb0a3405/pypdf-6.7.5.tar.gz", hash = "sha256:40bb2e2e872078655f12b9b89e2f900888bb505e88a82150b64f9f34fa25651d", size = 5304278, upload-time = "2026-03-02T09:05:21.464Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/be/cded021305f5c81b47265b8c5292b99388615a4391c21ff00fd538d34a56/pypdf-6.7.4-py3-none-any.whl", hash = "sha256:527d6da23274a6c70a9cb59d1986d93946ba8e36a6bc17f3f7cce86331492dda", size = 331496, upload-time = "2026-02-27T10:44:37.527Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/89/336673efd0a88956562658aba4f0bbef7cb92a6fbcbcaf94926dbc82b408/pypdf-6.7.5-py3-none-any.whl", hash = "sha256:07ba7f1d6e6d9aa2a17f5452e320a84718d4ce863367f7ede2fd72280349ab13", size = 331421, upload-time = "2026-03-02T09:05:19.722Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6248,15 +6250,6 @@ dependencies = [
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/12/a0/d0638470df605ce266991fb04f74c69ab1bed3b90ac3838e9c3c8b69b66a/Pysher-1.0.8.tar.gz", hash = "sha256:7849c56032b208e49df67d7bd8d49029a69042ab0bb45b2ed59fa08f11ac5988", size = 9071, upload-time = "2022-10-10T13:41:09.936Z" }
|
||||
|
||||
[[package]]
|
||||
name = "pysocks"
|
||||
version = "1.7.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bd/11/293dd436aea955d45fc4e8a35b6ae7270f5b8e00b53cf6c024c83b657a11/PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0", size = 284429, upload-time = "2019-09-20T02:07:35.714Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/59/b4572118e098ac8e46e399a1dd0f2d85403ce8bbaad9ec79373ed6badaf9/PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", size = 16725, upload-time = "2019-09-20T02:06:22.938Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "8.4.2"
|
||||
@@ -6547,8 +6540,7 @@ dependencies = [
|
||||
{ name = "portalocker" },
|
||||
{ name = "protobuf" },
|
||||
{ name = "pydantic" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/56/3f355f931c239c260b4fe3bd6433ec6c9e6185cd5ae0970fe89d0ca6daee/qdrant_client-1.14.3.tar.gz", hash = "sha256:bb899e3e065b79c04f5e47053d59176150c0a5dabc09d7f476c8ce8e52f4d281", size = 286766, upload-time = "2025-06-16T11:13:47.838Z" }
|
||||
wheels = [
|
||||
@@ -6773,8 +6765,7 @@ dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" }
|
||||
wheels = [
|
||||
@@ -6942,28 +6933,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.14.7"
|
||||
version = "0.15.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b7/5b/dd7406afa6c95e3d8fa9d652b6d6dd17dd4a6bf63cb477014e8ccd3dcd46/ruff-0.14.7.tar.gz", hash = "sha256:3417deb75d23bd14a722b57b0a1435561db65f0ad97435b4cf9f85ffcef34ae5", size = 5727324, upload-time = "2025-11-28T20:55:10.525Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/04/dc/4e6ac71b511b141cf626357a3946679abeba4cf67bc7cc5a17920f31e10d/ruff-0.15.1.tar.gz", hash = "sha256:c590fe13fb57c97141ae975c03a1aedb3d3156030cabd740d6ff0b0d601e203f", size = 4540855, upload-time = "2026-02-12T23:09:09.998Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/b1/7ea5647aaf90106f6d102230e5df874613da43d1089864da1553b899ba5e/ruff-0.14.7-py3-none-linux_armv6l.whl", hash = "sha256:b9d5cb5a176c7236892ad7224bc1e63902e4842c460a0b5210701b13e3de4fca", size = 13414475, upload-time = "2025-11-28T20:54:54.569Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/af/19/fddb4cd532299db9cdaf0efdc20f5c573ce9952a11cb532d3b859d6d9871/ruff-0.14.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3f64fe375aefaf36ca7d7250292141e39b4cea8250427482ae779a2aa5d90015", size = 13634613, upload-time = "2025-11-28T20:55:17.54Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/2b/469a66e821d4f3de0440676ed3e04b8e2a1dc7575cf6fa3ba6d55e3c8557/ruff-0.14.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:93e83bd3a9e1a3bda64cb771c0d47cda0e0d148165013ae2d3554d718632d554", size = 12765458, upload-time = "2025-11-28T20:55:26.128Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/05/0b001f734fe550bcfde4ce845948ac620ff908ab7241a39a1b39bb3c5f49/ruff-0.14.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3838948e3facc59a6070795de2ae16e5786861850f78d5914a03f12659e88f94", size = 13236412, upload-time = "2025-11-28T20:55:28.602Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/36/8ed15d243f011b4e5da75cd56d6131c6766f55334d14ba31cce5461f28aa/ruff-0.14.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:24c8487194d38b6d71cd0fd17a5b6715cda29f59baca1defe1e3a03240f851d1", size = 13182949, upload-time = "2025-11-28T20:55:33.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/cf/fcb0b5a195455729834f2a6eadfe2e4519d8ca08c74f6d2b564a4f18f553/ruff-0.14.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79c73db6833f058a4be8ffe4a0913b6d4ad41f6324745179bd2aa09275b01d0b", size = 13816470, upload-time = "2025-11-28T20:55:08.203Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7f/5d/34a4748577ff7a5ed2f2471456740f02e86d1568a18c9faccfc73bd9ca3f/ruff-0.14.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:12eb7014fccff10fc62d15c79d8a6be4d0c2d60fe3f8e4d169a0d2def75f5dad", size = 15289621, upload-time = "2025-11-28T20:55:30.837Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/53/0a9385f047a858ba133d96f3f8e3c9c66a31cc7c4b445368ef88ebeac209/ruff-0.14.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c623bbdc902de7ff715a93fa3bb377a4e42dd696937bf95669118773dbf0c50", size = 14975817, upload-time = "2025-11-28T20:55:24.107Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/d7/2f1c32af54c3b46e7fadbf8006d8b9bcfbea535c316b0bd8813d6fb25e5d/ruff-0.14.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f53accc02ed2d200fa621593cdb3c1ae06aa9b2c3cae70bc96f72f0000ae97a9", size = 14284549, upload-time = "2025-11-28T20:55:06.08Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/05/434ddd86becd64629c25fb6b4ce7637dd52a45cc4a4415a3008fe61c27b9/ruff-0.14.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:281f0e61a23fcdcffca210591f0f53aafaa15f9025b5b3f9706879aaa8683bc4", size = 14071389, upload-time = "2025-11-28T20:55:35.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/50/fdf89d4d80f7f9d4f420d26089a79b3bb1538fe44586b148451bc2ba8d9c/ruff-0.14.7-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dbbaa5e14148965b91cb090236931182ee522a5fac9bc5575bafc5c07b9f9682", size = 14202679, upload-time = "2025-11-28T20:55:01.472Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/54/87b34988984555425ce967f08a36df0ebd339bb5d9d0e92a47e41151eafc/ruff-0.14.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1464b6e54880c0fe2f2d6eaefb6db15373331414eddf89d6b903767ae2458143", size = 13147677, upload-time = "2025-11-28T20:55:19.933Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/29/f55e4d44edfe053918a16a3299e758e1c18eef216b7a7092550d7a9ec51c/ruff-0.14.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f217ed871e4621ea6128460df57b19ce0580606c23aeab50f5de425d05226784", size = 13151392, upload-time = "2025-11-28T20:55:21.967Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/69/47aae6dbd4f1d9b4f7085f4d9dcc84e04561ee7ad067bf52e0f9b02e3209/ruff-0.14.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6be02e849440ed3602d2eb478ff7ff07d53e3758f7948a2a598829660988619e", size = 13412230, upload-time = "2025-11-28T20:55:12.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/4b/6e96cb6ba297f2ba502a231cd732ed7c3de98b1a896671b932a5eefa3804/ruff-0.14.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:19a0f116ee5e2b468dfe80c41c84e2bbd6b74f7b719bee86c2ecde0a34563bcc", size = 14195397, upload-time = "2025-11-28T20:54:56.896Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/82/251d5f1aa4dcad30aed491b4657cecd9fb4274214da6960ffec144c260f7/ruff-0.14.7-py3-none-win32.whl", hash = "sha256:e33052c9199b347c8937937163b9b149ef6ab2e4bb37b042e593da2e6f6cccfa", size = 13126751, upload-time = "2025-11-28T20:55:03.47Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/b5/d0b7d145963136b564806f6584647af45ab98946660d399ec4da79cae036/ruff-0.14.7-py3-none-win_amd64.whl", hash = "sha256:e17a20ad0d3fad47a326d773a042b924d3ac31c6ca6deb6c72e9e6b5f661a7c6", size = 14531726, upload-time = "2025-11-28T20:54:59.121Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/d2/1637f4360ada6a368d3265bf39f2cf737a0aaab15ab520fc005903e883f8/ruff-0.14.7-py3-none-win_arm64.whl", hash = "sha256:be4d653d3bea1b19742fcc6502354e32f65cd61ff2fbdb365803ef2c2aec6228", size = 13609215, upload-time = "2025-11-28T20:55:15.375Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/bf/e6e4324238c17f9d9120a9d60aa99a7daaa21204c07fcd84e2ef03bb5fd1/ruff-0.15.1-py3-none-linux_armv6l.whl", hash = "sha256:b101ed7cf4615bda6ffe65bdb59f964e9f4a0d3f85cbf0e54f0ab76d7b90228a", size = 10367819, upload-time = "2026-02-12T23:09:03.598Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/ea/c8f89d32e7912269d38c58f3649e453ac32c528f93bb7f4219258be2e7ed/ruff-0.15.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:939c995e9277e63ea632cc8d3fae17aa758526f49a9a850d2e7e758bfef46602", size = 10798618, upload-time = "2026-02-12T23:09:22.928Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5e/0f/1d0d88bc862624247d82c20c10d4c0f6bb2f346559d8af281674cf327f15/ruff-0.15.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1d83466455fdefe60b8d9c8df81d3c1bbb2115cede53549d3b522ce2bc703899", size = 10148518, upload-time = "2026-02-12T23:08:58.339Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f5/c8/291c49cefaa4a9248e986256df2ade7add79388fe179e0691be06fae6f37/ruff-0.15.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9457e3c3291024866222b96108ab2d8265b477e5b1534c7ddb1810904858d16", size = 10518811, upload-time = "2026-02-12T23:09:31.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/1a/f5707440e5ae43ffa5365cac8bbb91e9665f4a883f560893829cf16a606b/ruff-0.15.1-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:92c92b003e9d4f7fbd33b1867bb15a1b785b1735069108dfc23821ba045b29bc", size = 10196169, upload-time = "2026-02-12T23:09:17.306Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/ff/26ddc8c4da04c8fd3ee65a89c9fb99eaa5c30394269d424461467be2271f/ruff-0.15.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fe5c41ab43e3a06778844c586251eb5a510f67125427625f9eb2b9526535779", size = 10990491, upload-time = "2026-02-12T23:09:25.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/00/50920cb385b89413f7cdb4bb9bc8fc59c1b0f30028d8bccc294189a54955/ruff-0.15.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:66a6dd6df4d80dc382c6484f8ce1bcceb55c32e9f27a8b94c32f6c7331bf14fb", size = 11843280, upload-time = "2026-02-12T23:09:19.88Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/6d/2f5cad8380caf5632a15460c323ae326f1e1a2b5b90a6ee7519017a017ca/ruff-0.15.1-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a4a42cbb8af0bda9bcd7606b064d7c0bc311a88d141d02f78920be6acb5aa83", size = 11274336, upload-time = "2026-02-12T23:09:14.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/1d/5f56cae1d6c40b8a318513599b35ea4b075d7dc1cd1d04449578c29d1d75/ruff-0.15.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ab064052c31dddada35079901592dfba2e05f5b1e43af3954aafcbc1096a5b2", size = 11137288, upload-time = "2026-02-12T23:09:07.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/20/6f8d7d8f768c93b0382b33b9306b3b999918816da46537d5a61635514635/ruff-0.15.1-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5631c940fe9fe91f817a4c2ea4e81f47bee3ca4aa646134a24374f3c19ad9454", size = 11070681, upload-time = "2026-02-12T23:08:55.43Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/67/d640ac76069f64cdea59dba02af2e00b1fa30e2103c7f8d049c0cff4cafd/ruff-0.15.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68138a4ba184b4691ccdc39f7795c66b3c68160c586519e7e8444cf5a53e1b4c", size = 10486401, upload-time = "2026-02-12T23:09:27.927Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/3d/e1429f64a3ff89297497916b88c32a5cc88eeca7e9c787072d0e7f1d3e1e/ruff-0.15.1-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:518f9af03bfc33c03bdb4cb63fabc935341bb7f54af500f92ac309ecfbba6330", size = 10197452, upload-time = "2026-02-12T23:09:12.147Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/83/e2c3bade17dad63bf1e1c2ffaf11490603b760be149e1419b07049b36ef2/ruff-0.15.1-py3-none-musllinux_1_2_i686.whl", hash = "sha256:da79f4d6a826caaea95de0237a67e33b81e6ec2e25fc7e1993a4015dffca7c61", size = 10693900, upload-time = "2026-02-12T23:09:34.418Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/27/fdc0e11a813e6338e0706e8b39bb7a1d61ea5b36873b351acee7e524a72a/ruff-0.15.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3dd86dccb83cd7d4dcfac303ffc277e6048600dfc22e38158afa208e8bf94a1f", size = 11227302, upload-time = "2026-02-12T23:09:36.536Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/58/ac864a75067dcbd3b95be5ab4eb2b601d7fbc3d3d736a27e391a4f92a5c1/ruff-0.15.1-py3-none-win32.whl", hash = "sha256:660975d9cb49b5d5278b12b03bb9951d554543a90b74ed5d366b20e2c57c2098", size = 10462555, upload-time = "2026-02-12T23:09:29.899Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/5e/d4ccc8a27ecdb78116feac4935dfc39d1304536f4296168f91ed3ec00cd2/ruff-0.15.1-py3-none-win_amd64.whl", hash = "sha256:c820fef9dd5d4172a6570e5721704a96c6679b80cf7be41659ed439653f62336", size = 11599956, upload-time = "2026-02-12T23:09:01.157Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/07/5bda6a85b220c64c65686bc85bd0bbb23b29c62b3a9f9433fa55f17cda93/ruff-0.15.1-py3-none-win_arm64.whl", hash = "sha256:5ff7d5f0f88567850f45081fac8f4ec212be8d0b963e385c3f7d0d2eb4899416", size = 10874604, upload-time = "2026-02-12T23:09:05.515Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7094,8 +7084,7 @@ dependencies = [
|
||||
{ name = "loguru" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "requests" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/17/40/f2baf15372fba9e67c0f918ea9d753916bf875019ead972cd76e8aa0ff1b/scrapfly_sdk-0.8.24.tar.gz", hash = "sha256:84fb0a22c3df9cf3aca9bdc1ed191419e27d92a055ae70d06147ac0ced7ee654", size = 42460, upload-time = "2026-01-07T11:10:50.236Z" }
|
||||
wheels = [
|
||||
@@ -7117,7 +7106,7 @@ dependencies = [
|
||||
{ name = "trio", marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "trio-websocket", marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "typing-extensions", marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, extra = ["socks"], marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "websocket-client", marker = "platform_python_implementation == 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/54/2d/fafffe946099033ccf22bf89e12eede14c1d3c5936110c5f6f2b9830722c/selenium-4.32.0.tar.gz", hash = "sha256:b9509bef4056f4083772abb1ae19ff57247d617a29255384b26be6956615b206", size = 870997, upload-time = "2025-05-02T20:35:27.325Z" }
|
||||
@@ -7143,7 +7132,7 @@ dependencies = [
|
||||
{ name = "types-certifi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "types-urllib3", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "typing-extensions", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, extra = ["socks"], marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3", marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "websocket-client", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/ef/a5727fa7b33d20d296322adf851b76072d8d3513e1b151969d3228437faf/selenium-4.40.0.tar.gz", hash = "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c", size = 930444, upload-time = "2026-01-18T23:12:31.565Z" }
|
||||
@@ -7179,8 +7168,7 @@ version = "2.52.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/59/eb/1b497650eb564701f9a7b8a95c51b2abe9347ed2c0b290ba78f027ebe4ea/sentry_sdk-2.52.0.tar.gz", hash = "sha256:fa0bec872cfec0302970b2996825723d67390cdd5f0229fb9efed93bd5384899", size = 410273, upload-time = "2026-02-04T15:03:54.706Z" }
|
||||
wheels = [
|
||||
@@ -7339,6 +7327,7 @@ dependencies = [
|
||||
{ name = "sortedcontainers" },
|
||||
{ name = "tomlkit" },
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/13/d2/4ae9fc7a0df36ad0ac06bc959757dfbfc58f160f58e1d62e7cebe9901fc7/snowflake_connector_python-4.2.0.tar.gz", hash = "sha256:74b1028caee3af4550a366ef89b33de80940bbf856844dd4d788a6b7a6511aff", size = 915327, upload-time = "2026-01-07T16:44:32.541Z" }
|
||||
wheels = [
|
||||
@@ -7643,68 +7632,32 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tokenizers"
|
||||
version = "0.20.3"
|
||||
version = "0.22.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "huggingface-hub" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/da/25/b1681c1c30ea3ea6e584ae3fffd552430b12faa599b558c4c4783f56d7ff/tokenizers-0.20.3.tar.gz", hash = "sha256:2278b34c5d0dd78e087e1ca7f9b1dcbf129d80211afa645f214bd6e051037539", size = 340513, upload-time = "2024-11-05T17:34:10.403Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/51/421bb0052fc4333f7c1e3231d8c6607552933d919b628c8fabd06f60ba1e/tokenizers-0.20.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:31ccab28dbb1a9fe539787210b0026e22debeab1662970f61c2d921f7557f7e4", size = 2674308, upload-time = "2024-11-05T17:30:25.423Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a6/e9/f651f8d27614fd59af387f4dfa568b55207e5fac8d06eec106dc00b921c4/tokenizers-0.20.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c6361191f762bda98c773da418cf511cbaa0cb8d0a1196f16f8c0119bde68ff8", size = 2559363, upload-time = "2024-11-05T17:30:28.841Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/e8/0e9f81a09ab79f409eabfd99391ca519e315496694671bebca24c3e90448/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f128d5da1202b78fa0a10d8d938610472487da01b57098d48f7e944384362514", size = 2892896, upload-time = "2024-11-05T17:30:30.429Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b0/72/15fdbc149e05005e99431ecd471807db2241983deafe1e704020f608f40e/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79c4121a2e9433ad7ef0769b9ca1f7dd7fa4c0cd501763d0a030afcbc6384481", size = 2802785, upload-time = "2024-11-05T17:30:32.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/44/1f8aea48f9bb117d966b7272484671b33a509f6217a8e8544d79442c90db/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7850fde24197fe5cd6556e2fdba53a6d3bae67c531ea33a3d7c420b90904141", size = 3086060, upload-time = "2024-11-05T17:30:34.11Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/83/82ba40da99870b3a0b801cffaf4f099f088a84c7e07d32cc6ca751ce08e6/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b357970c095dc134978a68c67d845a1e3803ab7c4fbb39195bde914e7e13cf8b", size = 3096760, upload-time = "2024-11-05T17:30:36.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/46/7a025404201d937f86548928616c0a164308aa3998e546efdf798bf5ee9c/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a333d878c4970b72d6c07848b90c05f6b045cf9273fc2bc04a27211721ad6118", size = 3380165, upload-time = "2024-11-05T17:30:37.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/49/15fae66ac62e49255eeedbb7f4127564b2c3f3aef2009913f525732d1a08/tokenizers-0.20.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1fd9fee817f655a8f50049f685e224828abfadd436b8ff67979fc1d054b435f1", size = 2994038, upload-time = "2024-11-05T17:30:40.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/64/693afc9ba2393c2eed85c02bacb44762f06a29f0d1a5591fa5b40b39c0a2/tokenizers-0.20.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9e7816808b402129393a435ea2a509679b41246175d6e5e9f25b8692bfaa272b", size = 8977285, upload-time = "2024-11-05T17:30:42.095Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/7e/6126c18694310fe07970717929e889898767c41fbdd95b9078e8aec0f9ef/tokenizers-0.20.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba96367db9d8a730d3a1d5996b4b7babb846c3994b8ef14008cd8660f55db59d", size = 9294890, upload-time = "2024-11-05T17:30:44.563Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/7d/5e3307a1091c8608a1e58043dff49521bc19553c6e9548c7fac6840cc2c4/tokenizers-0.20.3-cp310-none-win32.whl", hash = "sha256:ee31ba9d7df6a98619426283e80c6359f167e2e9882d9ce1b0254937dbd32f3f", size = 2196883, upload-time = "2024-11-05T17:30:46.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/62/aaf5b2a526b3b10c20985d9568ff8c8f27159345eaef3347831e78cd5894/tokenizers-0.20.3-cp310-none-win_amd64.whl", hash = "sha256:a845c08fdad554fe0871d1255df85772f91236e5fd6b9287ef8b64f5807dbd0c", size = 2381637, upload-time = "2024-11-05T17:30:48.156Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/93/6742ef9206409d5ce1fdf44d5ca1687cdc3847ba0485424e2c731e6bcf67/tokenizers-0.20.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:585b51e06ca1f4839ce7759941e66766d7b060dccfdc57c4ca1e5b9a33013a90", size = 2674224, upload-time = "2024-11-05T17:30:49.972Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/aa/14/e75ece72e99f6ef9ae07777ca9fdd78608f69466a5cecf636e9bd2f25d5c/tokenizers-0.20.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61cbf11954f3b481d08723ebd048ba4b11e582986f9be74d2c3bdd9293a4538d", size = 2558991, upload-time = "2024-11-05T17:30:51.666Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/54/033b5b2ba0c3ae01e026c6f7ced147d41a2fa1c573d00a66cb97f6d7f9b3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef820880d5e4e8484e2fa54ff8d297bb32519eaa7815694dc835ace9130a3eea", size = 2892476, upload-time = "2024-11-05T17:30:53.505Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/b0/cc369fb3297d61f3311cab523d16d48c869dc2f0ba32985dbf03ff811041/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:67ef4dcb8841a4988cd00dd288fb95dfc8e22ed021f01f37348fd51c2b055ba9", size = 2802775, upload-time = "2024-11-05T17:30:55.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/74/62ad983e8ea6a63e04ed9c5be0b605056bf8aac2f0125f9b5e0b3e2b89fa/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff1ef8bd47a02b0dc191688ccb4da53600df5d4c9a05a4b68e1e3de4823e78eb", size = 3086138, upload-time = "2024-11-05T17:30:57.332Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/ac/4637ba619db25094998523f9e6f5b456e1db1f8faa770a3d925d436db0c3/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:444d188186eab3148baf0615b522461b41b1f0cd58cd57b862ec94b6ac9780f1", size = 3098076, upload-time = "2024-11-05T17:30:59.455Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/ce/9793f2dc2ce529369807c9c74e42722b05034af411d60f5730b720388c7d/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37c04c032c1442740b2c2d925f1857885c07619224a533123ac7ea71ca5713da", size = 3379650, upload-time = "2024-11-05T17:31:01.264Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/f6/2841de926bc4118af996eaf0bdf0ea5b012245044766ffc0347e6c968e63/tokenizers-0.20.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:453c7769d22231960ee0e883d1005c93c68015025a5e4ae56275406d94a3c907", size = 2994005, upload-time = "2024-11-05T17:31:02.985Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a3/b2/00915c4fed08e9505d37cf6eaab45b12b4bff8f6719d459abcb9ead86a4b/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4bb31f7b2847e439766aaa9cc7bccf7ac7088052deccdb2275c952d96f691c6a", size = 8977488, upload-time = "2024-11-05T17:31:04.424Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e9/ac/1c069e7808181ff57bcf2d39e9b6fbee9133a55410e6ebdaa89f67c32e83/tokenizers-0.20.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:843729bf0f991b29655a069a2ff58a4c24375a553c70955e15e37a90dd4e045c", size = 9294935, upload-time = "2024-11-05T17:31:06.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/47/722feb70ee68d1c4412b12d0ea4acc2713179fd63f054913990f9e259492/tokenizers-0.20.3-cp311-none-win32.whl", hash = "sha256:efcce3a927b1e20ca694ba13f7a68c59b0bd859ef71e441db68ee42cf20c2442", size = 2197175, upload-time = "2024-11-05T17:31:09.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/68/1b4f928b15a36ed278332ac75d66d7eb65d865bf344d049c452c18447bf9/tokenizers-0.20.3-cp311-none-win_amd64.whl", hash = "sha256:88301aa0801f225725b6df5dea3d77c80365ff2362ca7e252583f2b4809c4cc0", size = 2381616, upload-time = "2024-11-05T17:31:10.685Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/00/92a08af2a6b0c88c50f1ab47d7189e695722ad9714b0ee78ea5e1e2e1def/tokenizers-0.20.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:49d12a32e190fad0e79e5bdb788d05da2f20d8e006b13a70859ac47fecf6ab2f", size = 2667951, upload-time = "2024-11-05T17:31:12.356Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/9a/e17a352f0bffbf415cf7d73756f5c73a3219225fc5957bc2f39d52c61684/tokenizers-0.20.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:282848cacfb9c06d5e51489f38ec5aa0b3cd1e247a023061945f71f41d949d73", size = 2555167, upload-time = "2024-11-05T17:31:13.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/27/37/d108df55daf4f0fcf1f58554692ff71687c273d870a34693066f0847be96/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abe4e08c7d0cd6154c795deb5bf81d2122f36daf075e0c12a8b050d824ef0a64", size = 2898389, upload-time = "2024-11-05T17:31:15.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/27/32f29da16d28f59472fa7fb38e7782069748c7e9ab9854522db20341624c/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca94fc1b73b3883c98f0c88c77700b13d55b49f1071dfd57df2b06f3ff7afd64", size = 2795866, upload-time = "2024-11-05T17:31:16.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/4e/8a9a3c89e128c4a40f247b501c10279d2d7ade685953407c4d94c8c0f7a7/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef279c7e239f95c8bdd6ff319d9870f30f0d24915b04895f55b1adcf96d6c60d", size = 3085446, upload-time = "2024-11-05T17:31:18.392Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/3b/a2a7962c496ebcd95860ca99e423254f760f382cd4bd376f8895783afaf5/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16384073973f6ccbde9852157a4fdfe632bb65208139c9d0c0bd0176a71fd67f", size = 3094378, upload-time = "2024-11-05T17:31:20.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/f4/a8a33f0192a1629a3bd0afcad17d4d221bbf9276da4b95d226364208d5eb/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:312d522caeb8a1a42ebdec87118d99b22667782b67898a76c963c058a7e41d4f", size = 3385755, upload-time = "2024-11-05T17:31:21.778Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/65/c83cb3545a65a9eaa2e13b22c93d5e00bd7624b354a44adbdc93d5d9bd91/tokenizers-0.20.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f2b7cb962564785a83dafbba0144ecb7f579f1d57d8c406cdaa7f32fe32f18ad", size = 2997679, upload-time = "2024-11-05T17:31:23.134Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/e9/a80d4e592307688a67c7c59ab77e03687b6a8bd92eb5db763a2c80f93f57/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:124c5882ebb88dadae1fc788a582299fcd3a8bd84fc3e260b9918cf28b8751f5", size = 8989296, upload-time = "2024-11-05T17:31:24.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/90/af/60c957af8d2244321124e893828f1a4817cde1a2d08d09d423b73f19bd2f/tokenizers-0.20.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2b6e54e71f84c4202111a489879005cb14b92616a87417f6c102c833af961ea2", size = 9303621, upload-time = "2024-11-05T17:31:27.341Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/a9/96172310ee141009646d63a1ca267c099c462d747fe5ef7e33f74e27a683/tokenizers-0.20.3-cp312-none-win32.whl", hash = "sha256:83d9bfbe9af86f2d9df4833c22e94d94750f1d0cd9bfb22a7bb90a86f61cdb1c", size = 2188979, upload-time = "2024-11-05T17:31:29.483Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bd/68/61d85ae7ae96dde7d0974ff3538db75d5cdc29be2e4329cd7fc51a283e22/tokenizers-0.20.3-cp312-none-win_amd64.whl", hash = "sha256:44def74cee574d609a36e17c8914311d1b5dbcfe37c55fd29369d42591b91cf2", size = 2380725, upload-time = "2024-11-05T17:31:31.315Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/07/19/36e9eaafb229616cb8502b42030fa7fe347550e76cb618de71b498fc3222/tokenizers-0.20.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0b630e0b536ef0e3c8b42c685c1bc93bd19e98c0f1543db52911f8ede42cf84", size = 2666813, upload-time = "2024-11-05T17:31:32.783Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/c7/e2ce1d4f756c8a62ef93fdb4df877c2185339b6d63667b015bf70ea9d34b/tokenizers-0.20.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a02d160d2b19bcbfdf28bd9a4bf11be4cb97d0499c000d95d4c4b1a4312740b6", size = 2555354, upload-time = "2024-11-05T17:31:34.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/cf/5309c2d173a6a67f9ec8697d8e710ea32418de6fd8541778032c202a1c3e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e3d80d89b068bc30034034b5319218c7c0a91b00af19679833f55f3becb6945", size = 2897745, upload-time = "2024-11-05T17:31:35.733Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/e5/af3078e32f225e680e69d61f78855880edb8d53f5850a1834d519b2b103f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:174a54910bed1b089226512b4458ea60d6d6fd93060254734d3bc3540953c51c", size = 2794385, upload-time = "2024-11-05T17:31:37.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/a7/bc421fe46650cc4eb4a913a236b88c243204f32c7480684d2f138925899e/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:098b8a632b8656aa5802c46689462c5c48f02510f24029d71c208ec2c822e771", size = 3084580, upload-time = "2024-11-05T17:31:39.456Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/22/97e1e95ee81f75922c9f569c23cb2b1fdc7f5a7a29c4c9fae17e63f751a6/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78c8c143e3ae41e718588281eb3e212c2b31623c9d6d40410ec464d7d6221fb5", size = 3093581, upload-time = "2024-11-05T17:31:41.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/14/f0df0ee3b9e516121e23c0099bccd7b9f086ba9150021a750e99b16ce56f/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b26b0aadb18cd8701077362ba359a06683662d5cafe3e8e8aba10eb05c037f1", size = 3385934, upload-time = "2024-11-05T17:31:43.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/52/7a171bd4929e3ffe61a29b4340fe5b73484709f92a8162a18946e124c34c/tokenizers-0.20.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07d7851a72717321022f3774e84aa9d595a041d643fafa2e87fbc9b18711dac0", size = 2997311, upload-time = "2024-11-05T17:31:46.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/64/f1993bb8ebf775d56875ca0d50a50f2648bfbbb143da92fe2e6ceeb4abd5/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:bd44e48a430ada902c6266a8245f5036c4fe744fcb51f699999fbe82aa438797", size = 8988601, upload-time = "2024-11-05T17:31:47.907Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/3f/49fa63422159bbc2f2a4ac5bfc597d04d4ec0ad3d2ef46649b5e9a340e37/tokenizers-0.20.3-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:a4c186bb006ccbe1f5cc4e0380d1ce7806f5955c244074fd96abc55e27b77f01", size = 9303950, upload-time = "2024-11-05T17:31:50.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/11/79d91aeb2817ad1993ef61c690afe73e6dbedbfb21918b302ef5a2ba9bfb/tokenizers-0.20.3-cp313-none-win32.whl", hash = "sha256:6e19e0f1d854d6ab7ea0c743d06e764d1d9a546932be0a67f33087645f00fe13", size = 2188941, upload-time = "2024-11-05T17:31:53.334Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/ff/ac8410f868fb8b14b5e619efa304aa119cb8a40bd7df29fc81a898e64f99/tokenizers-0.20.3-cp313-none-win_amd64.whl", hash = "sha256:d50ede425c7e60966a9680d41b58b3a0950afa1bb570488e2972fa61662c4273", size = 2380269, upload-time = "2024-11-05T17:31:54.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/cd/ff1586dd572aaf1637d59968df3f6f6532fa255f4638fbc29f6d27e0b690/tokenizers-0.20.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e919f2e3e68bb51dc31de4fcbbeff3bdf9c1cad489044c75e2b982a91059bd3c", size = 2672044, upload-time = "2024-11-05T17:33:07.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/9e/7a2c00abbc8edb021ee0b1f12aab76a7b7824b49f94bcd9f075d0818d4b0/tokenizers-0.20.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b8e9608f2773996cc272156e305bd79066163a66b0390fe21750aff62df1ac07", size = 2558841, upload-time = "2024-11-05T17:33:09.542Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8e/c1/6af62ef61316f33ecf785bbb2bee4292f34ea62b491d4480ad9b09acf6b6/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39270a7050deaf50f7caff4c532c01b3c48f6608d42b3eacdebdc6795478c8df", size = 2897936, upload-time = "2024-11-05T17:33:11.413Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/0b/c076b2ff3ee6dc70c805181fbe325668b89cfee856f8dfa24cc9aa293c84/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e005466632b1c5d2d2120f6de8aa768cc9d36cd1ab7d51d0c27a114c91a1e6ee", size = 3082688, upload-time = "2024-11-05T17:33:13.538Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/60/56510124933136c2e90879e1c81603cfa753ae5a87830e3ef95056b20d8f/tokenizers-0.20.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a07962340b36189b6c8feda552ea1bfeee6cf067ff922a1d7760662c2ee229e5", size = 2998924, upload-time = "2024-11-05T17:33:16.249Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/60/4107b618b7b9155cb34ad2e0fc90946b7e71f041b642122fb6314f660688/tokenizers-0.20.3-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:55046ad3dd5f2b3c67501fcc8c9cbe3e901d8355f08a3b745e9b57894855f85b", size = 8989514, upload-time = "2024-11-05T17:33:18.161Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/bd/48475818e614b73316baf37ac1e4e51b578bbdf58651812d7e55f43b88d8/tokenizers-0.20.3-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:efcf0eb939988b627558aaf2b9dc3e56d759cad2e0cfa04fcab378e4b48fc4fd", size = 9303476, upload-time = "2024-11-05T17:33:21.251Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -7858,7 +7811,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "transformers"
|
||||
version = "4.46.3"
|
||||
version = "4.57.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filelock" },
|
||||
@@ -7872,9 +7825,9 @@ dependencies = [
|
||||
{ name = "tokenizers" },
|
||||
{ name = "tqdm" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/37/5a/58f96c83e566f907ae39f16d4401bbefd8bb85c60bd1e6a95c419752ab90/transformers-4.46.3.tar.gz", hash = "sha256:8ee4b3ae943fe33e82afff8e837f4b052058b07ca9be3cb5b729ed31295f72cc", size = 8627944, upload-time = "2024-11-18T22:13:01.012Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/51/51/b87caa939fedf307496e4dbf412f4b909af3d9ca8b189fc3b65c1faa456f/transformers-4.46.3-py3-none-any.whl", hash = "sha256:a12ef6f52841fd190a3e5602145b542d03507222f2c64ebb7ee92e8788093aef", size = 10034536, upload-time = "2024-11-18T22:12:57.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8117,11 +8070,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "types-regex"
|
||||
version = "2024.11.6.20250403"
|
||||
version = "2026.1.15.20260116"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/75/012b90c8557d3abb3b58a9073a94d211c8f75c9b2e26bf0d8af7ecf7bc78/types_regex-2024.11.6.20250403.tar.gz", hash = "sha256:3fdf2a70bbf830de4b3a28e9649a52d43dabb57cdb18fbfe2252eefb53666665", size = 12394, upload-time = "2025-04-03T02:54:35.379Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/1a/fefad12cbe6214303d30027933a3e521188d9f283e383a183d9fda5c62fb/types_regex-2026.1.15.20260116.tar.gz", hash = "sha256:7151a9bcc5bbf9ecfccf8335c451aca8204f5a0992e0622aafaf482876cee4f7", size = 12877, upload-time = "2026-01-16T03:21:49.461Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/61/49/67200c4708f557be6aa4ecdb1fa212d67a10558c5240251efdc799cca22f/types_regex-2024.11.6.20250403-py3-none-any.whl", hash = "sha256:e22c0f67d73f4b4af6086a340f387b6f7d03bed8a0bb306224b75c51a29b0001", size = 10396, upload-time = "2025-04-03T02:54:34.555Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/d4/0d47227ea84365bea532dca287fe73cba985d6e1d3a31a71849a8aa91370/types_regex-2026.1.15.20260116-py3-none-any.whl", hash = "sha256:b20786eacbde2f2a261cbe7f5096f483da995488d196f81e585ffd2dffc555e0", size = 11099, upload-time = "2026-01-16T03:21:48.647Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8343,46 +8296,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/10/6d/adb955ecf60811a3735d508974bbb5358e7745b635dc001329267529c6f2/unstructured.pytesseract-0.3.15-py3-none-any.whl", hash = "sha256:a3f505c5efb7ff9f10379051a7dd6aa624b3be6b0f023ed6767cc80d0b1613d1", size = 14992, upload-time = "2025-03-05T00:59:15.962Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "1.26.20"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11' and platform_python_implementation == 'PyPy'",
|
||||
"python_full_version == '3.11.*' and platform_python_implementation == 'PyPy'",
|
||||
"python_full_version == '3.12.*' and platform_python_implementation == 'PyPy'",
|
||||
"python_full_version >= '3.13' and platform_python_implementation == 'PyPy'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e4/e8/6ff5e6bc22095cfc59b6ea711b687e2b7ed4bdb373f7eeec370a97d7392f/urllib3-1.26.20.tar.gz", hash = "sha256:40c2dc0c681e47eb8f90e7e27bf6ff7df2e677421fd46756da1161c39ca70d32", size = 307380, upload-time = "2024-08-29T15:43:11.37Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/33/cf/8435d5a7159e2a9c83a95896ed596f68cf798005fe107cc655b5c5c14704/urllib3-1.26.20-py2.py3-none-any.whl", hash = "sha256:0ed14ccfbf1c30a9072c7ca157e4319b70d65f623e91e7b32fadb2853431016e", size = 144225, upload-time = "2024-08-29T15:43:08.921Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
socks = [
|
||||
{ name = "pysocks", marker = "platform_python_implementation == 'PyPy'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.6.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
resolution-markers = [
|
||||
"python_full_version < '3.11' and platform_python_implementation != 'PyPy'",
|
||||
"python_full_version == '3.11.*' and platform_python_implementation != 'PyPy'",
|
||||
"python_full_version == '3.12.*' and platform_python_implementation != 'PyPy'",
|
||||
"python_full_version >= '3.13' and platform_python_implementation != 'PyPy'",
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
socks = [
|
||||
{ name = "pysocks", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "uuid-utils"
|
||||
version = "0.14.0"
|
||||
@@ -8510,8 +8432,7 @@ version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pyyaml" },
|
||||
{ name = "urllib3", version = "1.26.20", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation == 'PyPy'" },
|
||||
{ name = "urllib3", version = "2.6.3", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy'" },
|
||||
{ name = "urllib3" },
|
||||
{ name = "wrapt" },
|
||||
{ name = "yarl" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user