From 52e0a84829a0503ea09f87c3cf35c77cf7f3fc10 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Mon, 3 Mar 2025 20:57:41 +0530 Subject: [PATCH 01/41] Added .copy for manager agent and shallow copy for manager llm --- src/crewai/crew.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 9cecfed3a..c3acf4a80 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1111,13 +1111,19 @@ class Crew(BaseModel): "_short_term_memory", "_long_term_memory", "_entity_memory", + "_telemetry", "agents", "tasks", "knowledge_sources", "knowledge", + "manager_agent", + "manager_llm", + } cloned_agents = [agent.copy() for agent in self.agents] + manager_agent = self.manager_agent.copy() if self.manager_agent else None + manager_llm = shallow_copy(self.manager_llm) if self.manager_llm else None task_mapping = {} @@ -1150,10 +1156,14 @@ class Crew(BaseModel): tasks=cloned_tasks, knowledge_sources=existing_knowledge_sources, knowledge=existing_knowledge, + manager_agent=manager_agent, + manager_llm=manager_llm, ) return copied_crew + + def _set_tasks_callbacks(self) -> None: """Sets callback for every task suing task_callback""" for task in self.tasks: From cf1864ce0fd02204f04c15e283e1b995a8b1acd4 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Mon, 3 Mar 2025 21:12:21 +0530 Subject: [PATCH 02/41] Added docstring --- src/crewai/crew.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index c3acf4a80..b19eea20c 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1099,7 +1099,16 @@ class Crew(BaseModel): return required_inputs def copy(self): - """Create a deep copy of the Crew.""" + """ + Creates a deep copy of the Crew instance. + + Handles copying of: + - Basic attributes + - Manager agent and LLM configurations + + Returns: + Crew: A new instance with copied components + """ exclude = { "id", From 9ea4fb8c82dea9ea10c13daa4ba851a940ee8087 Mon Sep 17 00:00:00 2001 From: exiao Date: Thu, 20 Mar 2025 02:23:13 -0400 Subject: [PATCH 03/41] Add Phoenix docs and tutorials --- docs/how-to/phoenix-observability.mdx | 136 ++++++++++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 docs/how-to/phoenix-observability.mdx diff --git a/docs/how-to/phoenix-observability.mdx b/docs/how-to/phoenix-observability.mdx new file mode 100644 index 000000000..e962f981d --- /dev/null +++ b/docs/how-to/phoenix-observability.mdx @@ -0,0 +1,136 @@ +--- +title: Agent Monitoring with Arize Phoenix +description: Learn how to integrate Arize Phoenix with CrewAI via OpenTelemetry using OpenInference +icon: magnifying-glass-chart +--- + +# Integrate Arize Phoenix with CrewAI + +This guide demonstrates how to integrate **Arize Phoenix** with **CrewAI** using OpenTelemetry via the [OpenInference](https://github.com/openinference/openinference) SDK. By the end of this guide, you will be able to trace your CrewAI agents and easily debug your agents. + +> **What is Arize Phoenix?** [Arize Phoenix](https://phoenix.arize.com) is an LLM observability platform that provides tracing and evaluation for AI applications. + +[![Watch a Video Demo of Our Integration with Phoenix](https://storage.googleapis.com/arize-assets/fixtures/setup_crewai.png)](https://www.youtube.com/watch?v=Yc5q3l6F7Ww) + +## Get Started + +We'll walk through a simple example of using CrewAI and integrating it with Arize Phoenix via OpenTelemetry using OpenInference. + +You can also access this guide on [Google Colab](https://colab.research.google.com/github/Arize-ai/phoenix/blob/main/tutorials/tracing/crewai_tracing_tutorial.ipynb). + +### Step 1: Install Dependencies + +```bash +pip install openinference-instrumentation-crewai crewai crewai-tools arize-phoenix-otel +``` + +### Step 2: Set Up Environment Variables + +Setup Phoenix Cloud API keys and configure OpenTelemetry to send traces to Phoenix. Phoenix Cloud is a hosted version of Arize Phoenix, but it is not required to use this integration. + +You can get your free Serper API key [here](https://serper.dev/). + +```python +import os +from getpass import getpass + +# Get your Phoenix Cloud credentials +PHOENIX_API_KEY = getpass("🔑 Enter your Phoenix Cloud API Key: ") + +# Get API keys for services +OPENAI_API_KEY = getpass("🔑 Enter your OpenAI API key: ") +SERPER_API_KEY = getpass("🔑 Enter your Serper API key: ") + +# Set environment variables +os.environ["PHOENIX_CLIENT_HEADERS"] = f"api_key={PHOENIX_API_KEY}" +os.environ["PHOENIX_COLLECTOR_ENDPOINT"] = "https://app.phoenix.arize.com" # Phoenix Cloud, change this to your own endpoint if you are using a self-hosted instance +os.environ["OPENAI_API_KEY"] = OPENAI_API_KEY +os.environ["SERPER_API_KEY"] = SERPER_API_KEY +``` + +### Step 3: Initialize OpenTelemetry with Phoenix + +Initialize the OpenInference OpenTelemetry instrumentation SDK to start capturing traces and send them to Phoenix. + +```python +from phoenix.otel import register + +tracer_provider = register( + project_name="crewai-tracing-demo", + auto_instrument=True, +) +``` + +### Step 4: Create a CrewAI Application + +We'll create a CrewAI application where two agents collaborate to research and write a blog post about AI advancements. + +```python +from crewai import Agent, Crew, Process, Task +from crewai_tools import SerperDevTool + +search_tool = SerperDevTool() + +# Define your agents with roles and goals +researcher = Agent( + role="Senior Research Analyst", + goal="Uncover cutting-edge developments in AI and data science", + backstory="""You work at a leading tech think tank. + Your expertise lies in identifying emerging trends. + You have a knack for dissecting complex data and presenting actionable insights.""", + verbose=True, + allow_delegation=False, + # You can pass an optional llm attribute specifying what model you wanna use. + # llm=ChatOpenAI(model_name="gpt-3.5", temperature=0.7), + tools=[search_tool], +) +writer = Agent( + role="Tech Content Strategist", + goal="Craft compelling content on tech advancements", + backstory="""You are a renowned Content Strategist, known for your insightful and engaging articles. + You transform complex concepts into compelling narratives.""", + verbose=True, + allow_delegation=True, +) + +# Create tasks for your agents +task1 = Task( + description="""Conduct a comprehensive analysis of the latest advancements in AI in 2024. + Identify key trends, breakthrough technologies, and potential industry impacts.""", + expected_output="Full analysis report in bullet points", + agent=researcher, +) + +task2 = Task( + description="""Using the insights provided, develop an engaging blog + post that highlights the most significant AI advancements. + Your post should be informative yet accessible, catering to a tech-savvy audience. + Make it sound cool, avoid complex words so it doesn't sound like AI.""", + expected_output="Full blog post of at least 4 paragraphs", + agent=writer, +) + +# Instantiate your crew with a sequential process +crew = Crew( + agents=[researcher, writer], tasks=[task1, task2], verbose=1, process=Process.sequential +) + +# Get your crew to work! +result = crew.kickoff() + +print("######################") +print(result) +``` + +### Step 5: View Traces in Phoenix + +After running the agent, you can view the traces generated by your CrewAI application in Phoenix. You should see detailed steps of the agent interactions and LLM calls, which can help you debug and optimize your AI agents. + +Log into your Phoenix Cloud account and navigate to the project you specified in the `project_name` parameter. You'll see a timeline view of your trace with all the agent interactions, tool usages, and LLM calls. + +![Example trace in Phoenix showing agent interactions](https://storage.googleapis.com/arize-assets/fixtures/crewai_traces.png) + +## References + +- [Phoenix Documentation](https://docs.arize.com/phoenix/) +- [CrewAI Documentation](https://docs.crewai.com/) From 1e49d1b5928c865670dafca7f4784d07f3c744ac Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 20 Mar 2025 22:47:46 +0530 Subject: [PATCH 04/41] Fixed doc string of copy function --- src/crewai/crew.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 8fb56452a..57b3f07c3 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1122,11 +1122,7 @@ class Crew(BaseModel): def copy(self): """ Creates a deep copy of the Crew instance. - - Handles copying of: - - Basic attributes - - Manager agent and LLM configurations - + Returns: Crew: A new instance with copied components """ From da5f60e7f3f9c39c9eef94141df9c1d4732a91b3 Mon Sep 17 00:00:00 2001 From: lucasgomide Date: Mon, 24 Mar 2025 13:59:17 -0300 Subject: [PATCH 05/41] fix: properly clone ConditionalTask instances Previously copying a Task always returned an instance of Task even when we are cloning a subclass, such ConditionalTask. This commit ensures that the clone preserve the original class type --- src/crewai/task.py | 12 +- .../test_before_kickoff_callback.yaml | 113 ++++++++++++++++++ tests/task_test.py | 19 +++ 3 files changed, 142 insertions(+), 2 deletions(-) diff --git a/src/crewai/task.py b/src/crewai/task.py index 10358147c..c74e0081e 100644 --- a/src/crewai/task.py +++ b/src/crewai/task.py @@ -572,7 +572,15 @@ class Task(BaseModel): def copy( self, agents: List["BaseAgent"], task_mapping: Dict[str, "Task"] ) -> "Task": - """Create a deep copy of the Task.""" + """Creates a deep copy of the Task while preserving its original class type. + + Args: + agents: List of agents available for the task. + task_mapping: Dictionary mapping task IDs to Task instances. + + Returns: + A copy of the task with the same class type as the original. + """ exclude = { "id", "agent", @@ -595,7 +603,7 @@ class Task(BaseModel): cloned_agent = get_agent_by_role(self.agent.role) if self.agent else None cloned_tools = copy(self.tools) if self.tools else [] - copied_task = Task( + copied_task = self.__class__( **copied_data, context=cloned_context, agent=cloned_agent, diff --git a/tests/cassettes/test_before_kickoff_callback.yaml b/tests/cassettes/test_before_kickoff_callback.yaml index ecc2833c3..75c10cfb5 100644 --- a/tests/cassettes/test_before_kickoff_callback.yaml +++ b/tests/cassettes/test_before_kickoff_callback.yaml @@ -710,4 +710,117 @@ interactions: - req_4ceac9bc8ae57f631959b91d2ab63c4d http_version: HTTP/1.1 status_code: 200 +- request: + body: '{"messages": [{"role": "system", "content": "You are Test Agent. Test agent + backstory\nYour personal goal is: Test agent goal\nTo give my best complete + final answer to the task respond using the exact following format:\n\nThought: + I now can give a great answer\nFinal Answer: Your final answer must be the great + and the most complete as possible, it must be outcome described.\n\nI MUST use + these formats, my job depends on it!"}, {"role": "user", "content": "\nCurrent + Task: Test task description\n\nThis is the expected criteria for your final + answer: Test expected output\nyou MUST return the actual complete content as + the final answer, not a summary.\n\nBegin! This is VERY important to you, use + the tools available and give your best Final Answer, your job depends on it!\n\nThought:"}], + "model": "gpt-4o", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '840' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.61.0 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.61.0 + x-stainless-raw-response: + - 'true' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-BExKOliqPgvHyozZaBu5oN50CHtsa\",\n \"object\": + \"chat.completion\",\n \"created\": 1742904348,\n \"model\": \"gpt-4o-2024-08-06\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal + Answer: Test expected output\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 158,\n \"completion_tokens\": + 15,\n \"total_tokens\": 173,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_90d33c15d4\"\n}\n" + headers: + CF-RAY: + - 925e4749af02f227-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 25 Mar 2025 12:05:48 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=VHa7Z7dJYptxXpaMxgldvK6HqIM.m74xpi.80N_EBDc-1742904348-1.0.1.1-VthD2riCSnAprFYhOZxfIrTjT33tybJHpHWB25Q_Hx4vuACCyF00tix6e6eorDReGcW3jb5cUzbGqYi47TrMsS4LYjxBv5eCo7cU9OuFajs; + path=/; expires=Tue, 25-Mar-25 12:35:48 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=Is8fSaH3lU8yHyT3fI7cRZiDqIYSI6sPpzfzvEV8HMc-1742904348760-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '377' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '50000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '49999' + x-ratelimit-remaining-tokens: + - '149999822' + x-ratelimit-reset-requests: + - 1ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_fd6b93e3b1a30868482c72306e7f63c2 + http_version: HTTP/1.1 + status_code: 200 version: 1 diff --git a/tests/task_test.py b/tests/task_test.py index 67ce99910..297ed084f 100644 --- a/tests/task_test.py +++ b/tests/task_test.py @@ -787,6 +787,25 @@ def test_conditional_task_definition_based_on_dict(): assert task.agent is None +def test_conditional_task_copy_preserves_type(): + task_config = { + "description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work', check examples to based your evaluation.", + "expected_output": "The score of the title.", + } + original_task = Task(**task_config) + copied_task = original_task.copy(agents=[], task_mapping={}) + assert isinstance(copied_task, Task) + + original_conditional_config = { + "description": "Give me an integer score between 1-5 for the following title: 'The impact of AI in the future of work'. Check examples to base your evaluation on.", + "expected_output": "The score of the title.", + "condition": lambda x: True, + } + original_conditional_task = ConditionalTask(**original_conditional_config) + copied_conditional_task = original_conditional_task.copy(agents=[], task_mapping={}) + assert isinstance(copied_conditional_task, ConditionalTask) + + def test_interpolate_inputs(): task = Task( description="Give me a list of 5 interesting ideas about {topic} to explore for an article, what makes them unique and interesting.", From f09238e5129679399e756422e3f22a93955e9398 Mon Sep 17 00:00:00 2001 From: Abby Morgan <86856445+anmorgan24@users.noreply.github.com> Date: Tue, 25 Mar 2025 15:52:29 -0400 Subject: [PATCH 06/41] Update docs.json Add Opik to docs/docs.json --- docs/docs.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/docs.json b/docs/docs.json index 3798bd244..cced352cf 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -109,6 +109,7 @@ "how-to/langtrace-observability", "how-to/mlflow-observability", "how-to/openlit-observability", + "how-to/opik-observability", "how-to/portkey-observability" ] }, @@ -228,4 +229,4 @@ "reddit": "https://www.reddit.com/r/crewAIInc/" } } -} \ No newline at end of file +} From 838b3bc09d93c0a9571254d284ba5394fe35048e Mon Sep 17 00:00:00 2001 From: Abby Morgan <86856445+anmorgan24@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:36:05 -0400 Subject: [PATCH 07/41] Add opik screenshot --- docs/images/opik-crewai-dashboard.png | Bin 0 -> 101282 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 docs/images/opik-crewai-dashboard.png diff --git a/docs/images/opik-crewai-dashboard.png b/docs/images/opik-crewai-dashboard.png new file mode 100644 index 0000000000000000000000000000000000000000..80dc6fd8a4977de609b949693a770bb96b8238cb GIT binary patch literal 101282 zcmeFZcTiK?`#ze)08v^zC?F_AIf5ch5a}TikDa2T^d=y^iF6Vo2v}%}fQS${Dj>au z9)ci6DM5MB*W|L)wGyB%>%7HhBluJyj_eV^x9`=y@Fb?zfV zM?fGD_sttu?}0$<5D@5)3ydB37=$wytHwZc>4<4w9c(pp&W_7$k`onr{F`0SsD&V;-;BNp-b*5Fs; zL0Zj)z0BO)Hux2cZr5Yk8C68LmZOVHTkec*jXW^Kg9~Roaa~Xyb`*+up2Y`&L;v{{ zgp$4j$HCE{E15^YAoRZH2pImO4N5)(!Fw14GK(3dWVdQzb%hE=$H`9>oMq zsmy?#&ke79H_CP0&RF!VT#I^tV$;E0h_uC!-8{$riVxIQuVqeBo4+{B-etsfJ3&%n zqpDpCOh8~(zi}*0yp}B;awCnz%b2}79S%JDf;Ad-60-ReUSLbTY%;=(Ssh3acIlc~ zsdCZGGGT1{E>+s#X~&@5yNWrp0bIbXI!wsO4xg{IS;stuz6#mB%Z$5a9H6wq{Ip}@ zl-hUq(={V4t&pXn7MUN^0;_t}_qQdB z?SGxAntFSC@pG(B(QHaUo~5DH@PV-$tzB^?2+N+V2QU5Wnwh>!{w8G}_oFpp0*O|! zxjTUvGd3^*%T3)8IRuPqbE}wl)9kkaxCnbgIgDUn>g|PCZ=@^S-?gpyJa1zrflF8O zg>u#?B~;4g{)r%pr6=otkV(mko(Z-svUhL%d$;PsUs~Zj+7KLbpCOSS!p>b}+Y*14 z8Yq}b%_r4p85X#YUx`oyrdf|03U=UwhM%te8F!&-r1_!2H-d%B;#&9{T~fcLFlj$; zh~CAAMjgx$>Guo~Ru>Op5{!fLW>mFunU|^Yh*Yoev=Q~7Ld6u7&KVXJT{^{bEl^=) z;oEkzN1WS918p;`A#CZG=4P>95QY4hH9O+`Er@D&fmKLST$ZnWXzCG1Sd=4~P;DXt zSB|>VdJH6Hm}Kl$@f5B5zV4D-_w%10eMbF$RATpc2lA?x9lO%CKN)}ymR3Rth!;G8 zK-Gh3Ht!Y1(zXltTEhUG0kL-+Ae0dX+SA3EpN7;!?-=f!~= z;Y1XEcJ)`fBoXpcoXxGaowPguI7v+i!B2L(C zuzCVTbi^+YShn!?cYPT3#{^UysmHnBYwEB4(N%{WMw*VssTNN$nvXy_7q&2E-8+A_ z0AoxuJ`Ao4+Q0-`@-) zv(x6(#ij@8CpVC~$!&jyDZHW|$I2+dYWE~yV?dM+K;yzeBG61FAeb`dsJxx(-w)h{ zEQ&*Oo6hb618_v+j2Fvf%v?QL9xtS~NkYzB7Qi&I9^-O-_bNnx{+QFbm>=8AeC9WL z$vYyqKM1ENQc|VJyX*Cecn@=;EX28k-$j zp@sC0b(K7;#o3jV$Nk2OWbf&M~km#OAA-n~Y4XT7qRO`<`=GwsPH>)WG& z8`@PnGfA7G1J=neVObzBvRVM=bmJJDAh{OU@b#?Gu>d@#>Pt-J?b;{jO0G4Tws|M-dS`IJ<@7}**H}b=R~-;M3L%=B?htd zQ7mI=i^*6S>PXX4>zVahu9!{n9&#!3S~x!DrJE?{S9N!HJLePC21GF4A2rndKaOnlbH3a}#{bOBDLoDNH8KR)dk`>7 zQNmWpmwazic{uJaarnLXKCQ+?h5Cg(YGM_AtG#-mc!-mIqXRC=ZGqVZNSeqZXm zXDyhc|MSh*dnVnYPdm^rf8{-4EEenjAz~lkLZ#iz(zV6zcHzbI7tW@povE(rsHVfO z&_*%zkyA4#&-DOCQ3t|X=tc)qzi)46WBz!Dwm-ez!FzHLa-E2lrdvW{9Zj!5PK-JCIjG=%@2_HY!8X-65aCF`i}Ur`&2;ah69 z4nLbt%5ym<-Br2Ifwz=M_O>e0KHJy#;QTIQWy$<6!ho1%_?kK^^}fq5V9wr>oC+H1 z^IH`obW{8j%sq+}@yfM>I+T%z8i8F=Dr_GVB-TzTI_jr|R^QpF=TXb)7$k+HDY|?( z<<)&RR1P&{irtw`p5auUiqSfval}-fX}kB`%W)LEa(aw?U)Zhs?t6pU5Rh~q@_~#l zTn@9l8T>7O{AD#(c4I15SKiNE>B42^Z8q?iUOEAY%0aezEt#WElr%Lj`0BUk=e>tU zq6bY@bmpGGQ&T``19?sXTdOTj%f|D{UezOaeF%AWKD?Y$tcUbC1WNkTCPH!Ri-B5h zmwfI}ku4`0rIUhT%9c`bA%Dzcq}jn`qs;KDzcXMxH+1cefQ4NMFc<4w0o%Xq26BDW zEy?@y#~JPu-Jef?-;j+cW{CLK$y6?~4|(FnTx!j=7C&y}%P@RhPkQMVMennV;sb=^ zNQ}|zRr=ScmG!WP$%8(lmA2^%hW@H09cyuwVH#_uBb}9lc_;m{F^nZ~VUM27tNuX^ ziln7`aQ7`g11}-=>y&|2rjrqo11tjaMZh`099+m>I>@EED+oOu_*kD@udTu8J#V=8 zgoD7JlHral?!LtiRBq&Lm#v_orkOo6O{YB{?$RfVEoQtB@Av!Ky2Dm3cFV3VcH9MV zWgqXBm`A=LHHicq<6?~XP>6D;3zC%5#*uEL)i%x;*?d3HOlx9Snk_LH*HNy?+S^cz z=vjHfXU<3We;q{3lDKjb`mRe1)%m>17`sI1$Di83!ikVg^BKZV3;zc0Q(;4XtK$-}w&V!E(?aUFB|5G~C9zp5lu%5@827;r(8YY*#Tj(Y1%7k=9Lkk3#Ja>v!8| zFyA#s9yk4yPUO8yS3*TbwbNbvxX+WrtKU zqlCZB4HxNd0uP6lwkE1qJOV7U{kFuB}cckwnqnc@0Qlv#9o#Fb|EdRK7=^ z02>KdA#x7cn}Z3*Xiuiu!IBy7S7s-)c>J2jn{L56s?fMRmXrRK7jBf;Y|+R)sRa3`Qt{fcg-kWOw72&My5(753}u`-xqzC`qVV5rNutr zCSz5#4)gRZEHlh6SpueaA~~a-9Q~m0;p}u6cCez!i$QCxlUG1UTZ1cS6Iul(F1+w}v5l;aco)Q;`^OVIwr z5Kt&+pRV4D{BXI-vV=+mw!q)W0E!F+PYW`p;|1hxN1z6Xm{OuKf6HR*%{l2v>qBz7 ziTzJE&0&Q*=k9lD|0rPdo1qO`$!AxW=|x3idOqo)3Us*h`Tv1 zB}++e(d23wW4w0`R%Xqg*jM31svsBhvVk4ai!5YGvaXMs=(6I}(H&LkK8$|iDsvvY z<6X(?XVOe>Kp=Pu_uLRnb6bpP@p58bHbvNMsG#poFQp)_m^ETQ^aN5m$O%M>@$5#T zEho!NGl?!b)U$d4s3}^r6sPna73CW`O1C6D4&!uvP5~zW<=wpaeSgf>fSuZ%0^x8D zWBPiFtVDX1+r|)uS*n$M1ae6U_jK$T2fyM7QQ+Hr27c741|&L38b+Xocm?HZqf}K-Je*3QepjmeMf5lZjs)1KJ~HrZa$>TjFy9cGlf zp7>=|j_6WZG=j_C&NgUDpY$j;oq1YrKJ+|yp-Kq)e1;HKHtz@@V-Hfk6qB^#VF>3X zACu*zddF&3^H*LT*VC+YU5mMTgsnQNB<uzhZ zJQ@Re)yt(HxFL0Ns93WHe?S&WW48x`o13v`ozT094D5~2%X)2Fvj2dT zw7$1*^WiRgAQoNF8_-UextaeZ4D6cfv=@jMMTJ$|VdfNW0H6U-OZ^c`F&R22n!jp5 z^zq_Q8Si1&pHABdw~Blh3EdRayKuyOzdBUUY}}5GXA_ngi5PpSNi^?6xr0ERMtvcIy?YU;YXRGc6T(Y;61w@7zs%d$_U9W66he*wnOTX-85jVXf>QI@ zz#q1KYE#0eKS&u8m4pXdMEn-reyZ{(db^gi)S|wv;3f~Nxkb4{&J&oZp zea$?-yfzq!F9ejXLZMe=eTC$$k*X5aPXAB7OrD4zdv_z%Pt@MFwzx zVN=>wI=ff(i%3>u-B{w`-uwmyreD~3831)Qyoc(IXUHp-r3UxxsMRNWpMFF3dNWZ@ zBBPcr38kc{{t@Gh6**sH6IC|wK}LTX)7^+_QBYM`&f{+YvqPiqTH-G5C?(UR$nLu>}a_Olk)Pa}R5>zmOa?PIMS zD4GED`{wr!H3IVK(F5moQ%^E;U-K$uMU7iyD_FmN1cKJCeljkdt+_64%cDF6tr~kv zeIgp36q5E!l27(hRadLyiSRV=`;0p&2tDJ!G1N-Ym|HagOuZR`5RQWNg~)(1V;OTsEu6d4SnrAninvzhreVvNfyePZ!43; zOZ7_}W==}?dXF!cP6VS%*Mpg*>z(Lc?>3YZcCK0n@VZe0UYz?Q%57OO%u#1J2kZFgw#64*HT|BOB4 zR6!0&^$G$iWcdJOYD$un?g`Tynj1OR%G{@t+i*BAv{%e zV%$AT1(9m&V`uQ(Y4mPkxK=pc)QjQ zu_vJ0a;R~4^lN@4esRutbx_AD3fTV7CJKxt&aytXa=Kq*@E&SAnu@TT@BDIy{|>xk z5{>JCU;9wIU39^9dFw~S0YdISIndg)XF>FjB<}83c~cJQ_7jvw9cIlgb+(hXjjQtZ zjm%oiF_RjM&0_L}5BFWRfk3$KFV4U#Q=P_otQ4fpNEfT3?P-dk5q4}kb zhi09Whh~>M2J=qYw9!-EsTkn6-UGBqcE=<0NviHg<-(9(B0|f>kK2kgw)(}+%K|u1 z2fvJIf0_;(7zr*k586ngJe3Qo-wj;1;uR}t^P5fXJMDW;0=9|~!EFAz3=|J_$ZJt2 z2dpN>&>`#t-CyrtSP6Yr3DgRb7AwHYxlE6r_UG1~epJ8rS(|5Zrig30V=-!0{M>P9 z`Niej#r^WEqfq<)4|!oAv%`?#cXX$Zie)&r***nz1CraCoFMQnbL;J#&x3DZ}(YsVbl_Gv&@vmit?6yowqC4z#GlJ4j?9p&M4U zp$4Pxpx_MwsXJlNdu})mkyB=|=-TwQq}cGI+@gG+Y>mroTlj-sp2PnZ_x#(-i#P1~Gh9SD z*c{L(W1t55)whi!ZkN; z3l=lKo)Dh1l%?U^V;kb!^Ws_QLKeFBvSSP(f>@zNepYT!o9r`cBq(wtcV6*h^bGg~ zT>-o2djM|TYt-Hmb}diql2aQ$2}TZ+;|$CP98Nl)*dHvTpIq(Ttyz(S!2+b;EgVHu z{K%&vdB6RTzz?665>$PfnQK@WESH+}xt9r40fUe24nqR(86|#~!N~N8p&E-KY$(@O z>eD;&2vtNrP@-xJ_9tt8hEbg6yZ^3VHLjGg}g+zTjT6 zUe;R0pD;bQ{7G_oKl`#zNn4_t>BJ|_H5NeNJ)g*oB|)8=c+874yf-#gM9do_txsi_MQy!|-7gmY>iTZ?vSfyJ z*Q|U0F-?JFeDBWZ@8cD|$*MDQ`kCQ~T9b9g)#h=Zzu&(Um1*%@DXKe$zhvC^u&=7y zln#eQm)~&uyiKg~l;VnIF+Y(-u@bVB)ojsx$73X+Fqf6bR znZ4CMK_=Kf7q#Hv^dhR{dn!HI#iftiv9EUNbS3fZ zcBf$`zzreah!4=Le1>ECI@ILdhmXAqfZjkF;6_)9$w=>Ce+itm-n0Oqoy)$fV?16z zQ%MuAE~s(|>t|iZCEO*7{JDMdP^i^=Z=gsiId@nkPy`ZZfP*h{KG0pkDz{x5q2~Zt zuj$`IBEB+jf}*|@4W^3(PEe{WNBFfi@cxhKtSGwl*XPWTS8;<1uOM$THOVxi7le**~ z4<5E`e&`+4)J@EhJ2RXhyijAhG7>?qi|%V_@`RQjUTlhge5A^6L3&rKYe2iF{~R}Y zKr49Fu&eznmwB}S+IeuR+mjGAyC#8`HPWGws$+>^v;4do({%B+O|tRBg;85J@S06S z;j@w0KEJKu&}9G0@0S$I#BKFU-y#t2YVJE|*x$m4c7?4TW|h{BW%pCuzmi_p2tc9r zJ5t0gN2iehob+xF%EY4tTW;_h(b8@oYYW?M1xtXC`kFgmuL8NWMG6~VbE#L=0IjN$ zk&MOfQcv!WnlPeIsa-ywe>3O-GbLbozFW+4dv!cC6-Cp=BZ>8ee#roxfxZ__=jyn4 zCsCelvPSrSVEa|Sg2>jgf8OE*>tC}aw6zODQ-$Qd2@A&-?$%qq(W^n!)_30u0kz4z zwVVHs7JyF+UW@#l2ZOR$dugYo@h3HU*?Be2dP=cGTS*Geo9`0_%LWErnv$DZTb#Dz zs;^0BdlKG~tK1S3{ER~~dW;s7_0#)TCE`Bb)>SvqTwU!tOESm{7r~@YOm3x|h`TpW z+;C~x*LlcLbsH*ei$acneA{OT*nfz%|8g2U|5~3m3)u${Vd7miX9VRh}vb*V6?n(YvCY z21^`zJ390PJ7;Nq7P9XduB(~{43r9p2f01=-SQQ6qr-A@?pJ79*rkge#@Uwocz_FS zFrIa4{`81XnGZU}3rGeK(l9+3>c4F1KeJ=KG$`x5+gkCpQxzbRe)wef(|^*Y4JZRTIKI^}rjO~$ebi0@!9A9K|+Ns1!!wecr=0pf7QO*?wn)ukojUCyVSIF7gQ z)cFOCT!$$UV2kl_I;Hvex?w?<)Tbtj)7z1Z6o1K!dp(`zl`|il7J;pzWT4nC!!;Jz z5Ynq4% zj>*QzV#W%Ibe{}9P$o#qLYsx5&ktl3jDHjeV>^d@fAi6-q3usuLegpx^Kr83*4bfO zWwu(vIcsyzdcF4mhgtsCySXaeyZ0@-x6-ndAZ_BDdz;^ox=GKqo!BxzB)x)ka#7+b zN`F&j(nUCwvs5~g>$MOy9%|eoO0Q9Ain)s1aqZoegLN>}y=Kne*S6*ii3X_Lt8hJ8 z%}yBPpG8~D2?_z!3xs?c0+D#qx9afd=1fcP(-|O#u0xsl?}1FbuY?eiFV-5jFS@mm z2kIZ1Zr(QWSTiqO-`kot;4PZT$SW-Eee8r?m2#@`!x18d?4`-3AEcb9!2{CwB`$ef znZEdI5`j3GLF!b+3_Xh7eRxka4Y0dV*2WNzZsLl zXxQk36xMNkfMZvN@DFbbFf^ZBFPeBi2TQ-6G1*wjFMMFP=m22EB{K@b&J9M~rRyKr zO%$um1WHJx&m_WEj@nGEpiJxDpeA%{ zJSy4-5MV6DlKXrZSpPGyOq!G73SAHwL@Iv@CUxZ=mE2=$m)S#kthsQx>#SU>$?P4% zjNNikdXjEwT*9LD1+r7Y%xfX%p0i8&V)WA9s_3HLm$K6zxXLrUI?x=)#i7VC+#!3f z=-=9xxgLb&zEaXsv8>Y8c1UxEg>wNn1>3j8m6nR&zU7)4(%1 z?O{;Tex^yJK;q0=Kl6jIMOjYqZ3b8hD^ zK67;u5g5JKBYzzA!xibdJv*a;Di)-#!!=8DQH;(xymAA4Qs1wU@ds zC89+GB!AnI{Escm{@a#$o)+7{l!_=>S0|v*T!fGVy%GRL{@ny2l-n~lsW$kL(;CfR zlk9Xm@Au`)n*JYkH`wPY&;R{pptjaTxv?EAp5K=rK+yi9V2FSt+~BGICdUWgIUfIy z;^>_9-(wH5A0GWL^%Oq%U+vJpZUVGUjrvbd|3k-Q{jZJ*fkPbhcyRr<{I~Oqc0fbN!mBOihCI^Yhl$A|5c#2 z=<4lwDMDjo{U>65VBqdo)b9i00jdAkI`heIT^ELrDarvXaR1Q% zWt!T5FeU8&wzb?pG^EA_F!TT)!oX`jOnv zL5g3vg#*M7BmVXLfBf}eb>Nb5D>VwObfIn^4Y@o2L<~R=`H#5qpp!*tCQYT0(LQhi zxFK-!ZZ^#J_~)x;xwF#-=7b8*Sx@aM6_}tV_R&Ag_#6$9x=&|{Yj>3UXJZCTQ%WLB z2`Lb!M z>{*knJu=@s#)2DyWbJWQO%-K(It>&ZSDyzSzuORA;PU#xG0<`cC!M=>L>1ymd|DF6L1O3$#>Dj+}1@7MJk+C^10%lDG?NdCd zkxG>Dn*u;oYYfJD;h$RID-3}07A;DP zQs4djeUl~2qR1roe&qlIhVIsZ7x>keW`%-edJ_Aj45Ghjs;}P2TlX*RPzWpidTc4X zD&Ty|#l>nK?$cN5N&TvzIZXV&>Y=z^FdBqLiSk020R6SKUfy@n^C*y05^Y|sM9i9! zW7()p1Z@A7@G57DBXBcEby((OH{6wcobEd9h@RN9Cn+bGJFGROn|_L_nZT{o0?v=B zFKh+>$j9t2;Od{`9>R^<4wbricdywE9$HFT`WAk9=!5RX zwZo7zgp!i}p1!D}pkXik)jm_kkj;s7nJX9xpb)k=Uv|8@kt9nn`{HE~0I}4JGCoVwk64}ABJFKT4TG&mCfGsBvPaw^K1Zi z5g$v>;H^?N3NL)E=fVM&f5odh^&$mDxNcE zH(+~RL2Hv&RBqzQ%2d-N6;JOB#8_byzJxU%v}^ZL1Uuy+?6(xDcnZ25J?_f{o#=Db zYk?V5bPPweEfiFnKR{mUm=xWWpVZngnZ^WQ+O1{2OYhcUvZ6{v_s3&yj#p$Xb+v@i z!kgZEYmD^wd3*=|Wk?!j6z-gA2htfPD_P0|@=x#?O^Q)(iobHYwlR|Obe}?x?+Lwj z$s&lDcbRKvYqDeQJ6@~j5xk?^CIBY47wnenLc1I={$*wH{iM=EQ%X2rjV@3jNT6|d zvg)Ih2et<7GBI-4M^+W1yt@rnKUCx97qFuNLBZNd$n35sgwAfgtNsnBYfx}pWT>cc zN8zzQY!Zt-aPSYsyF<~DbK9P#Wx^ej2F?>^iOe;B&FKDPydg)F2$Pd9>TxMtkUIIo zIx3(ML5Lxdv#Urd#u!lj#t?X2_(oT95D01gpIH`OjcF)B$OAE;LQJhVR0l%D_g>%v z)So&U`$|8Yq&dKExoCn)-zHsIMzqSWr^@o^{w1S#Z(pJKD2o)9Ua%XsQkt%x^a(LL z(iYlb!%IKGE;x=P6&H+>ki`wyA6_5bMfmxvuu)oW;zo>aI385Ro0hB;x$ZBPGQI#=Ar^J zuO2Vl$z(m?WZomrO|bKLq~ZxYAjgn0iZ(AKOT!M=N-HK;Woe0hK$FymMFJ)&%t`G2 zVZ8azf}*?z;>z?DtLI+bCH8CKHp;?5%!*DCa6V~`cuMuD^%7;*FYs{3k3d-MC; z8OoPlgBw}pnvsox%#}4kqa)=mrp;5y{ z8se^JR~|pZzG!OsTwcv`=MBmX&z-f_m*L?lI+sBU>=#2c21q=gedoorGP45RUG(2| zznOOQEq&J@7<=ra?;zXRdWdXB9~)=FvEC8o1(5(|sK?gK&}1xa_su(g$;^n}uMyZw zp_*4qbZD98vY%|%c;#rus~G#B0~{_n1b0|gia=7AEAH}_PFk%iay<;GOL1CbC4afI zSCZnhwsFJwlt@@cC}=Gvz^Wh4WM$b!6>#9`m0jzPsG!u#A-Lra3MevOD_XUWO-t~t#w^5aX~ zLXS?nX1HYe4P#Vc@74XXXhZubwVllkzOFyBKeoQ((-ls6m%RS#DUkg-o`bAAEA@2<~%Mc)M+)6240r_Vap$o|4i;_$C_xG3RziCRr#f zqbdYf;v{HJdZZ!W8P%r64-l#<;G#SaU+%6j^UjN*X!JvP48bB_%PT=B>g%wGkP*6e zdyS8?g~cFqeAeO5nKqvx(i=0_!J0b`TKM;3srtaCKfmiC5}kneu`s$;X-;s-DQ8Lz zThjf&Hgk4u*T)!KCez`wa}0gAP{E$B1?VRJ4<{WOv4gZbCIVZ>Vk*+SD%;IYxD9Xv z-UGI&&2ESl-jkoC4MYaAtfpp&T>IPC-^BZ6pO0Hxk6IXZTV4AhPt?t>r+>1|(xUx( z4FTniElh^a1CRRdt{cb2rH@u#!aY*hG z9EAb&OtV~iC*q`=>~)iLZn||{%6;Q>my+DkjHTN{@4Bp0bveKZW*(nT`7NG!3BI3| z^YbKjbACx_GG2revRux1W|I`Fqed19-2TBsl_5@q*(5I9Pb?0%Na2#+Y<(~Ami%_K ziP0(FlR?YEkH63ZR2r`$zlw%|{UWFI8X|V?*$zANaJ#RZPc_h7?#R*MJidNz@mWj{ z_4MTw+hx1+H6j2zl;iaN__25L4PAK_09#bU>leVBP_ltC^qKH6 z2d%?E76dzG?W?hI$K3RlIuY>yJ98X5g3#}pxZ!?YD$GQ$!K-mRSTqSx3VT?7gEoOs z@D2tV*a^QEH_5u#9>?0$UpT? zLT{MQY$}j{e3UBBX9v6LIR)&a=%S2qIg}YU#D+h!Mr!)6^w3stPl6)aqe25x7ylCb zu|}p1mH8(OrX_IV&(Zp{&b3z??c8IU$m$38?dJmEaHj8%UGU#ao1Yqk?lhoLdxtKV z&pZo>@8SaiKI^Xu(4k|eapuKW=1UNWCDy1q8Df=+-wJv<=z0C5{4rsF0LsB35aLTl zO0O^RS&;K`VX<>%#|LVsWAnV+q=&juFD ztNgLG-4Jtt8XA~;-hL1QN~DiIqMX%2azTzEzl&q5TLZw~t*-RfQ9_`s95@2tm*P*q zJn*<-Q`yIR09p8*Z!`X$p{}yc8~-EC-+4{4!Z*NEDk5wgMto<&*`JOAN($423B=zS zKq6s0eb>+@Wkt&%1mwb(#B`wlZHz`KYXXbj22j~Zq^1Pp-1|S|s6gsOr;vF;gMy5j zeD{9g6I7#I_LeXGg3R%XIJN7zq;-cT!!`thOl!NHG77S?hYM|6CD@9#XY}*_vD6YS zu%6d;z<=U{VLfS;o1Y|k&i=N)4D3Zf?HBF@gCzt)e~?x2g=x`htP2NqD6OU8hOe$f zAyDKLKjU9^CswUw4Kyg}kzJyf+kb7i`05On8p7{V0RkBY$1dN3jipycZ1jJO4#9Oy z>6*k})(I+?%H<*K@&@qR0K$b;)Uzx7m72o_djkP-_K7V4!i!JQs0#dr!;lhU1YJ*_ zs--m=&Ujg7@M5eHJ6u&8g3A$s)tKY8zP6W`AA!*bBH)pu(C=X$=R>uAJ%3z!<(WGc zBs-a2SSF>Lgi;o9y*30K&RE-l6h$nhd=yKq@`q*;Fkh)6?MX2i;WrEbJ4S(tl9zt% zuld(){Cyoo=Kxz})8L{q63nBbI(q^vdtG4;!)Z(_>rwN^RV)7qYbk!{_j(>1?XdU9 zq0jr$F8~sw(;hpwLU7eY@D)C!>%gD)M?-Mi9NPU)&bwGRXdRk944DwMwQ@QS0tMqH zV%bPfSrIm7ihvAr(kdSZB<&^9?)Is^+xUAdS$}J&KNQr>@nMxd$JbsMrolsKh-|5H zu}{sp=}ik1^LPm><^hb3_13A8>w{^`cw-ZV+Y&> zff%jWr5o$lL3)hpnI+q8ie?0*{GwsBwsAIiX=RaWH0;~ z{unc;Tx+B`h1pJV$9vqqCQD@}ncMC$cD_&7Z``eL|9i}_^(b6O80qlDSABXb{#=F< z?hfVzi+Sj3?z=jVTMu;mxs9hcCmT$3<~iPCBYfRc$-77I>A>OPhqJU8nZ3Lx09^41 zB)w+PW+lSB-vLm%nh_*8w+e{*3c`^fc6Ecw4&+5LpRz%=z_c1xt0AQ;GB(`YUeX@@;P`T>}lJ0Ku5!SKa{#xNe9e zcVS{vOp5Y6qt0FR2v|q?tN&@-pPpvYv$o&E-@eR?r}uCH1=@_qeNB-S&6H;4lflga z7msm5N@SBwh8b2F@1@EghJYd|`SH6}iGWP#NeGDh{4rjo=X$`=j&ZoU!%S|3r&3%! z&F_x$i637Yml9sCNN*MgLr;)hXTsB-mu(unA8*REo!;_ar2)rA%hU3yZy(Q5YqBDi zgUXS6$mbifpO?zA(WtMqo+FUPXuP2zlM;kPD8f3P$Zm`VOj2cFHRAhO|&YAytn3mubn>f1DF%kS|OLJy#2g)Iq%}I1xh%asr7Bimb zVwwhCnM~g;K(BS6MZ~vNL)mwK>AOaQR&0TK4uIY?VC4rj?1qE2%f#!JR}1O)S?}KE zK7^04uG@z00NRuOLvokb_gR&4f1cwP|GvTJ`$ANfH(smrR0_`e5S($2(pnJ^^?eSAON^=4e}^jDLa0ClOD+l^Jd6@KQCfY5=J z?Yd;5!BgFXJ;`OC9De8sugwD0@K4QBcMB0yUowP}@H11SO(9j)`;`c|UbgyYy9n~{ z2!!R;B5gq8C%>$9rL5-)AHyrA6_CZohTuG@H7B9prK@dg!mnW;Qdg?Q{rc-n z8(sU)SfqOnJ|{S!uM8GwA_Mi5w}OmcvsMKsthpm+ft<>>`Hz$U-=(L#^}7U_#lwHB zs8!Dn=GX8YF|0PY=1XkhhP-0a9lUPnQwVIFAB%%=zA3mv9HQoeXY6}(RF?4J-tR>% zch-+93PANa_uunM{9KB1+0?2T#(Yi`99M@LY3`1k)2pavNcpT!+{*Vb7W|d`+zhW( zrvlj&%XqJ15y^ieD+EC9fYdr^`?W+a!6ck$BXDy2#@(+GKfSX)eOX7Kp6719Cd@=f z28vF|F3$5OZ98~9*Em|o&m4aedPFtPJ+rQ49D4X*GJV@|mp*(jiItZ$;c=Y^_;f6h zzE+%@a<{(mqU0pep2*{Nl`sBg68-0FgCuP@35dA&lzIxfd`vhsa%jcypR-)Bk7z+7g@|RoxX9YH_{O#2z%bsx__- z`VzMP!|~hltiX!JP-y+^CIMhVnGzpah?b{x>Pg+sBya7mWh38{W{F}bukfZ+dRA9L z!?(LCv!+t8_X>6?`{5+kO)JcY66{RtfY&K#H>XiLo8d16FcH9Eb3^)%-nX%kgbO_b zalpwVx|WzGm8G8>9WYXH7q5udHNE~&7a9uo&~11`oI7x$LwE(mvjq{pHg+H}59Fr|5>%?+K%Vtv2Rya_YS5UwfB*2&Rd?}y zv?={S0!J|7#2z#3~bakpEQ2`P$coU32Qco%%HX^u4tvp-@Meb6sV?-8Zf=uqrzKj1bw2F;k|{L zMs)CUbSXUrY?dt5PjYf-n#$dK-E(*G<7+@z%Wgk07PKpi9X<`}m{n+3RXPj?PW|45 zqu_c5nSb3UmVJ zGSsL62#x;xcNW+~9|y%B`(v}QLkD_*$#>Wt&}=w!%X)S$x^Z_{Q1>P?0qD{tgA}ecz^DM%Z=OyL(6TsdSfR9U(5*~(+jg}6c>x6!{QH-GKY-eaB$9#&NkpN>0D{}1 z*6dWJ5GlZze)smDM+ExQYXXiZA80KAR^Xt2!QVf-W&d~6Yrls^l7e+(|HngK$Uw zH-Jd&VKLK$^AkuKOy`&moE}>7slxajhbMFnMT)4cmP* zF*RRG`POut$^W^yy<+%bzIlBE7<7*Bf771S5yV(_e(%6v2_rFr$$h3QwSZaT;ZpA< zftQ=iEGM_yy^-aP?=btc%H(xsAY&*TV(7OxZ@=?aWnrLEvBapO9u{olB^0 zl`UJsfz`Ptq%=u}QGxqQ4OX49j54SjNJsvs^?R$(wQLEd2_=CkmoYk41KDoXcKM_o z?07MkTU^CzC-uT*b%~%$Vt$c#%Tv7y9lsLxU&Xb(yjcC@!R>|FTQIWP1&vk zN{^3Vr1zB<&LakkY+vs$q)sk2-lhMEr}=NV1z8vdEr<5?XAUjr|0Dz~zMNewy5M%+ zKS;77yn#77u@c`Mw_@)bVK!pX>tVMqeAf{VKn1wp?-xNFtOpomaA%{%O+0LCv4T@g zDnaFZ=$&Mw+Ihx$wp)U&cM1R)o(ePgHQS;8E>R%QX9TnINzpSXJ@Ar(ia3EwmtiX7 z$cCIt2p4F7ure7xZk4!dJT>sJ&u8wgop*VPtFwSw`V*532Jyn<*JXnJ3&dCJG| z+R%5~SEqb>Nk`3jfMFka7GUp#XF#KlVll&R_u3kOBi8oz@2qvym3;Q)3$#8N=F1e~5& zO;py$q$>0D_g48XKNTNPFT(Uz1d@TbRV2ld-AHO_z!~nJ*(?3BTWVPoyZayJQ>u5i zwx?EPt0`s`;Y|8W7*UvM6?1G^^U_{@zzRh{X;I@{M^Q&@A6-uUTC#4S2$rixYKelo z*Oc5VhFBT~0igp>AN?0wd$oCP7&!iZ+2LozFd(nx%Mc$5WbM-{wobTAD@JLT0}$3-lr2UAVX^sAkKc3Y0&6X^^so86ZSCqqj%Ln{M+m#U zgqBKORZ*qq^}Dvqj%w0<9-nD5ur`ODX>=$>O}cAqSEu1xX?%biltl8NJ`FuNcsV=D zaee3iqU^n+n%cJZ;Q#?5MNkxJLOfVdP>?D;fMNj!6{QoA-c_2Ekca}uBh3Ox2NeYb zqV$#oML~KeK!AWqCj^BMNGQJ*Jm=o~-uM0f_#ER59phy0z4lsj&o$?Kp82e41yC9hY#uiTT`&-<#ks)5w-)?3oy25AO=VJL|>`L?W__5m)@e*^?GQg|O9 z%SBrA!i^x&Pvh%uIC6-<;ZP|JVX}|K`M^lF>WU6VQkdk*`bK$D&i?t~o3DDRA3uM0AbR|rA>kHi z>z6t@SyDy<)eq}kAFs-V?q*X9FYBl@_GC06C`}jjJF3R!b_+E$WY&ZWv_0$Rcx@&a zU+#5N;K?;@`S1NB-rpnrYr5teg}B(<=>0L@@1GGJnONHRyvbNn^gjBIrqNlwD4~&` z<6lOpqCO317?fW4N~nFhp{=#`V|le@Gwuqz;zyQtg@V3Dg?&9%#$yz7r!~};H+gJa zCI5${Gk?!M`3{CoL`zBJ<4erg`wFj@|C~O(gz^wCq!XwLe9a-lZrib);{UzWFAzc_ z&92Xzk{i^>p&sb_Gsne05bae_4a1Iuk1qCTL}%LV_6~c?KQEM0J$_~_mJTyLID|jO zR|k3o^avvE52G?t)~nL8AADbqwH}=2XD==XnNz&em&nQ5XcH0fin#y|3uZvj()ERn z&rV?(b|wR#GLukr5;JXib87folt{isR0kfqB5{32RnOsI7U%@;ad;o z`yREaQ!MxVqVLz>PkWtTu@tJ%>&S70>mNzi&2vDjjcfE;eVmTcvhNW+j7go99Q2!_ z_Y<#qqK2wzPeUXYrfE|wV=L9U|hLBCP>$s`AHzIPISd1s_Anlmzz>L zPKXtKn}&62^F@v1fu#`r@$>$+4&U_)TF3M@=+lqbZ2Px1GNNb=PM;Cd)VopJs{eZ# z%x-nM(0E|$WA(=`TJk4cYZI)S=7!yUuBoi_g1q@f=6R3ugB$GSka@}Lx_~) zcJ3r)hrSR?M}FNGpa3+1i*o)z1l=}Pv&?v%@lFNBn29@ZovB*T*%bD*JxBBY*!VkM z`o`Mz_L3*3YO)og8d?u7d;OT*Gxq#=lpY2>y~!{S7U(e!qb6K!y%6Z2li~6Q-ya)x zjfI;N)F}Gi3rzf%M$`7Y2YC7a4Su1yb{Hn`=HAKyI|D0Lzrk2(3W-k zx~bEM2!ET7+M63!rgC)NiAIflEuMVOV^H@7Riq5P!=YV!GzXM>YYHnQ=pHF9;kHsW zNT=b$tNkNGNfpfk^N&OLD|bo6Z7&K5&W}b!24;XHI5NcI#{&bC)K4tk0moR3Jbt73 zW|4dON$c``{j=2DbR-|;gIovsQG3&|+p!(TJG6Vt_#Pf0+{`~l@j1Q`@{4e>kBU?M zOwGmf=93L1r7~ps1VbhFY@d4DH`$owa9vcl;a;4k>=#yO`ZtLW_y~kxe9ue#{V)8F ze+JO}2_*p5zLkmoc|&oyiA25fLH7J;N&ZdRhzUZMt)1|WV9 zyMO>ghM1Uh_X~=1`IOlOu!VcazRT+!pafO$AG=HQuyU6jn z#Pj^gI@BSkVK;x5>d!BMsPsFh^b8=G|}x=75VAu?B)jVgoa$r-CpeoukZHvE^)G1y``94p*_;xty~bF z=pkii=)e9!%l@ECX5fwN*!#G-rlIrs7klvd0rj1rYbT9h`MsSeY-JW?*ws%C)!S-A z9Y4!T#0M35sT7!khRn$oOVTWEuItWO?;jySYD;AT6#0b&!BsmL#{&hZX}Trz0xw&s z-K5!Q$MIT8*5H2+z0*jq?$Pm*y_Zu}bbU9wCuNy$4q49)lq~`|ijyyrYqCDp``>rj z=so+Al-vIhGux9&=_95n=Fmji2ZN_aey&a@A=_+pCtr&;F(P;m9I(y25mmlSv-CGJ z2$L8Ox`;Xiv-oi9DQ+d{E>J5v3B({eu!mEWgRCwCILFgPbn;M~_Rlnm+S=NymcKaJ z6la%9%i}`e-*V|r`I^dGPCugjn?X+(lV8P9*G@wPbK;o73sycIL@njmzQ_+A0bD%U+^#V!2*CMtLPy_bolim zs^7boIUBynoG^MUDw)`GPjn8|6@Suy0gG2UetcqzpwDK@f!wa)&ZvmW2+LWroS&F~ zXnGRLMyPSfp#;_y8@p*oqRV5?O!+YzZRt%m=#dzLQNN4mCf@Fn(&+Y5r%(j#D|=K? zbDXr6d>+UHc_3lME%HJm0gapKfGW3*5BmYM&~~%4h%B~2w9BiDArmzth4Xew|wR3=;$bEBB{`%+6#f{l#tc@mD5ohjjP^ZkOuzy)T~IlQs4L z4h#~;{?|*Ab!$JW);D?E@&TskoUnDdcS&DnVMzym*fmuZjVznTR+~*XdJvB=!3DWB za704|-yKmB-k7Vx*e986FUttt&3+pIW9IV)P&Qnb^w<{n%_CC;yV*URC3jxI3!pf7hm8MLzRh3PL zPw4RR411dJvfqAPYas}q7>(MxD!Ogc>=;LXe@}6tubUhtc0w4shA(b6X&}s9)1Dg_ zq{DX-^>9qAAp1s^zel;V-MctqQH8&_nm-8$U(_G9US1F0>I4hB1txXG-iUrDSAvsG z9;b?bzD_y1KGA{e&%05p<57i;ixRztGa)S9v`2Zq^{7e=I%xHlK7MyC?6Vzj@X4D# zc@D(8kqu}GXs$O+Y{|h46{xGIYFXr@q0=XdJa20qx$h6y%*m!TbHOa)opq*^GAX(K z=HV83!ab*7qeShWj?dEr$NzF1X9Cagwkz^zgenRE=cHj<8(5m^h)?5`R7a=pW-|9$(N-0W+5nCATG#Jt^o zH$3#}jz||7*^$GhAMOShLn7GR^y-96v|SoALyP#e=gh?YNmQ@Z1@()9OLHDRgLrO9 z(4Y-;cO2tHU^*YYk1gLm?|~NBWp*mVY@l(|M2X_iV`BjOs;8gQBpJ1L=n5BV{Jc-6 zLtTjS{Wxeso8snjhNb}r@Dq9nRk7WFLb6_~`pgWUKpM~sHr*X6$Z>zm9meXa0xQD8 zvn*Ny)cWg-6$wuFTlOV26&{KhHce{6c33UGtN6YvA-`|&P63~rS~MR)fKM&8Bjtsx&su{)tpl^_}xbj=QHZ;Ll(npx6RL3<2<>aMC|ji?^SVVxfklCb-* zrQR9cBQ1*|V?RL?_vp4`5^}(D+Qyk^L$@a7Y=^DBe;9iy93#+RQrr_|n+9cjm+o&5 zW_$cmm3MASw0X-~=i#`NFSZK|9i1qSUuB`!J z@?qhaYli-=vUJI(u)muID(Ge&U@ZH2bmCZuuXPXbYX;OaYT0 zq(`3~N*RTQ(gdNPR0z4|5rTZz;XQody}|f{{;0wg9F0)HFi%!+T=ELF3%{m^@Z9iV zTD>$r!^d8P8&?$%ScH+h#@Hg@h|E4|gb-(xUR`Yv@3RgbNT_QTk$k^#YuqJ-q)pfE zmNnqm#R*XybGldK_^fe))?#YzI_Biq8s&5e)JVPGGfIx~uHG9z`$d0mI(sW=gOny4c=tF~PX0 z!q>eYn=8$!ptX~C`$K!JHhZr);t=4unV#fi+uGXV-ge~`mK*9a7#q5xSH~-lOA7@u zXx)I&3#{wC;b^x95NhT5aa<@0I@z09?qzi+-mRdt;8akW_vo~u`_O!gl08u3MS#TV zZ+0>TzDcg~vL+r)`nqOf?lZ~^PJaSSDxe#{{O}IgstPUPEP!T$ANSH6?L z!|oTY*pEA)H&BiIgy~;@xvBVrhBw3Y?FO_nUNJ=($#zf$>J1hgdyDH37<-GV(!(V2 zPU3g~Hlr*TAH_}+fv%-N6zBVd5Zdg`d9MfVGVbVsHW)1`LLG=4*bH-I-wcOj3XMIw z9(l|jlW3Bnby5j7Kpk^?$mm)749WRll`8Z@7K6F>@q4&?fg` zRGKGz{ z)A7#32Q*iKPSmKc9`mPp>z!~Gne|(4Tq83siqZ9hkRk;yY9TU#*uf8i+jIV?7Ol|S zgbX$Sm)7d=&%PF{i~4^2MpVb4Ecl@`R}0)O$FcXv5T3&!aSuVhWl=F_Ccq|e!u=$y z^L-?D_Xk9j6e@+)p>iB{f4PwUtRqEkAd1VX+%w3&cr0BVXnQ^0*``mpfJWBG<3I^8 zrTyrZIg-@$4(Ui3pvqVeTUd%NNQlBP)lnSmd!>^m1yB1% zY-xfC)U*3=QVsZnj8JNn5zjylTRpuqKTJlOo=haJS`|4->aT_E*l&_J^vj`(1HO91 zlWBj7?18R;5797mKm(aMk^LZ4GodS(lheHqi-D&ZTV(E+%Z+Y!Pfpzi0tWpTPly*` zfh1gHu#8%JeG{gh_H$sv`&&x^QBW)UBk}|+{xtRZcD`Lx)UUfjx@lS#nJuDX-yTA} zsGq|1F$yxdIq+M*fRKYr1l!#Hcwy*;InFFeHXaV3mFDu=n~73X?p$_20z1Ep#V14A z%xzFYx(hoK)cl0*9D=Sy558IDG!kk8nK%4nMr_;wU(Se z%+g!Nr0c-$)Q^I3G^NEI+tYtfiksZ>wQ!WCUU1A#e%a<|6;>c>_lf<`Zx$x+uajCr z1?G*##c~Heg2|cDG8>JO<(XVQeCyF`ysC11#Y~c8J9GL&S}@;Qyp9f^sq1%Hh$)s0 ziKUN2zvT22F9{0DdbdfnTwYOrj^qk+El%QLA1+8zr_$z8XOgjWv3N5Ax}78q=abo3 z^SE1lEAz000SDMwwAd^Gy9spXFFm&nv)5f;m-kHCVjfxMxF3gUXfV`74Sc;QQYWAZ z<{%tX1Z8tU-YHA(DLTVEcBo;5BwZgQa02EGfL-@qGMV9>20Q zlVNLj<{8&qLu#Z};nH%3L(a8<_( zqRstR#IB&j%Y>Upc~;?BFZMwn0Voxiv+s0!h1zJS8c>8dgb;yV0KHXHQ3t{3^bkqm z7~rf%SrP#6?xm`F70K}B(hr55*u}<5w=i%>U(u|lM_Bhfa0vMa@4-Q89l!+`)UavT zPEZGj0VFj`#9kz;D!5t+7XPV8m_EI*jQAxcwit|^l+q$w$UFxPrS*2jU!Uv97W5QP zFEVnMEU9I3r(TbTeO648WHjod02d6etp`Q~hBWeR`o_$nB=`R)5@k4SJc@e;ztuC< zR#gv)F~Ta`YD?*;J_4SIJrp?nLU&Fo%p+=xDb8b$!4mH!+*Q&UU3?K)^PDm~L!9oc zB!(o=?;vPVnNAz?CE=WX@2btBe6UVdWFb9-2dOcjecgQj6leUZ`qIGJam55T>&;!P z;rv&*krKK|)6jJN))X+iUyTINntNW$l#0}6OJg}n`hwDWPcYw%%4mBCM`bM$G4LH3i7&swi;m!x z)R6InS#K~^d0@3sdX;2AjqkqY3jbesU#E|BJ2NHj8}H>}70Z(+CBqhy4@2*d=MeAp zk$M1G%qHH*VB@}MAbMA7dUpWXX6tg~Xcb?ORM9bs#A84g2hf&mT^Y~0ChOjKEp+sH9mg(IQCaqb zqROd?$E>?%Ek4BDxa?K#c=5f8>K~7qpKf`t(0>Dn;E{sRFLGA3y{p617c{rOdpv*q z;Cx)r@m*|^K^ud`^Z8#62l|fkhaC5u&D;-t<8@40cX7X9>XsdHIJJ%g))d8`p94n# zOg%;9C76B)!%whtXvo4#HaV zj@)PE-Jq}jW+Vjm!0%E7VwZ|anf>S=W=<+9JNPHK08Dh>bc?X@_7(S83214-dULtg z{1!7C<%$IVQ$-5&vMDy4)W?x!v(&;do4YOgT_X0x?kvkK7e4lWRQOLRBjVwyzdN9- zH^V%KHgKq4AgNl0{X6+K3g$BNeVOH>ippGp0%d&- zQnkPMm3=!-;w+#Gu<$5-a0eKB^#iX&zQ-kDZ8w?u{?oh=RV+Et=AlH=aiDg*I!kP# zXNz5{%*u`ellw1h4!$#1)?)k~&~H!{-0$oT+-z!~SN9_CWI&&aEG2DRmZ#eJ#|i5X zcbJSCG$FsfnpfV^%?H;PeLR}gkIzCZS+AnUx*Y-_>qR*G1b*#%mfR(10!QLtrFkkv z9;{{g?MwjcvKi2h?UaNy-goNx`u2iv+v63y+4{Ye2J5!f z??$?=pT8vLCzqPUd0%;CA9MT0JkyZ%{s@(_JX@sKfTLGtp=^l0rq{ARvwS~=SyNd4 za~>$HC+I%SEbl7M=TPGLXL!Kj=U>tF1(BF(V|u05E?Bkus$@{_J@K%uzw(nC6=TkC zCXNcAEK8irj*_tkk#e%3bVIv}zgVk=u~R#Z-}HV`xaFCSTAef`m98_3rFMhX$xU$Z z!JDFC=BSM&BwiSndy4NknBN_z0*pNmd`v|xxd}n?LIh#!r;XEbjKot1KPs-b&xTBC zU-L8Z_u0jo#F@*e+5lW3sWPucVAtIK4^zvuDms(aXk9JAzV9=}r2N)}|1n?GMc27r zeI#S7UO0sMFtOJ^a0Hj{4j$GXjD?g9gsnY&`zEcKc`v!j>YbC~eFr6C#gi!BYn@qM z%0EAfe;&t=$o0P$IQV+f{bJEXcl8H598nP~RF4ZtSVb9G|DIu!OH-ReSk;>pz40D- z)d7Z&YSJ)OZNB{GN$8!tE5`qRO%+L~oK0_vG2cdWrId0|v%o~INm?&x;;^PFlU{u- z91(-&Mhh&zE>@gI{dwTz^F8uCNYW!;_AmbL(;^=|wZzhg?3_Mty5qQE`PP|bx)(oT zRhBac@)>h8|1TUH z4~|{utvT!SU~O~|Oo{-z;~aq&un%gybqMxSxrzxYdW*pCj}Ga`Nh|3}>d4p$6HnIb zEU3Vh&PWIwHV{@1(oZbW02}rzGd|lUW}V-Z-ozo9DGgs#*(o3+kZKC$-X!Nj5B#Yr zvb?p1`1=1Mm+^2|DZCNX{~HSL4Hdk@w4F8J$UCGGkLSWE&ksF82fe=|Q9<6kQ(PcMlV&+2%yuSD?MZQdH4(4W5cqQh9q3YffZSJg zW+FQJZg-l`B|a{I%Dg-nJp9he1xgi-*@;+C3=8B`fr3)(%xH3J+VCNqz9jl6}Vx1fgm7 zqzOMCrEZ75_OM#bvsUtH#B9DFU5H-Iiy0CYgWBBbx^Y!Ru)@T!cGX3NykG2FfxIeo z?WdFUYy3j>;#S*zwR^3VuBo57T2)yyz%m**Ovc_Vt6=isXGI;w`v|DzI=@A-RE*Aq{(<{PSZ%8wJ8kMfoF$&|;r^RsINu56_a z^nO=Wsi>i(YcAujJjy>;6G*+%S7ckmaGP#VM-6>+IRB>oN}FKk0>8-YJalCAYY1EG0rkb>A zAK7zmSr5<0X4@CIV9A)-C#KOWJ`dq6BhTV_h_QOw&)fMYx^JOz#c=Xk85f(lYDIP8 zDn+E}PdGdd6lpnL?d58Sw)Cl-Rho8m*W4El5na9O!TCrJk#Y!&Qf<~%UarR*Yg9Ok zd@zLEHiYQ6{`~=oeD3zd+`fc|E1Q9Hi&n2x1bJv4RRj^wCpcWHxy<6u2y*^43VadD z*L0r*WgdZjCcb8t1a*qc|hS`#6ZZj6Ov^g5`uaT41W(;+4_1GwCw_tJCx01 zigJf)>>>662#sRRavqq4kmu;flIdbNsYGVi-J2C;wV!2S7)HrgOrIetLS<_|Vr}{` zh*q+*Wp{Q4I7llIt^=UL%gqG_2EKV^P*}gAG(tV!9?*jH>Bu`@6r$p8{nyvF#8x=T&>JBr`HOdb9HjF`%q0 z3qdXPjDHgme$tPXul?T7Apb&~odno*Mnk^&OX*q`Z=25O1bjwq3}*K(OK!|K&b_Kz zpF{X$PAZ4&+h+lZ$NHtkHY)$)bucMFl6XkLbI*ygp`jg@C39Ku&eDYnQpxs(rY(c< z9#oVqak}wZ2Q!G6_l%ouPU!l-qNqAJlua{0K^BsI&1k!5Cjyi?CkRKC(&Ohfx@}j` zhZl7HdQs2pRT1jWoDy=&p1)&b)>^{YU+%}4PC9dxQcivA1&aZ!?Be;jOh2i`*}t5* z_FX$Vv%DzOWsHei4LH~6qC@|9eX)-Fh|I2)PqX;{*iQLw3w{Wpa~CBjK(b%upwBbt zI~*;<@rRJS8guU5QvOn*!3uLqQpeAlKF$trM{T^&y!YV)0?{~QL3nHdywj9#a^5xewJ($u4MQbLzoQ z@0EeOi*e}Sp*G~&?o26`4Y7jZ-wu)$ZX0OBUVPL;T|)`48QvQIE=yFT;0 z64zhBT>Pmc#W(n$=1=-|m6iH?b`0b18X5UX<TPrAZ>RK=lV#>o`P423@J%?b^*tvu1ik1j3=!>00Q ztKWu(KeqHG$1G;QAPTPL5vxxjgYBqtwg&&lQ+@>6D!KGkVKe;g99O5^)n==bMnuWA za7F87?Z!8GK8C-}j}n%eH@*!Yh5aeOUfL#bZN-kLk@k7Vw*Lv4K$rtIH>h5n^u+e} zQ|p7@dIR1Qd(TfkOvGB)#t-UH)Aw^UkFPmbBz(DwfJ=wYc|75I@#U#~W+}4Ky=Lo# zUuPJ}Tn+73YQlr94~HmC^r4HcY4x|a3$5n)=EYSi9B%poz{QP#KEVt@P$y<(MgK(Y zu@=qb#)XAmR|S{)H_mak!A#w=rX`I2e{n4qKyn2!8 zH042Pm#!af4NCqsbo8NJvG1B5X4NA#w#VbRxgcCgC(mRgim^iND>~_RGQ2ojO&5fqcN~R1dt{NsB&krs_XxiOJa%vCU1g0NRtiR5Da1 z3btglVG*L#MA^IrWxI=FG`eyI#mFua0ajE7ly}g7THWR zYH`ElI!^YO zBd>52U?>Pd2flxi1A4_6K`}9n`!l|3jJF}f^5@C3lEEvFFA!ryr@bpDiAA6(%V05V zYxS3>n*UT98#XFmXKO`?-m^z-1*NLHK1f@9uFY(|SMLbSjD%(&YJjTIjA}o5x$tS$#*F&B(&gY$ z#sZ~A)q>a<$j0&;X7zfQ4DelZJPPpa-VL}AGKlPx)P@9l7x1==&ve(h|^BAyiVp?wZm>M3j+-gn%fic z`IJ6Yue;<*I07{%b{Kv4$D1dy`tZ`Ya^(1QsBf%I*syNGSp9hkUF~+wY4xebxLeWBgtt4V@80jht<|DQMJW%xTQ2n>kN9k_n3JPdBc_kRWYS1ft+`Y9AK$Xl zm{kX2kBBj<-_nyZ77jp$@AOP}1@ufxGb);zYuAtcaf6Y;#gY8MC3OC?MFJ_SPyMJC z&4w^9Ls!R223a4L(G6~A7}IJ58*K?;rPP{40Y>|a?;+2qfqK)WRN0+cZsY|)!FEB5 zUt7Pv8Ph6sX|29jMV3B4QTkOwk8DHDv%mcAyvy}RExAGJBDP%d^Crqo4|DL?^U>Jn z>d1v?h;0qJa89>9i%4@8{Umh&y4LY2JgOA&U}v8&)9yBg@Up#eIcnmIPFF6YZWbrd z78|UoMk*+$wPrb;?el~gm#t=aXUKIRn%d@Q`sW_}m1jjp#KL)^hb~_ytr>cHS)&Ej zGTeV!v8?QJCzS+H^_jm##FfpEjoxc5i)VrkXZt z99*LtqSJr02pJ){<@=NQ)URO3mB7wP^I)z-mm+yMpy8NoQRqyqV!MIk8*rwO!8`Ii z))=j{MYHpg6ZuBGPt-O_sD1ZoH@jo=&0s@NmZ5O7G03i{A|H9XA)idV^^iA{cXQP2 zS521hR7l|t|4a|DpOdX><;(Twuah|;>VrN^=1fc~vL7)@W@g(I3vQJXR{UOai;todn+(@{JvK zP%p9H8mj$r_2XOOlw7p-&FR$+AA1a?XLFs@y+~SYlH$ZJA5fI76JX!y0P<6B6TOO< zYfY`2vtf?IYs6_}$bE8o8(K$LbUT!}$zA4|Px|KJJPZgaqs$K!z7bD)6ir-w0Y z_8>(`tyca-FT~nKHIz56UJ%RKPUXImIyVkumU;z@dSIEMnB!Qy(=R5q@8qTHdC&zlRCe68(__S3L}8z=$-{%tH=^%B0rrw(N-U0sAKk` z@?x6^ftxY1Y7QOpi#EYnipG`cF7;xaK+T)iNcW=DB19-jD_hjkBHdbV)Mz!a)24%F zTWrb2IJuor0Ea)}xBi5($&mQ|tX?;I z{dA+=$sQRFi8gs;THCb1%?8whr?;H9shM}M{cRmK4x`2nN?~3vDv{R=-&Ptv@vt6} zSq{w!6Yx7Ix}@=5Us{)syL(Zy$jmf*Y8B5B6`a{d>zco8J!CVH*gr%2niTU_vqqT@ zX)-Ke(xQSee`g8;PfM^@f)s3+q*X~piri(3L&aX~xZI&qRC~?hugCB#z~lu-Pb7ir z)bJ|P9#h`5koauqOH4GqVle1?IG1L{(-w=0Fwa%Quqzs!Y8+A%aPWOyebMFVG73Yy zT~%f(BVQeWM0w~5MfJ8yTATa40i`!C8d*l?w;{cqaQrogvJuRpIOpc$92@c(Jt3Pu zPh+32i^Zx|TqS8l(Xw?41;`r-qJS`y`iOB8t&xQG=||qDR;yCz^^om#OLd9uw1BoP zoJ-K+mMSf~yib|*sffJTq4w>h#D@-r`M@Sd@unr=z-qxlNOrkBI7#Gu{dD1cvEXO< z%RlHInGYCjtPqRkyxp&y(A}2^XVqb|#}7g;y!Ngo+3Kz?;yK+)xY)qy4$>CV^*6Zf z3`=7EP?NTBF7;73U}d1&JGyDRNN8scVu+SbIhfBww5o8-5`H3j;dUz}PIqMbRMl-` zWnM(}TJh7a<)PA?GP*`!`YCZglFzUUjxHWptHUvJy{On&c%OynVS-oMY$kvXJ7oCS z^?N`Y+fq1$3{iWirPW~Um%V32%qg=^*~DGiK=%ookA>(Vk4$0c;j)iUH^x0VM$A9b z&G_{&qVJE2|MWDhL>8Zy&M9kawN(1B`LvHg=1{8fTB*?eYlv?I1WEk&l4pmUrAz$r zcY14dhlZU+AE5?ZLv&%^_Oh=LXXCVpN*_Hs*Zr|uIOc0QWqqCQ-O-VemtK!((lj=L@ylG)~yG}By4=y zYph83_fZ`2MH%ua_cow+L)pqip6bkHwY8Dw(#WUAC5nw~HhW=1+Jkms2{5+cABG z6?=ZTIoY%hlreevM@t9yUG$&aocKQE2<*|-PjuYA>6JTR<4-Rlfnur%^bIQMSw_$y z@{5$XMn) zNU^Q}>~cmaMJO{hL&Kw90J<@Dj_5Rw8F^hq<_jjo61EGv!zEKsW{d8+=)voX6k*>3 zHFFUZjBS$& zrS%z`u)K`WhY zQSS?pb{FXB9;gSQZy;mS#9_r-HO>JGBS{qWw7)HWy{O|;2CnEj)8A)RYh}>21{Lc* zFAt-1D4J^W>XlyJe3|DnuWyli>w*3mg9q>|4RAmPU%byZrV4%Y>W0+|wI^;x_q4FJ z!CI>uxvqfBDohTu)WW$x*rZPaYN0NM{t zleC7`e983v$~jy(>F4XwR;j(Dc4gg zIV0M>s69j;3LpA*OMoAp{L39X(Kjl*30Q|X+xMTLWj!j9#J9e47L1Gt*{wfu%okE*s62=$gUwSy1IsR$&SlIe-f3%uUjksWp~M1D9h~GU5P{ieNo|aqk#%mLI@A81o{1bd z3U=7ld5B7b~79rHUy164!JiQVj%9w$tS^5qwn>w>BmsI0hJK-mfprh8xa)$9;wiOI6^ z_*vP`v46M#P_`S9m!=*$`9^B+s1_Kr((D~4&GKd#P$1Uw{f$HaQ>_6629bo{B`J>H z%A6d>X;ZYKe$r8oNCwfp2ml-j=DZq!wg z9(vbpI8-!IZ^^E4#jDq(vZFn3f}&+F4q`I^L9vQ;|CABkbzt$yrzW%|p6c2+FW?1% zzJIG<-%L`KWIJSu+!!+@P?0A<%kynXBUX757+oH=&tppzKvO5@UX3fL3v(R;;=MOf z19t;P3Y0M5?qZU9h|{+aU_JRHH+Hj{VhXRV!t40%BFmA|Kb_)9-$LFu`jkK&zeG(1 zxi(J&qH`fy2WKu-b%|qry7!|yRsOz&_jT(Cj;%i$cmyn508xz9$j%0e^`^KbO+Ikv zvP**xAxd4SbuY)*mcw*@e=>sTkxWx9w&Mypt%pfJU&7{z)X_t9i?N4cp3*eHD+A+& zaIgbVB6uv2rC;x!pof&Rklu1yv>ue?x)JYic(h;Qu+ZqdIZ50m3a|!|QpnC{d zTU4hpb_E^KmlGvZQWwyMC<7M|mv7s+R<_&T}KC5v3I?3A55 z>nqL|;tH>hiLc)Le4eVJ1d_w=!U}+ba`1KC=gg`s;?`*>8+fmO^&f)Zn}s}J>_IEA zqrm?5y&s-36u0W1u8NKOH0BX(avsQOEs>Z??4D-{6g2}fNH%?~ZV9&Qn~uRJ$hvU^ z$Z0*u&Rl_Ajm6%8&q}ap6#ChbeuAbaj?2JY_Ux0@)yjQ4&$7_pfI;pE>$i|w3Ty0< zCs^@5>vMm!vXVdl?KJ&UMp5ng(=#s(aE-#jqnqzyt0>2+ZRQ_y1YK;Dn9(ke+QtJa z^r5YtWi@r*u}-j5AeogqW9Bb z;4g&w-_a15I*g84w+r=w^pSZg04PSDzHYouIyj?#vskds@5A(&?Hr)ZZZNlFK3O(9 zdk>2z14NfkO#5zp$$Lf%Tp8F!Z^GBl($l9sG4wzm|2Uj_n>Mbpy^l(6D*%s^*#!mm z12KL7AD0idqx0W2Qnr5Fa<5s{(#J$OU%tD&RkA!X=y1LgYv=n>li9FmF}?jH$55UL zSe5LR-?MdUXSTk*Ni3%n;XnTWgflbXS#>%kkq|&>{!sLu(Ad%Ok z($^c`?4`Bw6SHcWizLtN4*1$3*|~aLzV*Ve)UQw6QZWve*KkK&WsfJP?rC5n&PjgYX(%)VorK{%m)ywe2+=D>``LhIxUSj1W^K7-!A>| zv99U^Bc)~3-2-8tY+sK|EV~Hr`v+Q&ioQ?jaL3Ck$6F#jVM`M&hK8s?|B;Ywso`Al zhI6ESpyLB)#3!KYDQ+?!ay$K#Wm(d%8lka=%pIm&NUITeY)?^zt5J zLXV#l6eLECdH{ASACAnr1IB&FgfMB77-b=dkSNQR|5W!S2l)FQ*gdetNaI{=3wrXT z`kl?8x+k)u0oy^8xDmQHGlx0#Qg(P!U}QWotaGRWsRZ4cXy#>l$sUDu%0Xi4%+)2^ zps>q91mn1&^~T7f_@Vrmgta}?ZPbl_BjnOWc=eP$S;XfVsUSKF@p`A+xinVaAyB=? z2zh5)aoPJ(r|uiH5TioxUsY*6gs3yeH;KPd%Z4}8Z)+Z3f4ho9J9oF_N%w)yjSP!3 zpgNxaWVqCyO7jw@Zy<+nOi*L&I^3)%_>Cu0RgaCP8YDA)z=5fDZ)=h%Lq(hTkJ!E1 z&Wo~wonb%+NAQ-Hl<2nOf1%h1@VLVak3*202a()Kcs0?91KA?8WUl{Qg_}7C4jcXn zA1h@$J#eZ1GsfkZiU0G=OuN0;kFbS8|(t!<7h66LpPnM!Pc?jHf=;e8hV zC^Q@c@>5=R<#6QN+PLz;Kha9RCiEfMTbqMy?(eoXymDmt{g%F+R(?domQWj4&dC`5 zk6B^afq@Q8#FImM{-=^NZv1UW+Vx@`0}iB9nB%VwZ=0?PudZ^#z^UBKYM*Gl)(N3E z_w?lDkS|LLO#1=595rmkW-l1;{LLdX=9fU*#s_(W>3+V4*SOdeO6~sxg#Mf4sxSjo z#F%{i8|RVeF5A^QFOCEJi=*?xrCxsu@9t;+?DmAuh5!EsqA@D$g12LKZ1-QlD_|)` zV?&xYZw=3|kz9J^@5XgxCW}G#T@$-dMlTFQKMTTkG3~3Q4!Za=Q9{Uh?p$4KJ`1m2 zbBhTi+avT9um@nWnVLM{uK$9IfiHIeoRQb6ferpi)gj2>W)*+5uorXwy9WKMES@SE zMq4k2vf+RFJlT%a!{91XwiIcbWe|NKBzE&FhJuUWm^;SD&4M2P4L|-4MBC~HXiaRy zp6k`~SCMl{G36pc=%3}67gHUI{_JNz6S4rK`y{2$FJ|hT_JwuYAp~z3)}9oTFmG=Z z-rpZ^k+#`3AaFU<%iNlmXJxdR>!Ov+fIc^CA7lLo_*F1=$6{q=Pe1CZ^_mc$CPm^0 zdxui~>j%L013Z0xecjr9>LDU(!?CI89TPawl}@^6>)?^>1b8b z&6CX%>=))r(qv>lK;TG#SaU-#bK!8-Q`{2(0!ziy0|S|r+{5d-8&%A1QSy^My1c}A z!^rZjeY?H3t!L^aj?`ZrIEo@` zGop3@ASr#?E<;EB`QLBI)m69V3f|EFP0dW$s+7i@&bl1)&-FK!Y!SJ`CEy`hs)tyl z2#i%ufnHrik_`pFRmeO1>MJ#;+{?|&*9FClT8z@g=Z`JB8GahB5MFVkRCDn;nC)#E zGh$rf@2AJWZ_UhXCd`lIGaACEC>3SZ$ASt)S=)qxS^# z5d3a~DTZoPoQitc$2^s?%&23={wCXnMP#NaE6zV@RV-Eywm)}1zQLK4{Y#(Talh7C zeyR^m5IM0@%YEhGlhzG=cE~{^(CGqsxViHgnR>lYfyEyWo;Vp;^DZ-J)YgCG{D#`Ah}G>l%OK9DbMd z>(dRP*doC?cFYpfzwMi~bcB~IO>{5x1(HGvsGRDLO`998DgmdMett2CQexi+aV3-s zaS1|i-2%f|`aK%=W=ZG|hlObpXq8$zKjd*X&~XvTW2#yid+)|ynmRe!# z3aq$j?I2()Lu8IP@w1=WT&FF*!<{f^0#v!xDtWrm_)aA_K(dAX~tYuY%`du3ZIgn0GsjQb2XFl3ObbdNOPtLb6+X{=VgQ zFXM>y#?{fg%!x}JIR_nb95$9d`N#|D)~d#A(JLaHoVpoj`}RO(hChlP+~V5{z#^=_ zc*+R#y{hB0Az^W-3;+w@u(~?%oUuT_d4^>5zCeAvW$j2KP1{Hma^IO$QKLOCPp75x zzE<7#AIJ0h8!(J@uAkC!CVLm*HF%Ecdo#xvOk`xt;$&XQFtyR-PDNZuU5vyg*}L~8 zY2;v_wxCx?(9rv$yFH1;g?nxtu`b|?GxoRUKu{>CA|PQXqIuJ8O{MY%l(&EzqmoL)k8slVR*n9`SY60#4U!ou&hAYa; zk$zr4ipOAOlL-0-u znmi3Yj#IuNWqqf=ij?u}b$n~I^{N&hRMbV=%Js6&B_a*#kTz#pPOI+KO7kC+=PFRB zWAu_0Bs%>W)E>s!hnmue%*^sZmwaltPJO8*N4+*|pm%txyxhBmSvi&H<-Od$nv%Q+ zUca57HcELu8i#i0Dghlp|7q3gz^ad43GUkQ$>89b2|)Qcm@H>6?q$uOCoFd98s9;x zFKel}HMg7-EmLg1toZ*Zd-HgxzqfyU#?}a#Qr1K%6^X1xwieovqU@3_d$ufNMxuIK zTM!bGt+K_8-H1ZjvkWtsM7FVH9cIkT_ssBqf9}uczQ4cU_kPsFKk0SO>vhh#&UHPn z=kvPk?Xw?eV(gb)($K~gsh-NU`B0m2B>sdVYGdhfcCDNY$=dqXcvDn}cnaD;`2lGE zD=;!)oCmi#0^|*xF*C$rDt1ru9z$7OlsqS-qvQ3wsA$TG8Sei!Ex)Q~#`=2lMe98mFa8x7sE zEpgw46mEe0CWwkyh2IQmdo}UEpasW@of^MJZkBixr92(s7uE-o3IxV?1)Gk&n z3)ZtiMGT$zO!!YgE4ojb7Rw|jbEhXk@2udt)B=b-k@K6L1N4%5aZ?5#aGUI-vrF5u}Kk%h}>;|rdv{_@e7 zyrDyhvC_P#WKc~A*oEAitVr&kPyQ9~EzgkHqAI5Lm3c6xkmTE-!Zt;5nM(J!!#(a2 zg(|MyRJ}lTjfMbnkSmmvt)9--BhyRKc#q6^IRiX2)E!n(oBpNVYQk^<(?87%&YvYVTCru$x@s=^RNrg@dS-7+OHbKcY39Phn)sKh#x;VnWjk!i*s z+1*L?#6Wc9uIFPjG41+UT~{O>px|8haIxIK8_KZ$LFV~$WuCubJ5>+2rcQu(o$aZE zpni2eq4m0m-B0#aBl;oRweF7RwHlm!e@iO*5Qr&4G{V_b&2?NjAw4ocw&P_5W`57H zZ$?du?-1*Y-K~%|j{YA6)#Qss-iISp55R=qa&bX+H+9X;olOAc|jyPFbL_8E(-XDkO`{?&L+y9dOuuPj=9bbZ{%nK^X6h&*7x&n-Tgf# zhNoCCT6)6doYr_FeOk+ZHkTm9ZdmEQEDl(P7W+{m4_A>US8Ab7zmaz-;h~{d^xV2# zQSTH0ttS1GE^3%W%*0HgX<~h?;%DdG1Y@rtXyyrdfEsY-9C~4)Yzj@m1^C2xj)RD= z&T|KJ8Hc``9U2KoEAg6}Q{YN|VRJ|@(Qu-S?+9dFc3wTYT`*Pk*4 z9k=%)ELZ#Q0|bGT<9XQb&KyRw{Lbcf;8RxQDNXMJDPa5!r7Z~P;N)e;;KHo9_e3EhZYpbKj9>{*&<5d;k4!o4&=pSATIqUHbZ}oRu7^0(b=Ve~{25 zQ3YQng@g%70++|8bIVM{gI^W04$NnU9;Cx=4L#>KK{l2T@%e9CA!8+aVVu!=+xOYB z=-I6hfpxjPhqA1Gx=CK1yqCUutBSVsH^N!zxijDmnu2Z69mA>?*$ zg=D(u6~;Za0+oBIjz*OedAFf2xd1xIwAx@{8d$e~kIWuEHoS)Vfl;6K!S#Z<=zuPl zBpD#`m%bMA%~ENxFNDxIL^ULyQ1t12+`2dVXBmBYSX*!2_x0)rOQ(-6RhZine(vI< zX{Ll8>`G=xPDij@p!}5Y1AZC#cZf=Uaf#tb@GJL5ty9`wmH&JzOSO!|=jN}v1UdTmz$(8Po)7D)gP{q&r(Y)UA zq~hzR9A=XDA2+s~J*3Ui86+~HZZ%tPv>J$PuSi_x5o!HHF6_nk2lV97DKv_G@NPjT zEFQonXe#-GR=$KA&YD3ly^44*!LxJb4Z4G$LxEDV4JxQGrS#$M0H}Y<3||N8CZ}%V zp4rWXXQGOPL$EkCAw&ONy<;XVy|gh{X+yQy9Pn#;RII(s7wQ> zwmw99p+ps9eW@Ekps(fRWV+!^--D7*mC?Cs;gMBlr-YKm=xnvM#UwGtJx7A;xsa2) zh(tlt!+;@zYW9msbM|1V8qB z_c-3jyRN%w@IQolE#8QwdKx4GI`j}3d=dY?fWCL173?rv@1W+ zcpPOzjEnuyz@(+&1Wg?pJu2d^bH@F&2?)CL}7zv8Bk`wdE%4#{`@XX6B23nqI^%9NVsdF)A#kE z+d=<+)x7Z_GdP@UM?5dvL8W;fZnl}hXp4o1-ie~=Ga6rF0BQ0{pM z(Q>&Vi?0dtfA2So?={fypPT895!X22>vu1rZuJO9KgKPEG&$+%=H~Xy%-sAj0-kmm+@I1t*d0l-pwWj~ft5tP--;21taE$t5G18V z>8aZ#vIKnSO!DO_g7i9aaBRZrxH{~Ktmf_n9JkPziU1Mhfg%b3S+2qZV$KNhG%NtA9TJH$ zTOBwXJJ_*{<5gE7sSNcQ>F!02d~Q#=+Q0PS?(MD9+f{d@X}x4QNrhu=NcP2$P~lo_ z(D=})E~=b9xxC8iL4B(C(oOf#k=Sje2BS8({ifL36Goulx|b2zotC)0ssP~V4`XpE z+(YIWwB3KTPiYe`rAhn)vF%bG`F8QC3;UxV59@nx5H<5p$E@-8#dz#L1P|v}=Yo1=Bl7hB2#Imr zlBm6XoZUj24)FS$SK)ELtw!5Vk@LR-#%ki>oZZ3l%mKhCI&g(nJzwhYAh7UT?r@d{ z<9|}0Sy?P#s2wiInOc8>{E%9c#mI5wZqpT_nGCyI1Hvua+wG;mMWP z*}Zcu*tKUV<#*j@7C;{I?h%(Sl~G|&8mf2&Z4c6 zFPhaCEgh<(KbHMGGmlU@Z3?%MT6!_3WXgCx*XD5ipruE_Z5T*0RLJWTO}zEKeKNy) zz`JDES#`%g@D^F~R~t_j8cmgOvThiFU2IjJJTE33malw__krKy4Yl?4sY561e_aVq zqAIT2uaam_h{V|t+M+e%lz`1pkkN(oBFUo{fM5rH{GM1cRS`o|IOmpMLsaF3E>>h8 zYieHG;ke@w;XcJi2mLn}08skdXhCHn+c0tk$fP+8w)2OMSBFuD{Knq;4(Z^u->)t$ zFkyaW=Ik_m*1T^#0`+Z}*rd2d$PZtjD+PRmi&zF_{$G<0yJK@>dQGHc*t6TQ{pjtq zjLqTW!@I{|NKMarUp)8qigcj_`PSPexw`?&y;U^8T@T~&fDi={ zdpef_DiQN8pV7-9Roi}heehSokluO@sFI}4QLB83*t)K4T=P;F)u~AexR=tV+PsJ3 z?y2nS3Q{N$sDckte<>8g)ZCn*%tQbAnkr%(FDpAb>yXPULPPfagQ}igKj&)PbqNtp zBQ|o9uQAP@M}eATLFK?O?_%elgpmS2#rFIwal{-4Hm4vnk*tGZ+Ic>457~IQpF1e6 z;gUVzKNHuF`oq&F?8tc&jwHTG;h;2GSo8fWSvJv-J{Stm&lZzfx5iBQcWT$81md~b zS(bI{fPv;0|5LW%C8(wIZJ&FjQZ?-v{e7`V<{uiyq(RHt4lju1D|S&@8Z0cHPn=tb z1+>MFg%(QuzbK|~Te8&^xJ^eZWzq4#T{*s&zlE7nS56sr)KnkujA1J7dix zco=%?)_sb_4~pO{r{@q;M{zy0+oiLGk)%PEDP8NGkQL;(l^QJ4yK1Gx9^9azobD!v zI_W+59hNjwfK_aysla~mB2%8(%(@!=DnB2YA1yaWN+DPD!8%=1rIc4^%^K)v;=J}{ z5vi37QeUxj_#X9k>@{>3daWru?dk)M+cDf}9~d*;!|WhwR03trQu$TMX61pcFLX#F z1b29wg#q_A#a#K7Bt7$}r`QJsX>!{M({nD+*P4EhsAVnfzGQ>6SEo3fwk`s%?xsI>+3_mR9f*`41q*ZJ$}{$6 zbnRR9$a+*iz=NL?TeOA$l<;tCaCF}vfh@e3g>qdzpI{eU@93c$5w0~71iq|-f(CQG zyAS`fyUt3(KsF@fIeps|aocK{LR z?Mpu{wXUsDS}rE+oc{WhCz6C9k1{^QF*sd*_E+q%mIC zyOg@b8uw#5Ne5xMAE5`tEKAt5)|PF09JQ`LN#Aug{0+G-&+b{0^#w?}-Ba*Ze>19> zd8PxfBa4;WM*GjGtR@5vCbq9xr7HrmXiusG^E}2bk?fnj{TW8M`RDl zvk*8|o*s<&>7KWDig|^@*^i~MU?o8lRy%N?If(R+@Iu|l#HJ*Y`f+7iTXO>UzJ#wv zX?^SG?l2;b+-+_vd}4v3$cvF5WMKTE+7X}zP4(2Sy8lrF0EOc(@wsxWy`9blT$rC^{f{{!9T^qJyUjC~MuAMr zhI}^66MMxoC0;TwpLg*cWi4aGU1}bEl{S6a(?tSg>is&V@1uF7`T4b$nR#8xC=B=8;ppAcQZ z8JXZfX)}fu&DSw=uuN37&UuueH%!1oFfpj4zI%4#0nhW?CX6+RyA8}dBzf6>UAd3c z4EsoP;lNsBi4N!PY7Pqu^jTZRhO>DovHfxYOm@~P@!}+P0$oO)L&s~ZTvAro-H>D| z_gs4rQerkzY`t*y;^+G1+ulO}>t)9CungP0b&sZP(0DT?fH!~*r0^sw^i0@*9wjH9 zYwb#&7?NXP`HMwZK1}Tl)NPKQNl}qdu((2{C_ZJ1SLN zy*U2B;;6XN``KGbgI-+z34vsX$s9{yE+&KNt?V^1%#(2V=yXPC&gpuN0JUp#4*yX>T&5?+eb)exx{1$$7ffEWcT3(UcA2%YMR;kk1w(UjX z5xqGMJ6~$RVA04k%nNN(Mk>n7rYx3TTm03{Z79Ln0p-A^T;{5w`ND=gryDdpk_?Gg z$J{`CgLhol%ISA$yB~b?&EvU4ayK%VyGVC%^bx$NJeItf6}k$a3DzB5#`R^Owd$?2J>)%K#Zn|9CGM z1G*V(LDT?Bz4tUzziQ}J%Dk6U)8N#;^@bPE^oA7Lqia^Q{7b7+ws9!5hC$?(Wxth^ z?Md4~3c_T^k=LgTpGxYO;ffCJ8W#Y^?OvSVM$LwhT182%cd^Uhk4t}J(ctt!WF*)u zkfWCFmI%9LD9du8e`ZWAzVXNA)(72^n;L( zBMjvkO7t9pU~7o&G2N}Gh{o~Ppzegi+U!m+sM(M4VMhC~|IbY~WQ*>Ydn}pTW$9ezhOuZw5hv#m}Ec>6&D`6K3Xz2rIaEo)8ouI&eZUPdqJx zL{>wR!W0xbk3Ia;Uk*BZ4u0eSOPQO1V?yJ=Md?Xzr?iLD1Tp8&vUI`_^aW$@4^{51 zsWeQuE~a|FyI0Ld3Z2zUE&HN^^!)**0(k-Ukwk~&J9rR}JPP;CxF zOE0$<%gF<5I=~j7n2&5yVgKdOxPDy>eTX$(K#^tMHi!7w9`$hwdv|NP6Y1=nAhKH9 z!!}vy!24=J{ta>=KG!uaWAO|KwgHp}!U0c9C7a*Le8oR%uctm{oi7%n5mKj+?d*~w z<~v&6k60mz4dH#oGI2oc_?UqI@#Yi^W(2bt$3`LB1PxEZ+Xese6Ah=%|CbG6^GZr7 zWCb7bg(macd&!NQvpTvBYNk;qG3D_aMWK1r7w}UZc>2|u5CdyE!0Fi{RIy%V+{xb` zs8d#wBl_Vw)EEivHz1&OVPCTi!*D0jW%%zp`Jov5P|TCcv=q!ofUmomlj|9`C7)1R zH6>TL=nw77-zI6PMu~Bf)=2=(XxTOvwVzuNc%f|^hDnlB8xpj)M6Lhu{lHY13#9ng z7gBnp=p{y4r;CyDiFB6jpLYcn!u~QEC>oG6@9oTKl#_d96mYr<2oFE}G8<-6($oG< zA%L6YU+?!14IP~(eI-deX|UI6*U{WpD&(Q}Duii^g?FBe!f zESRSY?A?D}lsybWyyZda-zV~2G2uhk!5<$v!}ka~ut0YvV-fuJzdYo0z#_ji-0WI* z8)|HA4myRG9JrUWim}WHU3E^;1MTF&4I67LCZ4%VHBf&2w`)%!1{5~Jqo-ez*0+OZ=+gE ze)Nj1{mYqZhffpC$xJ1=q|8}&G*gNczPx!x>GX1-KTgX;8)@U`wDoyT^%PRdI_l2| zwl-%7aY){fHS6wt{l1r!LUF!HOVG-bH6X!r>v)oDs^4aEAz2G`|eN+iCG5w zzc}fVC3tzhyP4z$qgmVabkCINbNnroS9x+E^5gV(!)kvT;&mY;WGTlbP0<7H`ca#{ z4!tNCD)@@0O4quz?BM`X;uhcAPkxeZv$05qEo>J+yXSo+Mvle)St_yNVn+f~Mev_F zmn&VG@2&q{9pk@hOvTJ1fF%*kL)SE*OVJ?h-Pfmz8~ibyjKQ445B9Kx6n`6uj1eJ@ zX1J<=xgL5}W{96wfh7~QKmRVYu?F{sMv93%6sT7YiF zY0kJT+;)QvT1Z`-4+9WEgazLXX_Mr)(%#@B=)gTGe>1~_;WnV4)@=vAFK(BWQh>O# zqTL#R0QS-E0XeBqmRX1a5Dr-Tk-7X#a0y@n@=*`YF)dKh5x$q`;5h5m!+9Im_5&oi zUFVk7;mF%^-KUQQz3?c{&;lj0Fn(t;bafI)Kj;>bl8L~3IRCKW2k6ho%13N%cN-t3 ze-ENm%p0uSzM04D;h>kZ16OrYx9XkY)(2QW7Pp5oj5sMt#aQ|NaC(2+{$+4b8bD_2df@Lj4)Q1-O{Jbgg7 z^QrAvuC$byG{8e;yX=04-uoRu2;{h!mCC$mnO_%7Z__aUn{j2H0cOCsDbQigOE*i+GbH({oHM^E2PT!S3IHbNQ<>Hno{(GX=;DA*E%q)3UOMEev#_I=St`TG!x! zpbzJGsQ|lG8vG(taTK$vX_ETz!$7ut-b4VJ4p=v{AEHUUv z+A`69II*H5dmJTuc{i(+c$x{p=s31+z*;0>_e1$?h$9yTpJD2OCJb-Xey>~R%< zCB(mn%pc|l6v$USLaeb5fNt%7#xCY^@DW;8wD-K8Na=ZiG%FF&|f z#je?ONs#T;j@7+QM(oT}Bm>JeuU7r>J)#e=Oog{K#H*y18_%`~9 zi`3dh5?}E|&F?HXS`04WuNol{nj&dM&HW29Ll+hE?5KqQN6fks-|*|QTx*mKVb8UG zKlDQE0<~8qqC{weHc}kVdziKRnXru_iv;F=`Le#Wx3yc3A7cK5cS^Uze zkY6{xQ8WT)9#QWSN0zDW-l}!fs(I}400A`mX=ZJmv{ zMMf^QHxxJZ*MeSyvp&J6zO#jAu0`^(Z47X#j@DVL)oDNW0J(i^HVR{&4(Ed26XFu8 zcnEQT0<*MrMs9*uwL&L9VG-1JCzCoG;bsr)Qf-i1d-bHpb+OsR`dL|%g3AC>Iyf-6 z7u|iri`q^3H0o_RVe_p(V+vn7|DNRTr8aoq-$#OZIOFG(Tws5yOr@*jtrigE)++C} zWb>FgFaMf9k2EI6KMXcDNbozQg(vjw09mJE17_Cg<}2IE<9T~0_qFsgyO-%Wg9OQ% zmo5r_m=jw28KB00*u$VBFf7>fH|fpdlTjP?*b{66RqfOoOZ+h<1kpEEKU?! zDNPbs;$@m1mMzPzluUo6wasqfj!3t)`e+cMa``VKTGYLoK2^fJL{QTZ|Ek97Yj62) z&Ogkxv53iHD7C)!c=_b_5L`w`FV@ly%>kkNu6HQ7+L7NW0Q?*fg7$KpHY2}H!K@BJ z3G(GGr9mK=d-xCq1Sf9uWXAgtQLmM@IU2U`J4tKLfmVcz*S9x>Sa~rK%a+dtKN3$S zFV;s*)?Oa4Mgz*H4!kZ3^*LqdPu!@%`!s7%J7v|G3y~lxnfJ(`SXbvkhp^Lsv1mG> z&-crRQKp_EBZo;IPCaSsvl8dHFq~YNA^i8cvaXGLhmB7IC~RZNni{nT3jNjgH0pJb zs?~Em-jQnBjew%F;5^y2WN&@YLYjAl0~_=DdwBrX+00na=X3wsBwkEKKEUxAT`+Wl zOG**q77ECUBTL-umz)bo)!U_dh(K9$17u?zvm`knAd>L>RvjyrmDV8&-OhWaTidiQ z&LtXkW-N9vR(8(~W+50wg{2_}(KTO2cOR@-4Fa2HAUsDn}c9Of$obck3^iB@mG)Wsk2(la+3}?q+ zwH$Tl8A(s#lxlf$nxy8qGo}m8jETK?@jp6k>oQBy0j=4ic z>jR1CH7uE~y)3eflwNU|}2;<7{d2$#8-3>}|gyPpa&f^OFY){%M$@rwI8_4|`nbx!9-u%ITZ!=l=r<9N^ih! z_5KxeSjH^g^v$B5NhbHlAO=XjZ#xUQYe+N&##WRfm7CQMuRU zEpV3}+!=8tXMH$+zq_vnSLQajg!D3v#;OHs<~XCNi}E33(#Mm;f`Wl^GRj+I&>_8L zETKzYz#_XmYQFP|sqoLlZ$@GlXMJDC7z9}=N&_-0&r2e!$Fh%<9L+XWs>O5Am zw>m4@X80XBoYEw&=cZx?qG0GUReZMh0HS|}JA6mhv1sL27L?(;QA?unTIQ0l+(Qiq zoJ$#=f|*Mo$lpu}L>K~Xh>uhGVK;t2SzZ|xn|63f1!{ilA0e{Eqp|6+N`noOlE?H9 zj#}evarQ3!Yd|o$F16&%39U51zB9OTR7)M-80}@h|HNR>LF>Iek)egQN4kb zq~qO#63%H^2V>igEBw;KgAho?+{Omy$Rl}p=<);cX=~e1B%dPv#T%pYIIr5Q&>j0u zne_CWAy5@PEdUBnI*4TEdNb$U&%$Jp%pP$oIer`JmthcsU-8(9}cD7>e zQt}~PxM7OYE{E2@hO23(a_%A`xN1Qqys@;Q^0yv@w{FpMtw!3uL}5e#C@t{&q1CNg z1;^VNK-lDfYB{JTrD+QPxs5BC&n7G42f)AA_`^aEj%p5Od+O*uYX8th0XN!EqV`hV zC4qHvwhe$DAK5>;=N1id;82i)yxPfxCV-=~UxdSTcyMf7J~$_BT<+FPcH67jz5?^u zyhso=_a<;{zv?44m1ZJ(M7hpBfodPJ(Y1y@EI?>=!!F0laqP9y|FQmkCr3Xz$#oS~ z&~}Oa+t0FHTFx|QEVlj3b+}CqMb{JEy4qAZyA#7@QXVl|#exoNsKdk)et_JUTlw`N zZRcd|Q*pIj+6Bsn4?gD0+vrvHHX1y~I(gmNb=acx0GFEPP?$EGe>}i0>_vH;12e0eHzCkcYiHfl-ij552?Ca;*t7;{%*!4B+?d z(+(YAW@R%%ejp`y*ClnNyG8}1cl2v{n^@l6u+W$`AxAT4Qf0wkWm+vCMd-qd0JsSX zK{Co$XLDln@4{^|8TL0?gw-0<-5(*G90-#kNxuW-luow~#>h6jP^MVpm=%zJQMQLc z7`Hx5&C5TW={VCF63$WCUaro_I2Ki8_?ZIgU_mdc0&T1$jcD zm;o=7kPGwPZSF9f)SobZ@v7jj_~sd$b$F-zH3So)l{^8vwQ7{2FgU4l1JyU))1jv~V3l#f8>U5G1Qe zT_J1Nu3Gvt!Wd`ahT#)LrdH3mo2i$_6&+_ClH42ok^*8dfecT-G;uU3vJqZoub&ZDOC4n%%m0W z=f2=EgFNYyxrQ`bFQA#CYFcX_(TV4CUZy=CaU#}uk+sgj;|{cO<$Ab2jiMb>VpkTh zS?Y`FR^nFoH#rT;GtGWJiFt|0il-V8!(S3X_1?MMy|_17S9WHF-UIFKm?`r@6 zmBigQfc_0lG@|L*1~ZWeP`vZnjTx*a7uZ*G$e;XkDH9gU2BUNrqMSPHPqZ)4m9*Kv zF(qBNPaKnf$EREO(|JaYe2#c3Z$5#QZl4Y!xVqkx%YDs+ zI?v8;{0}0;l7ER1V=@zVCQ)VV-|eLJP`05)mNi7Y$g+AJtsX))WaDpH-1B(^U{Rb9_MaM^0{@@lTN($hAD zzT#SE*F!3$1_(eqoKdfrtiu)*or}hNh_-x8EORr)(!JClV+LqbHF#=c*~N)#DFcR) ze^G0{x=C#Y(~{c%ilb?dV=4`_TC_wpolSK1%ivs5X6LiQZp=MaXFwDNnP?FNb;+n$ zGChPA(R^!evR^e9+>EyZ#L@BVZilQ&+ju?2N5!aquSFL6tdPC!F(;IUc!QM&K#A$^ zY~LQ4idpLaI09UB*nHP_i9Gq#v$%KHRciAJRsClDJ{AmI8kj@_yb+6^5?-0LPQ$P1 zKEk`CFg`{^E?X=I*NZ^oWzI7}ZO{)wR5jeN?SCk_dun*s!-%B890P1eDB>6J?z$60O zmUj5GMtZPed7B>LF;|VYx>a^j%o4dNX&jRu2WU{BJF=x0?w;-0!TRqP?IQqAw7RCc~X{E}Y zEyj)&g|7qN1o?j>wtyK{tCd)D<&VX7wwtw|mzr)!;s{AODM0iP5ZZkZeN+_&(;qcY zN-y>0@QGD7nRWAf2%3V0KGmKca@gApD;#61=v9@{AZZZs09w*mnhVTgN;o( zP%;=s5CI=#lf_&OuFOoFz?;`M`hMt`5Q#UgSo#x%<&|O0zVz#y&=CYEhNXOe=#o%g zO%1c2ZW6y>!XuYmyxWfce9B|HOj~li!U85026=gTft*kH`yUM^K^=Vw^cBQl{M6Pf z5)C?dU#-4p)+;?=3GTJBTHCV&m)Py)BmfDtSd?cijJznvtvjUH4wMFc$F+W1T(s;4d}cUmW?vC20CG?md2IQpf& zFNLa86(o&2X@#29QXtD@qH1W@|M*_DhTJ8~3)}+61kKeoC3eA?{2TC}@_{Zq*C<>b zz|SFiICT;x5wn+N4!<}AD+-UE#W#qsfnD^Foe|=DrBAZX`k|fXOP8AuQ{LmaGi%kfLWZy zaF&aU3s8c2?bpPCpn=7)Ryek$J9}Z=W)3RL2P9A!XN_d+-udz?i%GhlaA2va0eHyV z4&htdp%#^4HvH)X+Rs`Bk*RpDA;1mb#Vm+H!H}tQ9`OfqO31(KxtIe(HW#5`3!z3a zK?&yn05VqQd_*I*&`%a*8Oo;c$myQDcVWkEcwT^u7(WM!H9YDD3(05}`QPOPz&PI< zyE2E|TieLxfXD9>yD{<&)H@WOXdDhq)4&h4v9tYT&0BIO>t*h9K~MkRXxV<1vxG8$ z>Vxt?7W(>+K}r6|9y}hTSwq-g zSN^vNQm{ZkBC^>tdsDju&Q$Ilka6o{{{w1b*EPhF3DOOJWjgxdDW%sLo1=oqJ`|ECER&TGr~K zcmaM$`5?mXun{?79ehYe>~3x~yv}oT8~nQ(PxuJr=9TU~Rki6xuVU&nPOTD6Qq?O& zo}%n1LuY9+leO@-@VZu{2qRf8-Ba?l9K^5%7$8PsjT*QSWx8qe!XbaEh#9p>@$Rk2F}8zkgr2Y46I46^?5i8 zMQ3Flg^$2|EABSRTa+sM*F-EP1yh!PjIK%QcyC)vpsVifgc7ItQX&F*`cJdNR!Y{y zK?VOuhFH*%h?Q;JJVx;8`?I3L%@J&7Yps72bhv8!W(P`gmQQNIiYlO2C?D;gUqSNm z-@QH$VW^#<$%Hg9gTS6b(CLK9A7rI`cy~Vv)M00@YIt|FyMAG`qjj7ee)!5sSrWJ5 zA&$6U_|M0TofKD1jvfJ>29Bp;{F>t^-~W&K?04P=xw`$>_i+xVb&VQZ0VqV&_?JHb zBjI3taN{!5HY|;|jxKngt+$eh{P(v4liZqW(|=4Aq>+dK`#%F)DF(a==0{+C4R%g2 zz}7H`W6WVtJNemEUHgR-@-4&?PGi+I^mpRN!)b(j5_1}G4j*i2Xy`+*B8gw;g;`O< zO>o_EK&3=EnmesOx%%EhkN>_jUJJTNb<7bR8}#bIq$mB@*_i_w%13U^;e(zEY>nJf zz)d_gM?D**z5ekJq3KOs5!>V_0M%}i>Zg4Rdxkw$@TT>D^~!tAgG5pbAGis@3)CF@ zdoe`OlAvq$M7RNhBEb)1qthj7_Z)u}aw1}nj>IXw6ePOcuig+Zq3^u{EWRsV$PeyuJ^Z1@1Ar{IUsfKSbF6%+b2oa;0mI;IDY=eQgyRrTvqd*voN$vgeBLi&Q3ja(izPhRO5J=YWE357r86$NVY!VcH4)Cz24dM~muj ztx-3)o>M5k$K<0$?=#7|6tCf9ON}X*i%G6_+`X5p%&Ba;I@(;&K+{R;{UN?G2PBEu zpNrGDHZQ`%>ssOFl3m7j!R>H^hHn9*dIO(9Hra^zWali#v*cqHWIZjOvqCYS$hC{f z31pk>_l+@Hwdzwbhq&OJeUtIhwN?*N!-@(DhRm(?>F>k#K2x_Y&+!UR(_ZaUr`P^? zM8s7reX#DB0v($9l+g*Ab6>i5C=4@qA@k4s17|Jfh%?tR4QuFaMV@0$4F$Uc$8|6+2D)7}+D8bh83vWZZ#$zAWgbCY1!a&xwDG>O}RG#jk`=89sHU-ABI zd-a?I(&>;_`~-Q`&l`$}K1B^o%$^kIyv5Dz@QstWv|8B(jk>7C{wuLz5X3}}y7nH9 zFY`wwM8A+$#4&=)X9(H6eziUC{sngrV7waMUh4%`~Q5gsy*Lb--KJTVgc5J)8 zGFDcc@yutI^3l8RsH`r8Ydbi7UpCKR<0m~iShF-AVzq)jkY{6V;jq$@*Fn&+%lAyg z3tnxPc$c%czQ_fl+f#xZRtM;+r=KPHI8-}}5vOkw2o=i%-$yzG#4HvRHOgAmltz56 zXm2}nd!1<>()*)gu(AOyqmq&;95Eul>EgR3gd_l8wN0>dk`*b{Akxg>G3!Eb@l9wpkJhL$@D6wEBkK;80=w%fbZ+yGD3T% ziS&@W!#NSc|536sGOOLL!tZ836*B<0{19_%MN;9aouy0bsm@Dz;tJ7-}Zd}wSqGy zZ{FjyvSD>$(rjO3vsuT)ND%z%{Er7eepO3Kq6>#ziuYTW`POh|Eu4ns?MMD~29mO@ zw(os&puw8L!MXEaJ9WS@1I{Y*nE5WvzhXl*G+u?J(d+l0-5Wf4BAPAqwm*^hOj5V0 zrx>8)`+E@6Rg$1z*au=`<5Y*3XtS8JAZ+bKg)ue7_U3MmTbJ#$-^HQbSENH+i}F@H zQzdnNNAg{k=8=X8&r1Fru>a5y!6)W2Ulzj9!uua2^_wvW8~YqqbB2|?;5MOYSAV(9 zM?To~=Z0Y-j$-}0c;A(Ulav4uO_&mq1f0P`3x9W1&sdW>W*F%ui_qTtoZoqLKhCvW z*}8*HZiv|PX;?t;_?q{?UUsiImwCLSpr)ACQg~qmVQvkyToYC?)H@SK9new`R&^T^ z>hs8ZyxLV8wn0A1ye$NsNk9QGr9D;ZJm6^Vw;r$N5Q;kUP+fiXOLqx^ASZ6zq{nnh z$j(=`K~=44MXoDGUeCYNhKjxFD@G(lX^G3LFc;P`>_U>`RdYJH?5YesP={Jf9^Z7aD(F$t(S({5gsjJEa;v)U zMN^L3R2-i84GZr~@>_Oz9?i;89& zq;a#YE)9CO)W0mr1}Fc^UhRSsm=&qY-}j7D@{zTB3x0IggI?fBq!$I z5);&BIbD}mJ(lXfqoX4P*g#<)9g(B6{hIw4cwOfSaCjCpq`x`EjifzKTI&nM9)n$= zxi$UKOUBG?KZ=eURi<+?K-Z}_puGB9X_7^7x4DB1LqS9ia$bpz*b z5vpjregoU@=vY3$Y-CbgIvIGwJJTJ3t^sTKv6lM7SS^)}j}^Sy*?|e1Rglz2tEKCvy;sW;`S(8x9Pw{$rT5(ZtT?Rl*~7>wy_o)i1G@0% ztmr%=+k8$`ah>)W`n;W4gyD?SF{e4&Qoa2AtJEqhQYjw@@Bu@sQwmUTqd1|+{taCR z`Uj!9QF(V!Oi23+m-?KTSAsF>^OS^#sKO9hRnoxmw(9j5PkfU0%=^lz)kUy4g&Q#g zV>J$a^1yIkQFk*Fgs(jfupe|u>Z4+777A&>4}Koz!4&CIos-1Po0gVfP7*0T{b}Yc zz|jIJHkP=QX$I^-2HsOAZSDKZKPeM^6QZjhgHviG$s9R=uWl#jmJHFxz^Z1{b*pMno2+(ts94Qn@u$m3vkOi~A|vR1P$Y~EpV7bJadTlWbXH62KZcYK zLopUQ93!gsA&Qk@o$o$NFc$L$4A0Dh&eZN3-;9+qt%8@H2q=|UZOoAk92xr%Gh_9m zvge+}E|iEUJ??W!HH6rZESHW9!uD zY~8vv#L{ceCXa-8E+}~;l zhVn>5Ts=AZ$r_B+U09{P(Xx*7KL#uE4bTg&jyAnHYDN8^hQgTghk-LXPCG9EM+2hpb{?_IhJ zY`>ew(oQ&svXD22!hb&L!CZK+?Wo?gDAOA^d?ER>oX6w%%+5b+JSp|odqYA_u5MEs zcUQveq9XSEH35%X_c+V#TrRO47Rr2Ipz3Rihrh!M?pO_S<)1~3pej|+4MaZE zI)7u<1A+0>Sz=#1#ig7GISU*c0A+1$k@Uw8A;rt#y#v^uQ%rv^eOkB9ZRria1ntLB zk;m664qT~na6dWHq|J{+^eq2*T6l$fn*pUmVHQu_eip@N3Fo)~l)iH=8W|a=jj{Ue zT&}4Y4J0LNi*L;w1}xTOmgLV~?T`E&$IZp90__nFFRznXT)qfWwA;4iHG}+MB@0TV zenW}8$I;YdC;7!oQr`8^4;<3A*ouZ&eY7{BhjhGxu8Va$8#GlYT`0g|>icy;t*T#; zH&&m5pKwN6Z%9R2!3;D@j`6U45#UMVkCnfbywLyRGK{S;*KcN@tLxfCh8>V9ZEb;E zaaMc}5f<1O;U`@VSn1W?TPIsst^TD&9{YUZ$7T7)zN8)=8_ULI6_|17Ve7{FjUGmn`U z3wjHs^Ms+wROjpJgd3hCu?U+YBgu3`sk{hC|1W6##r%9d?x3G}rxeq8Z1(Nmn)oKj z@Gn{7=C5aH2{LyN^DjW(z-C$#-)uq7!Z%nJh{cEBns6)&4%(bBbFdJ7mP5ITd2Ajq z=7z*(GJj6nftdhv&DkpT2Vp@W%x7%vVhSko4(=Y-)yVwc|BGqoJ}$lU z8}Hds!*Bo50-dmZ>blzy#%)q-w!iqr_NkrG%kz`ls9X(mni7wo0M-*Wo+Mi{#F?^y zzsJj9F9oLY)yeg0&tG3k8Vux9pPitO<=Osl?yfHg4Xv#nw;l}fFZb?yZJSnQIqyEa zE9N9+EQ9LPU;d(gTy7SrEfMra;n*U2M0tB*5WCzdQx}QNWw(Zbs>c3wXJ^1!% z#$$!Uumj>e@;1x1{+)M2UEN&0orTZ0Jr#`$6uy*Cz9nYT?w@yCKvk6{FMDxoT3)_ipk&N21~5_E!D_~@sB zZQbxQrk)$D&DQc*9$#UWN~@$d5*mD!Jj5tLBTAQmfz0LO*357d3tF5om0ewohO#`^nCaYDdIh^CX#pE-i&Yrfy2MLA}o z$bI#><1Y_~Zclm}G}2fjz--`Z_2F69H(=zG`+P~*;mo#5G2aoGGr|HFb~ zNiV*iF9}I_lZS?B?I3&+u5p?eh6c&*$ z?fbJrkM~ISJyH^1(N$ZK0#uAuJRfKx(63X}tNT*|BvC21Sc7Qpl6}=mh9g11?#%o($>ymypecHaH?2gHFH9*;UZ7(fITxYh#K zriha zFB=@rWXWreYkG~Ym$8Vv!pNr}o$h2=X6pcbBgDVjgivNbNGl^4sPLX+kLU39Y@uLK zn6KWcXBv(35gFcGQ)pCj=fAm_w{3V??^=LxX;--2^Pt7l@%11ld_6YL2E$UIC6g~w>G(e4VWXq0(zqBn&QlxHu7RR$|s{XI1_J(C^#hc!yN)LO};A=A-qD=hkW z6Y|p`kvLtxY-IzN2Rie?dgllSDH}eWhZ^iUTQv62-~v|d~^2bDGx2OSs9k-u$<{v^MU5a%(ymRxrNe! za+f@rh*vFk@Un-(KHW;)s9e_?yCe6&CD*W_POC&i&(Sd59{Hfug?dcAyiOk$WD%qe zg+wSwTZfgbhPU~xnX5HSN(GH1UHil+)Ypy ztI~;oVARi+c9W4609|A0kDMUZQgLtyrb`ifLgul_RB4+h3D3NdSxqYNYfLVgV%o8& zkX5~zlm1A)o429Vc6w*@_4f;31${Z1uM&)nW!UwxB#AW~LU|LvO6&_c$fu6UTug3f zuu&2odq}BC3MLNFPTTq8{LzmaV+#Xj--tqsM*W-y#g|fKOQT&cidK-xdi% zMaJe3PzbtB&@NOZyj~>~cdc>#v(1&-w(^P}e}-<{{`%6J*{q;ACxUDexaZXiBB3eE z<4QK_gD*|*71-&dPvQM%>H6-;Rch_(9}`w_`p zmN9*h;uFvWs}wsDu@5G^1^K$xB%cB@gO2v4?*;domI1QlxA53+G|}dBj&%Z7D$Mg)5E0e=+O2X&N25Ftk~(^Ym$XA??ZxnP4*5SFGcnb z>Z*E%1bg-U>Q!K-Y@cQ{A+CllZ^ndwGGB7yF4t}a!?ih?0}mYOfs43zW_gnRy^b=S z0!FPje!I`O0dV+yM+7lrGFVl`)(KU8 z(6k#$l-+ILe#%w>^~_fi^S4wA<1afU$KFe3IdpfT_48(b<)LrmgGKbdYLB9_2zF#V zPB;_vrfMNbZ|h@ zp1(C`hCj;OTY#V}kNspFx+ZQs z)kKcC#!MmfYx&fIbxi@{u0CD%&5I5r>L*ZW@Xz> z+`Z)TKO=x|?E<)u4(>GP(uMe=5T~yvqCs>wjTLbW9 z16FG#rrm+P{>BFKLT{I<-R6!2vHPME(8Ygl{%neeM=)ryn5e3plqE}rb6}p3{+~1RPN=JWSsWhoz7=G z71*@knBJCPoT0zFo%g0LfzUssk|GN(^4E1vG0tQ{RZUo5BpS%ckZntFNxS_b4T3!V z7yH7k&xFtj(6fR)K()eJoz^s3AtV?ks}olrAKB-szT_|?kW|X`&udLVp{_0<@WVHT zD{)XDX;*Tu5H%Y$@5+4sE++DKRw#$KWy+&1unmm_3-0oHw-FlwxFq|jc+wGUHUGR+ z0aTTZbeNmSpJWUMa^L~T+w|z=VakS7{gTtTfH?^VGEMw#w1YlTQzZiEhz_+%62>vn=Gn^UdU zRhaSF4Z=cTv;(XK@ZAM67X1h;Ul`_t`T3a}Wy(9G zF6PuuQ1~cmm`iF^~OgQ*0AINhLt}ltp_~@zW(kysFlDxFH;g0%YnrfOEJx3 z&2i8{RP;swDcO1OWA1%dE+=-H&$ZQiTz-EX_@5U*qH%-#zKSM(Y_9u*FfZ1n`lR4+ak?f1SE_J=GhF87v>KEA!PDsB+Yq^;%+HbTMbNcH$d; zd>U}Jf0;yo-93z_!@d(6;H-cx81ZXNO7ebf<=u{@L18L|cXt9Rp5{$;9=%oW^xl}_ zhJyWv3fcTZiMr+X>i_(+Xr&$b0fWpt2HCya-?nV8!GEG&ZgyrlC~D{0=Gb|SFkEf5 z|A3dgAoU9WeT}23sFsU)p)@Mh_|WBPjexTjYcq9&xK{5vzK*J`=gS|y;g~y@d~63c z;6v5UekQB)#TGEXg~-@~ktkqY75e5Za|zojwB(XJ3$$BW?t5(eQyTi;AH#nHav>$) zR;6=yMq_Lc;q9T(Vuq!BKDw_F$zgVAVG}u1DisvZXAAJn7<^}H;5~Ed zm5mCd3nR!U-1ve3!gh-YBoseCph5TGPit#t4XP@(Cty)>Ns-WW>lVV{Ci0gz z9UVja^THae0;gXbh%HlK)D8D6tBw0ZY}`zp{25=?(GM(km86SOjS>C}`j>u>>fc*` z#eJj1VwxvtwbzMPfX+c7xp4!eQ*a-=?5SlGJMbPYeZS5XxmSZzNzP|*&XD*739rR} zJ94BnwXmXgZ4*&O5Du!XcM5u9)}2YL2+a^cUG`t^a8IYtmFfez3&9sF-MT94@{SP4 zlW}cDG5@*md+~0&o8OD!@zwi_)iy(;fgp?JFG$*IO-B`0kvTtzb(hSNA+LW%JG_wf!W+kl=S~o2z&eW!7vx?I%O&jX{0~ngZ1gys7&R*e<@PHH@9T z)i`l}+J0*9^O8S4bXiyY`y&3Q2lto zj4gSmFw1sM_IFP-i<+wJt`QK;sS>-tgMgqo0rDp9lU2Y*>c$>RulFYsQAN@zX=gD= z&OFPx6uEJ)xoR4_r`zNz;P)=WZaxSW=uDAuu%JgCIBpOZys^pM$f=Jlm|1yRuqr}= zKPt#P>?1A3OWW@)P1ZRp*g5@hj2B4QMZq?DyNZ3q#gC-&?JX=<9U3pT3Atp^)vlv# zA(yXP6H5QeM(i|J(?NjFB$|eMNb*Nrw`=q3oC4=WrrWlH_1r^ttN3`k{$`se<$YCU zZ4n!_u)}0|cAEEQT{}jb{3^0)eNbqMs7doPKN0riW^~TtA4>tc@SrC%$5f+f5LBaH z334jX>kDD@d+Of(csD|%4QZPYbeN$(EQ>?5P2#&%>xERefX$>%un~E-Yl$W63+~$Q zEjp?C6x3$V-@X*XHZCOJGd5nrVSoPAO< zl^|{JD}vv*_&O6t8_IAb+Uu%;0i&PCwZ;{rXW&<)i-h=VswlNEG)x3BNj}3&Pwu-)l>uo!du@QtEIFjq`tEmyLJU% zT8H3r;4NDvVjL38_VP4>1$e%0gqu(8*ls#s($qO2__@`|?+Cw>gTT`hzUZ_`;iQ(C z=t-ih2Gyts#)7IWJ*`e{y^R0%lg7ra3K+K$Q*Dx14T3zse?YD0B>}pjXw=8)lsO5> zv4y=J9hUsDibgN9hWYf}Yh~m&7Z<0Y=Rm1v|4%LKuBX{l3f~0uK^vm(p>2Z9UFm{O z)WpiY&F0}SNgi-(?HKpFYU0Y2B$D1jc1w;UMegS%0Z02q0DJ|~B&-}sfn6$vrIyYA z85Z~*#2hx~-qCB#zs*9MFeF(8_j0sSDMt}bb;(HnAQVP;^4|HX>C3$k=#W-W$`{1u40|yf9Zhb%LSw~`(o&cM)ibC@M6FEi7L4nJ zla;TBC|`-DBby+3H_nPNHPy#JhO>cFh>uU%_j6q1I;Jr11SH?_J20Iu9pUbmd{_7K zLZbYxkd=x0t3ZiKtn917H3{yNtqyDfq^#czH+8b;#vcKe1FY9q zd;_lE28mcz`VyFz^d}XO`O)}c1iMMXIO^Chb48*&B$#1(lAe}DQ4Tc;ZD@1nzG&FA z5qASts!8uNtY?KMZEgcszjh@B&GsjYl>tz{be=w~obp;uhNQ24U<;aNN%&*XYHeNc2S$XkWns;x%6c(ZqPr#R23Z>u9>m=)M5Z{3kdJ>nqT4b1*-;cW7eQ10y zCq?qB^C?SS8KE+*+_IqCtS=%%h317{|>!kX}HI50ph6D1I3>rV52;M%B+*URJ@Sj;Zy`cu za;6Uy^R-T}n_Pcke>W~+VuYP;261=Sae)6R&HePkP9#08*59KI3$phcD0fer9CsS5 z7pdKHAsB7>)cPev0_x4ckff-*y8?FGD+Qjm23?M>2HgRe%)^ko zwVJ5q9;fSRh5Xye5B3hMg*s>4cv7Z~*gP>L8<+)=HIa29AMtAs3X& zrSdAz&hlE;L12E4j$Uo@=YH>ERoCj=!Pbu?B;d}_bk+Y3I`MbbE~Hw+swZz_gt-7E zMwr^v#lw(@FSlYtzv*{$YEH)prLv_2k)IZ=S4}rk42ald#P-2KnFYBx2+jW|FZh?6 z3fxPOtzzjLXE7`FDT>!nEyE}gxc52a0WiZtHSNL1s-4`~wp8pq_9AWMbF*vK%L0Bn zL&58dp;YJrl$~Lvx@u|l7mKWC6D8IPG#Z;^Wut-g5*HP?N~2vzn4G9aRsX0u5R0k| zY^H7Ant5;ksV*Exts%iBq87|TC(lje!6r%SYr(c592W#SdG}qe{h-TZbnF*_UeswW z>qWQg$wvv~IHk>@uHsEO`?pGB{xgjNhr$GWMJeW_stNOpS)O|yGm z455J0pfGCS2le+pHbmm3dp5Yn33oqY!a@u1bsnDUb|~(>72F(i8chR&e#QzVrUU_P z_}g=&@Oq~h9n@KW_#M17%iMK|QI~~gA&a#k^x$d?6C1^b*+&n7Q%mIrK~$V7ZLo$I zIdA=NJaOVkB;vqpX6A{ogRANX61H4-2A5R>tptw%Qsci}?lfi-mJq=@+UnyJXND@| z-hT--chfUy!^-@s@pj7@>gZ8jDr#2&2r7H2>>tz~D8&q}Ks`YAZv~jV>46}Z>;apx ziM3CEuyr0)sq?tjv>B5XdLv8gNDV!GHYL#rcn@_pQY~caqjcEO|9ehubbw7q#d?rJ zr$N{M(+y=${cbpi5qlaCLb(Q#|_3{!CU@zkwhwE%r$)2If& zxi3npBgLzszV5-^wHg>naxObyrbcc&m-pJjE@Go9YhfXA zHEm~jj^N#>Als#%$*ZR7%ALI+p;M#8 zIMeIdg?J04@zMCt^`8Cl5=*e)K@PY(q*R!h`K}T@*e$r~pwOxE%Q`1mnbyr%buYDP z=5nfyoIF$OCcxszRv{7TCt+|1q8EC{$!yDZ+nKbbhvtmSy6@i^h?vsK*G*b(<^oP z--jw@rDemyQ&4h@*OfGKc62wG zW_B02+;OQjyf{s; z4wcJav(Q#~qS3P3Ds_;w%xB1iRvcGyVIKJd)Y?ooW`?vBWVC&C*7Md1p>aiconm~3 zI^flw1F&dIklB{Rr;-H#d_9~u?nwGM#;?2_IoWEF&YIxtq2>ixOazpXR6=!@@6w}p z2(fK)DJYp~nJUnf#F)ZlqbXcvEb>Z!25D5)oKZwb}DD}koPwMA5` zslIWW)Huy}PLc(=`|-RT(kGm$Me%g)Hwn>MGYk#}>;Y!=_pB>aZ1)Jwxn)WG2Y!T=oq2xg(bn zYprTl{@o*;^t50DJ`|!f!q0T+5GYxXl~96b>cyl#+STRuonFU$4y|VndQ}n70*wg! zF7dd7MaY1-jl3{!Vtm)M_DyDM?SP*K(&;(R^In3FAM@L{l$Eh zPOX}yXMuHKdV%+opt4<_n(7=6z$(sN)5 zXZ3$uIh5!Av0GCOEXuRLOd87XpC-@#i~m0A_OtrzznF4m>Iw9>V+8hoN;^Rysw!1? z#XwCT`PZ}i0zHaxMSUaH)C>u}$VSJsXpiX_87->->g3$V8-w*hxXkI1Vpne;_)>Y1 z-I8Bfm-5mA&YM+$nWFtV!|3oiOnLWZ(Va=Nxp%iV+oq4UEdBwcN4~a;+n()Sq}(GZ zk9_^btA8Zy4m7LWu$*|%H259JIQVb<+*hf3WD&Inir^}q#XQr28e+e!$;MJf zfE94Gyz+C0c4Tb~4WzInSkqtf)^;n6Q?sJosQ)m+s z+wR}(73e#B#Z>SX?AEpqYQSe5NvyL;9g>Avp?@!aXms(9=WSKJZY>enwfJZF+ONAk zIIMMR9(prA2hl3^h(yY^O`B9wxC+=a@6vsq z*P9!LUTmqcLMk5cOqmv5dQ{ml=J%VGy3?s?!ky>QyejU#IAm{Kad8t`cSAex{*N8U zXRW+(Qp$Gz$zGkgughk=WePjOTg;47dfj#VZ;6yyo#<+sEVr+l_s*N)yZMXp;({`N z81>(nA3>LoFAM%g%M(4%yj&BR_X&mbHm|%-o-exFdJS<9=mXU#*x|<;6U@$1 zKF>AZ*AlS5UtBv|7MBM>+f7@=^!x>y{f8NJG4SGx7-3#xn>>#;~ z^B?v)u{YSlhjtV$eWq3L`q1!Ak*!nuRoVGNo#@Ds)XL^-x!xLm)9S>F^#y`xp13TR z*70Kb7L06R{t8hjTE0{DjeV7aPe!j_g9_R!nGD-rvbN|q5EsN{G+Uwe3OEBQ#l?F?X+8qxal+9ZxX&}l`!wJr)Uz`m{#2@6wOX!|;NYe^X5P8O z&vSlUva?r*)rHi=Z{kH(C?O)6xx zO57b;-i?wCb+)>ihpuI(xCY)N$HKaoNHhw0eFtxm{Q6Db%=#9UvGqXE8C;B66?~}4 zt!CHs9#nm7<-K-l@g(qBvxg2V)-TqwrrkV(7gSU2*5wI|f9n$!(i`ZOHwTP~RZVM^ zb&ZkO&cJ7J@6uhziW5)TXMPm7?e5~g+Nohtk#q7{A|0(3qRm4X9Hj5fed!Y;`e5t) zKvurFpx+3a1oe4#^wvi}skWSkVke($d4jPJ6{ThV(+Qb~1x$no1HX2{*xEKNjNQCvdc|A}mrD1P4*xG_F$d{I)L zUZ?LeWXuz8`&O12@|;P}HTXkQTH*H=R3AUB;S{_`GP4b-xK{Z5&|BFvudIyh6+QMW zzJ$U|Z<$MN+_5c~jo$?6cNqY7JsKO23h}H(f)66iI%gYi!Yn#{(Z4 z`RtZStK<9Be~6>pztXk>1ey||(IXo7hcXZo56UPrc6k9Z)ZM{KrSwJUc=tn69+qzC z0=&m{q+H2-$sE5&38!c+P3}NCpZQ6Bo(x+31X;)$2eQ-QA*0FgLUXk3WEVn2!}KzV z?Awekb4Z9vCy=*2Ma9D7PWNg%BZ3^|wpWrGHI@`P@2AP@gEJ{YH1KQ0!0VO9PtSV0 z@r*AXbRKD095EnzjbSv}};V{h-p7tae$UvTx;d5+MYU=E1M zeOy@^$XnAH_%){Yfjk^-;!$5bKE<8umT;$0t`IgpE$pd{(lV5iy`YnPSxnCpwlN|s z;^?0osQrgvr)rF!r$h2r^>l7>sZ(J(Dg-cB<9 zAVP4WY_ae+kXg11B-z8uuU8~&83ppSGC&Ml?wx$J(Wy`$m4`3d{)o`G1fwI853(1U zhwW|9N036Z7b#d&(*>>2a@rYCmw4X*wEjJEvpZ9hgWkfgQM{2^b_C0zF9iRURWL|^ zR5%@aB+!izM4&U-#fYBKHF?2#2Gq38@2hj|yP$4P?{$Li>kn{7GB}`As|#+vt>(^`FXBmTC)2krH00#PR62;f;k z`>5g)sy`1H(C74;Ab@7f&*hMYh0pf#jmHewLoCRE3rTV&Y>aVQk`_XY5^GjsQ}Fej z*rh;TUNeo*`oBm{d81)lXeX%9Gkt0Hn6+S?Ye4E@$S&)(dg=Ot*3hjSGi|=+g6~nc zSc9u?v>j_WLCZxEM(^VYR44&Nwpd8+^e>m?u!V}DY`@u3WFMl;G{Doe^JpC=Hqw2u zQFAiOin@Kb2S_KSX8J+^f8}Obq9}dx;}Q}&Sj%wrCQ$wb<-L|# z9sB+(CAS1++E6CTPcJ|hp~&bnMkpKoE_8yw14U>ly7ObiX|Qk2K1@YrPGt0*)kyD0 zXhVdPO$@#X^Yt?kIUgXhm8%xar!Tfh!u9k1DbTD*q;i9wZCB03X%z~{gwf!oH~o-RN4Ws53gtWi zP4}#ytXC;yIo}dOvmm5Ck~FbBKYeT&pZhw)p^iCuBH9w(W4n;`a44|a2zR<+%2?vO zQrDz6%(j3ka6Uq1u_G|pB1^eY`|eqDnNX4G5w;7pAsaa6ZfUA?|M6&v~PKE-k@GpAsui@r8w^K$Ujf^gcDPW9J zKs*kkg4e=%Srmge8jx;9o)1`}cqnN|DX#m^wF3+z>$J}ZQu2ZgrsGj;bP0T?V0o&h z&-}?W;jqJ%EWZQ51P154hVuGo^poQv=%|4$puK@@0yk^$VM1hMv|>4a&H(K&xdWWC zxdC5~1wd6qPILX|q{y9p9PrrYI|fsnLaRl#ogqD{Gt~(O`Yw)*g*@fa2CM`ZY@`M` z$@=3Bsk!ST@FYPJdIW3N(G10>Otaj^BVBB2!7Z;VBHNY}*Q_#qy5;#~4H=TJOrK&5 z7K~_GljjJz4jkBh|Fl7MP<(J^?)dVMxp|3gzuL)=P_Fw3EOBF@qit7^HhAMK1jMLO zX708Sqr*c!xLSh)b*)m@3;03(tpum!hX4d@vPM&Zz=k0Ic2i;DeV^95;nB106}6LC zp9)V;O?yjSJ;Nt*n{{I*&lctI!5i6LNd^Gdz+==v3>&i<=ww-dCwt0g6*|;3i4^ zKyxY;UR|P1MhB&iO^YkrCwGZ1DVT3CO(aW^VPBdfQA^~&*GdPj!YN`YN5Lm6B9Dw$cWsif@&I899} z6(G9NqYZnLm#qjs+>dU+?fGKR@o_ZMkU>CJ3y+Gp7UEpC1-bfpiX=}WgV z@HtQ6vo=Ujko0ZivdbUlU+zkcUw|)vnbb}w0*UQWnq~@J-~Jt50cZN%W4^S9^vlpGgokGoJK=6Mx^EQVMe00$bkIrAOcl zk4Y~n)q$CY1H@vMKQ^^W-I4_l~-&i^z1vH}b139Ddxrs$9 z5+>!q)md9~o7-WnNa^$wZ0&zDV!KznxdU)^klEoKQPq@{l|`ZV?&{d)?$u7!Ztt>l zjL(+LPYAyb=v-?#{2z-PGz~~Pl^_p+=v#|%F6;<59+cO5F3(REDpb4*a5{p)hc^Mk zYp2OI`5LNh8Paf*9cUVx66ATxH+lSOdBt|NSz+?bG2IUpT28UDzACt?vQBKc|m z*A#NR&iw6r)MdceuK>}UXVV)U?mDdEMyTf-Xxi5^VgNa87RFS3^@-M+1<>E^?C-!} zZ?=sNqZj6BI_GK<51#A>(Y&B~Luk=GcvNt$eX)aOU;pX~@gpAyOkYbR@}EoM{4=P4 zZ9PYMcjp{`i|~?sE>jhI5a$qp3t^@dWd=B5z&ABH5!j4`cZ<%iOcZ&B&3xmHYHIs< zszj3i=KDFhxj=^~pUQYJ#QreXi8su$V`0MQdB|Vp%D(#OSl$}BxU7#JZM|*pkk_j=2?eP7d4TVir2r<*KNwGdxYj`W>Vd9!Uvn4*Y~~!z!wE%T&~DXhlDp~ zp&on<3b2xOI&7slvn+vM+-U@{dc_3Dzsw5?ipk2O_CF)#c*;>`q}KU3f7S;ey#~Ma zu82ez0)IR$t)cuQ>Da&9N5gq2UFKE{#8Ej|ju)q2Al zh)1m#{}1os>v?YI*tn^mDTo&H^)fXn-Q1dABMQDfZiIxPW1%1qk#iw@jyo76c>>4M zKkPa%H$1P$XeV)STrf+lflYw|MOktDph}?R_S3k~1A4ABF$jpmCN#;33v}Z(amIKz zyt;au-|9goQ1St6bkt*lIu%v3Q%nC}5Q^YxLX0Lw9_=r-P%G{2rd(-37qzHE4+ft) z{$Qo1EBE}rB?M8nrvc_j;fnwpcun))trP&ULYK}Lol9a%GrZ>j#b2cK3veUP(db6g z{QIuJF1Zct#FLMngdYAR0lM$M`TgwZkYFDNs2~$n89-f+`yB(0&TO5>KqLVe(@&TF z*E||FbJ8A9|Ki7ax=)pxVU?{`-sF2R#GkF31IoXc`U>Q81A|Jng7!yu-w62M&q67$q_P5Fw7~bzt(EfZ zKjy~YZP`);Qk+Q@(WdFrc}w5>YwaDMPN8 zQf$-Rqd>|~d=#arlsQRBl-cx}o>&rKE#Wj&EuN3lV_C=mu?wRV91j|H@D}<>g16ET zK`mGohk+mc=J3iCZdWRsR@O!?_q5A2Gi|D_I>T0DDCPGaK^O7FUw} zq1mb6WSaI$Qn#C%POzwld)DgoaSYWPR$7LSYJIAd_I85K$>p59rYH7iyG5a7+clVJ z5~w)u`e=YD0$q`jZx{?vFL?ST zPAuT@LiHV+Z|gNVK7DD`Zz^82#Cm^4``rfUtFe&WH<7%jYMy@WuayenII*B)&xoDM z+7${)+r1;Y*F@Su?=Gqt~l@D$l+8_l@3UnTd2phS4-7 z?L7z!Ui8YlvYTG6+p=r^ z*lA~-*%D{$iezfkXQvyuIPRRU9gd(YHS4d0wVIhs_c`7UUo7V6@nTsmFY(NoKDYR& zXOW*au#!i_9!t6>PYe!l)$0uK@96dJi0fO7ZkdnH8Q*j!tsM(uc;1pbNgnLFm{)82 zyhZ*uU?<^_wcgj>Nc5uo0P%QrsbrJqIi%iTMA{cv+y_>qObJT5>!kAt`uTY5IR-9UAHga*`D=BYwS>c?#TrEf~=Gy&b++u4eN1kt+NBe6@EMn_8-$JQGt-Bf=llV zM3%I{MT07Kt2e82m8C#O7ZXh_`imtpLD(LkO%J=aVh>!!rM|NVu}qr_B=2`~b${hO zVS24>sNi>i%e6}cEOAOACTI7QuZRN?lhCxj(I<1|ue1{evFut0X}4uPm4ot)LV}H& zq6$GllcEfOIve!&>WZ#Noc&0>iE9NcPp;ibdF(|`yHO#~|K1@!10dnpCqq}&(>`}< z_(%KW16;Q=7g4T!u#MepO=0h0d|Jon$(-qXXF_PK1Xc*f+;bx2qejxCR;)eX&|Q?Q zE=_31K%q=Lp2(J zIp2MonS!C*u2dJ8pBvt9z=hFwX;qV}O#g1@2Z2I`=C&Zh2+Jp8;QHD!C;RU&!TwFv z0(=aLXK&h@=zp*d3^`c0wIpAlpN1|~ZfQmnwX8<1#KPi4FR&u9 zzEdw00Sn*Gb*KA`LsGn|Z|>nKv~MW9{${ZefV*ditsQ+gJ4OF?%xAwtf3PH}dN)!5(Z`%r7M_t^JOx2G*}daTPJ@1?91=0k!k?ihuQur!n%fhkkk}e1RM1c6X zq@gd%>wutD;8p@W3-x_eM(r2xl(ue7(X=VAyg5HA+llOy-WD(~zvw^qSGf@1xZC!1VUMxn0%&_SAVEQA5b z3qtYD?y}bNAOMGg;nL8M4*%+(bpzHeS3ud2=aZ(NCjbx?XDxtgKIq_K8SV!#Zork9doS{rIN?^5*Dp3>D>?L0 zCvz2k+CzWYcD>R{kotfP>CuSRKtmonP1GWJS$x_G2O!D*N$!;u8JJG`g%rdSe&77$#(6xHMyC*z92eajy;`__mo+VgX3hq> zW~I(}&o3z&m>=n-@u!0?LnP$EMZll?9jlv4N84=a<8Bij+-x<#P99rcYy7BEdC@zm z`zT0Up{p1c+!n-p^EzHyNL4759+-`{eN(B@*L}pB+MG0DwT9{nFMBbPcqPI`Zaajx z_%co4g9Xx{KTU<=x|19Mx7a4g#_zr8IXdaR;pNNY6GP;$`I9U2N%z-> z>h4#h=hy}UGj!hf@kh?WjK#I?9Q!mJt`OwrMFbHViI0B`H^`v{50K#cl!*kkk$e+_ z2R+pWypV=rN_lw{1at?H@fEdG|MXeKYq)!ZH8~yy*)_psd+_edmO3? zFB!jYDLbN%pldJ*P_CXO?bl4F+XpPOGbeDa0wAgu9U>ah@qHH zE=(U+Sd2fHhb}Ijs$dev8c>^x z5^^&QP-3?X3nC8GhzD|h=!mO68hY<9N)5PUV33}WXOF$GpG!!eiaaG1byslE9-|`t zS}PBrT4oK#N><%vSuLZ?K0uZ6AZHv1t`bX2k$@}jdZ!)Wh^nn#!;iT)q34)eD!)|7 zLP;ePD&|UC(>XIkiR%UDjs(v2+8!^d{3bPvGB*s#==C%}qHBw%`W#yv3VD3kl>`jy z*>ZN?A^wPT-m7hD^y|w(3paWUdTu_H-U`HAB-Q(l#vLp+ENEN|5T+6H3+n-y0DmrZ ziY)4PRtb85Z$8_=WP+>>8$ba>j#p;Sw!J=;fbtcrSIJ7qv7^asv5;oWRuH`jBQ^i| zdkiqve3z#qE7m-@(Rs2666>vcZI9a5HkSQT87`XuWOLBKPv_zu6=V5;daosg1diYn z&pu3bWa~8nPJM-lCHCA1;LPKh@eI9`yB3hkMmG_VIxY!7+5S0|!iw84ONx{7QxpIi z3Cz7FYruF|wm;+l25lf~GoLs3QhGC?$Iy??M2A5^`~Libpb}*F#Vy0gq1sV{+@rvD zjl-Uc!1wDsI}wK=N12;9tEt|P=Mi(2W^{nB4xF+bX6e(b-~@o{b3Nc$VH7}k!yBNQ z>-k6`7R^baK_<_*KZ8`(LMw9EirAT54LqE|v8IPh7e?E3y5%aumvpJlD0Tk^O9pHT z4O~o++zrNT3=b$@pZ0B84B2+WE%+ayX(@kmNm%{B z7q<7tzG45XlXN|!f`M%Z6Cu5WLiusk1vxMeQvGq4+T_R^Fngbq0(6)7(Dm8M$g&50 zwoQaNt*@DYL}a1hpg`SXfZwY|TN3l0o|8cH z5ma=YdNI><83GbUOtdTAHt2ZQ#r|sxqxj!oeFp%MEAW%J<2)Y+`@|o{6rN*TMwXFu zr92}Dz4dm1lgwqdQ+NCedL@+Xj&9r7&G9&{NNy8X@)4i@^09VS{5^uq#la)+~fd{}YPCYd#bP9-)ux7vah34r? z=BetcHY8Ik9To4~XI&@*OwD2kk|bWqe=3J;{2Th+zhTOO0QNg7LJD@czk<;JI^dsl zalH}@sF}uwNrJpXx1M@DDtE!ic<6Y|58QaV?~?ybd_kY+1}qFovkGb}i@%jynP2{X z%l>YJitqh+kD<~cN9#2Ef^p6Y93L>M><6Y{eKkoHPfkZE`_81CkXd|R!6dh%fg8+aORxat#!ZmYYB^buuSOx|=o?Xj&mTlVdd%ydT$*m&!LI=@nnC;;sj}iFCY{Z9<2ct5^#hf{{U%Lr{Jjj|V@7S9&j= zu2SMYgS%-74{x1l<>$q7{5Ig}SJoraz(7k*0oh`_Mnr5LJScidlUBT0JR~WSC04Q; zJJPr*ACNrDb@+fkX-oBYf{SP2+D!a*9F(%tQ}=rCU>_Unwev0cnJP`9(SPIZn9+BRE8mHo$>|?QUT+TABUv^o$DNn9h`a$`qer}?JXH)Xj7Z^+y{Im)^^312x~W=sJ{}o2sH&wR zdjDZ)QZUEAk!jw<`0^X9th`O-n)Fq&Q5b1&N)49AJq4fPqE0OkG}F_c(a zxqUox@138C$C5sXVFO?oCB<>z#K+Nyk}R8ERcAX3Pj!lRckWwp5Qt^_HZ{S0gm{B^ z9WA1~v7)m&A4{40p>uhQYe?@LY>sDV4r%^vZFJHozK9nE0wR7be4a+LkN2Iqf?9ef zs?T;_I1t5S9yoIXX?YM5y)QF9F>6SKhwjo{cUvyC{y-L8#=nT51} zBv1?aM*=mT@>J%EjAF-Usqaz_^_Qsq89D+4z#yQXgD(E|pjcBR)1r&iE00tI!G!;d zxc7`|GK<=V1sOy^#WG42W*pnlrI%1f2gX4~L_xZUgc2em0zwENjGbZyLP=Bx5Gj$8 zASDS73MwVi5=bCH5dwq|DM^43@}8iMGxI#_eSdsEzO}Mk%bVoB&wa|?*LCf^&$-Xp zZ|P}#1@VL2>-zNk@C3Ru>&TSzy@=QYxDMfqoUyCoqYIT|5xuPb!#?UoImzX~1(a}y z`(&I3TjtHL3xZkEwOby!bQ(V7USLGi&~IF)!Sc<#& z)JIfz0@imTkM-aUAIJDEW`rkv75T3E`wHc^#lpOhU#GCIGO2G-`5&^^o0326HH``(x(YXQ$=PT^0YC)mUMs@Gd#{8kbF zAK=Z{aO0Y&YlmD4uMD?f%zHL3{joMM03!AD4^Uip+P*5|&D{6IU9~A^bX*)~E_u5Q z##0IJJKQ6#BFj}}PQfl|)wg}BWUPwM!&759dM*U-2+iLtqlg+u%{fV-zXXQo8ZvWU zmv@_)Som~8s@GzU)VbR_sqYcjN z4vn)mESRe?4QRNWv)|igw_A}%zL4@R(j%+6FdJ;Ia>^w13YOZyzL}hTUy9Z0a$?!4 zTaQ6zmdvX~PF>c6#gT3)yq=H2#qwJ8Bg`Xne}+g;C}3P#v{Nyc8<4{+E8=KntnqAD z0+-hN3!yw1v;D_~N0+OQ++&y&+y2NSl*CH9xHS5}fd5}>FyW_AfwnDV2Y{_7V^S~) zzC^Gl8N#~X2fy#FO(2bYrxx?*gnR!iDC|)8*t6;%1mx+i_SFw5bB{xhCc5s5cpfwm zdp~3MFO*ymA+p|#=k|4G_`m%#Eb#PYIq8Y);0n9Vs)l%YX!TZ9BV*_g0ORJfDFr`n zDRUW~&nXVYlw{uwDD?1gcac5Gz%%wbqNvO94{*yJh8<^io}~ASZnb)V0{&JHkk5;8 zBbD{uGtG3-dDfbg_;WK9sGsH72Bga)j|Verbe$h?URbEd#9IO59K}l)HaG+@shlh51#$n-nFFTPu!ia9uMj79 z?hRPgd_Z#qB?KC6K-4S10bcL&^xbxMNz55!&LZ8tnG5o|&siDr<+VK*stZyr+Kis; zecVN}e_d@bp@e%ld3cKfC;d)V=d^=8J?(e)%hN`Qdpis5JUjn9eR!RNl7bPfaCgDL z$K44pPUO88b@md4?H(|(87q#+nq9E+IZ0lO@8}QY68JP2H>{V9qaw&jiOz@eXcQeg z+vULw&C@5!cV64petrtY@JvcHe$;jM{V8Es>b?Ojcx54kfLhH{Q@7lCdhX*QBig9? zJlx3~i?eIY9`;tbmp?t2^MipQ72$*;zippfD;*2vk69l(aLeCko0VtSR++ZNhV3#v zLUD0&Vn3uhQzc2dS7Iy z(z_Bdu>i`CB+H$O-F%6glGqvB(k9oZD$@p)sowv485i-QGg4Nn_m6T+pj{)n0@0x? zRrIsoeIc%0H`s5OvR&5q7sc~!DTxKs;ZLJw+CUJ^QaUY$)pmD0q1djG_9_3$ZeWO$ zfZwz|c<^9SqAD*f(SuELLqAsejin%Mf0a|Z{@C2 zs+V9_{LYGwsYi}>cUFRt0H|-;bO$6&78m>$3mv#y;c|rN0X+j#yiLU#Zi*eznpn2K zI+&rts*)ZQU^GO!UJ%10wflV< zs&>g`rM~eA&Wa0ry(@k~A;q1klbXJmLttsIk*X3lfvxPRVfo63aa<^pxLa;4^)ik- zrNm=DNr&!A2B1Yo3c$ka*RRV+)gKA)E3G>rk}8UuF4r8o$U58V@*#8a?gYfiLh{Y+GAm#E`pp%f2VkcnxV-WJFxa1y z;J`yyW}w{)I{nw}L5n`)DDX(|qu(T2>}UQYBLybOPZFG+1fH;RO%u#wfJ#=LuyXC| z&}@$Byybgg{$mw+uu5dJ{PNy4uwByW>1Zd2sO_X@vM~uWJ#=8B#*Ub|ij%^)@m8O6 zxdV!4yk1`F+HRWT;=`sM8F0)Wq*h>`U&Fa3*1TkE9x%RY+F;?)M` z{2{yLo_9nH8s74^YJPsv$%2E6AM`&BQ&eoJVeJ^;8+wFsAm>_*t82=>H2zBuYzBQJ zBPHpfsZho9<#_Z?IsKi0K(6%3%KM)`;~($jp2Dvl4H|6FKKv&`5b3T6b@9wt|5!h} zG8~m!*5t z(@R^nv!Y-m3Y>zZPTy%c>zzS9G$oE|s3qhROInV#gbuPQ?8mb;HEW8IOx1`;&&alp zq^t9}=>a_~0{bCjPnwQYk0Cjb8jIB-EZ=~C9Nv*yTVS9MV^3OF@O?-p_<=?y_SC_> za!THw1mS@VH!{=8Onstsl(ng*Lq@iSy+u@nvP5=#WJYYSXcrEv+-5H*vB7_+N{%=5Jg_hb%b-2Ct5l*vjz`18Si}P ztERnDt3nIY+KPd#_i;@h-wGXVhWgwHjuJ4kJ;YLu`EI!zWX)EY+^z;S2y!s3g#YXM!`a0eE!kvG!tRoO3|2T_BZ?Z|5uT=by0|Rgptgq+ zr-LJUArjQpS*2yTRMt9a3O=$G*GCtiHBX!PWTt_A7FQ_gxm`@-Slitntcdn%liWl~al7poU405);;^l2@fG`l@UAXE+N`2Yn(D}n z<>g!WKXywCDmFT%jb-^4b1B(_?TI#liu#DZEaLC2PK=AE%q8q_gkfJqfQ`z3rhR?-dtk-D=bM>JGz7=S7V@>_o*3r`a^;?oyuonB z(mj(C??}0_r`%lNPCCLmEVO~&R-rRWk-YZ99W4|C{Stp`Hq}}2nP5EVMq1ol8Oay z5v|rDD@E`h*dkL6A7yqdmZ#k@*^LlU41cNdDsi;4g*y_WCU};XS|MkSD88fB zk64@vVNZJa*6BJJ+Gb2)2?o!4C_c4V9YK4~YN8Dx(94ATsNF?FpX`rGz{Ux8cyzn(p1pcgysh-SUGwu7>#CaVQ=Y34jrv@thQqe5hYM#nCqU^dN8 z*1~~aVqTb}6~GYH0;m1ieLT8)G3!!vZ(FeJL?qu%Nt_8?zUH2H27Z5_KrH{I zr&fU9Pjb)i+1ctE-oxB4Z>?1BM?dc9*<(Fh8LDe?fZtO9hql~t#GGiVD9>E~7Hyxt1lVSyh+reA z1IztOA3fi@f@(dt$dnpb{iR<#`aWndRR3fI&&x?23rW+js0_bPy=4;UzgNS7TRhTv zudVlfd!eg#tMED#ZoqIbl!iKWwphS5%Gx<<)g=0&XWOSB2YR42wszoXXmrU9bBm05 zYfL>OE4N*C4As$sk0dLd>ZWu8q>lTZGlrtaD17A4wT&}y=Iq&RtJ=%UI+2>(gw~YA zSkcD{NH(vNE~W@=)#;HroVrqxF`L>Mo(sEraF9itZGdW%r%3tkI#t$~@1&}TmZRdE zoiW?Kum|0BD+u`LcT$k1x|(yw7iV0k?eEMTL;_q(r=cr7wm-3?CH%1BZDF~Jm&BC8 z+el!St!-QO3%tpblc??%z}+-@ulv*!(C+m$Wr*g+47PSUwE(6i9)y~IG#O>*yGa*E zZwN#(*kU}!-IHznJfN+Yy$4Ul8yo(SIptdg8m`4rnFIxz%=E>`i)wIjk*<^pLV0e| z3ai`->`fVi*>mH}hSa=#oCm{IiR^4~KKtuXW=Oo2-$715x-`^u|vpOXUxbwaaAqt|-r zw0(Mk_WvzAYgR}1Lw+OOBk>KgHMU5@u=5i6~vj)>&1QW zzsmbx#d(y$6)iJa>z2CAoJsr4BfsK- zT20N&LZt_ri$`qi*{;LL6Z@ZJw43#Y_Y_g700U&UQQX(R@E9(|vj)IKD7r(naZtQX zzuVeXs#D`a&k^p)jG@hx->`Bmi9v>X3Q{iP;G73nQ4bEH@<S~oM&ifS@4tdx4WKxhkx5p1*pc8OP4cfk6xYeV2P6~!K8c`crjGSV;pE^<=m zfxpTFr?-OQex<%xWNP!r48*_PiKUsKI}6db_{&?J=4FEb-9$vp5w+Q~CTR(Ofs*&1 zay7pAjn51yf*pqb9m49)@`Nf+#+-@aZm9*gg9WyYAtV$FI6{ffT%m8@{MP?RQYLX? z;I?15JMd%@2MR8Hb&(R^_Md0tXJ6;1tM7;#d*9B6@a7D!Z70m{avO*iuP(`yvP-!I zJlk>K0oxA^gZJ5sJ~feCk@0mQYYzt5k+tPBwfy3w0r_UAoOHCOAK9-E4$70a{i1GI z7+#;8sHCBX>~H0UDk{Qn!sS2w+s#M-+G+b$ZGd5YZs%9sA?NIGA1^=_ZjAFU28pay z@i7uE)i4-z0O=9ggPzaLWJR(K&KVcZ*7YaeD!N7&^fxeQ(eRv#)TM(Lri$v?85JM7 z!&we3zL|QE9|kPb+&vvQ-rZ|>LX~wN10oet@hJM;mS4kicxo-os^Pxw=*WiA0LVSl zMRN?cGsbITc2`6#73TjAb6yPK$L@T{K+XxR^(bG%tNNMwDP z&U9EFM^Qvu+$)H|it?vvmcGCE{Bt@qUb(u8P~^zoQsVbxD!M0)d2%+;36!g!bg4!i zuj0R~488D*oBiH1-GJx|JJHMU34m*~Yx{Il3gS^<6FhE|RqgtIeRxYQc@}+l4`Db+ z2wYg(;yHY&x&v?#-DDiZk!75It8)v5_xu70&sU>IM=Fd4w9lo3bZR=y)RNrN@j)3E zX_UcUuCWU6ZAvXLxQ#7~i=Qz*9}2n#lbUXk5!hm^s8QFr4#LJyx+MdJ0w2<(rwB&-M?@F5%Sxe%va1_HE;h> z*WqVWigQ9cjn-C+t|fpVKb?cR-2c(?q~h%wp!j6*VB{0I$|W#iq@&_(G91$E4;XVa zFKebUYDvc{2Umnvwk(?^s}(2rqxX8gERwQhDys+>=*Z)8Me zhJ=#aX3?m;)Wrf~Yk{^y$d!iDG;2?rk6nh2te7xGeKxiK6!0&$Sf6E@ zveeFe5x{+HO-(ZJFI5>d!*y$C6z-bPU#9#A`A&gaWUZnLp^DGu4kE+)OQV)bA-z9g z2s=-+UAl@cIoNXQot5+J@^E&xW$Fp#nx~`MJb`;C&~Bpv&WLq{XQPT1lt71s=m?$L zc_u!?r)bMF;cXqWz(Mh7G|;8e85XiouG}d^x&$Oe?pM_?K0F8kA zC7;oXB1i<7e~e{va-zr4U8g)5yEA!X4`iQ`ibwl`+y~Fa`;BJs&*nefF}$s+`kZNv zappc{$cq6(*UaC4PVH#XmJ5>g3#$i6hk(}x>}ellf?CjJTh-<4I9yRqY91@|4+Q

BrC2%1JO+5%1S7!lDfPep^hstVCekOT((qoFpd1BOs#`C8f9Ytuy zw$of{=<-{ke!nup*1I{tz1K;5s0!M)ZTf5|znXh}V-z(jypP{A?g%+iH&%o7V_4Q!cLlxeK1!_*B?)>!sH1Gd^x|Z@( zi7&YZSddy%QPa+J>!?eg9$n+Tf(#WGVQN)!Qh{&jifJxWV+sbvjj7fdL1J^P(-*7x zH}Tu9Ci6#aT}vZewm(v(s)C$JoE%a3@J4BzM>ZGk)~Z;83*Ow#2I8u)AKm9RT9+MC&-UOMdq0La85;cMmXMYQ^k z+3=1jcfxcf{z(3Gr)m{^A$!IKC^aUsP^f5(Q9qaP;(CEK*1!C!biS~ww2Z)Elr*lK zB==RrJ40=l7{v20IA z<8Xo7(6RQmx^CTzW~#SgsZaaEUz*HVR~>Q{)JDZ=^IEcJjV$nYP%7-6+h@v9<~f^g z3-i<75S$9BybH{BV3dYKd!i97jgww&89J%d0)uCP_O@XfFaST}y=S+^a?uZ{H{g@z zqj73DUj*wk**pj6;&4DF0MFH-wq`3!aRx@hP3rV?QVEWz;IF#n3Xy;A8qz+ee>D5@ ziR8r4KY53zLVG@v0%g6tw+Y>2A76y(x;6!YU;y1EwFhWNwcd~|aE`S@ODFqY=tIHb zYUAO2c=Xw)uj&jBoOe<@CiE{Er>UwEqRv)*I$>wKVK-1UT#N0A-iRLrRdTRl9f z@fX$dH><%hAjV>19TIb|bIa@e3l<=Da%vm9IUJ7kJBQUnROwzYyYLoNCo~-oepnDc zxLKxDc{T23Yw5)kK6q$DM{^^HJutRkAf4OSVyJ>8!2HJ1-?MRu=8THtSZtieYf;== z$t*hhy-aAWZjFo-XEs}SSM&6>1-YTQZ|g{RpPUkEqW8RIl)bIVPYyrWk>g4^s<@cq zl#U~7rSVpe>R-vAmBXu)rC^xn>72F{5$!6$4g|H%h{}+W`v8ls`UMahhURHUn^fAp zOt>k=w<=zX*H3 zDe4zXkxb>D>|SDMK5VzjLYc$b>dKsy(rNdEr%Udbgmi1mW%aTpTrngWt0MvqW?JaJ zaJN7)06L_Ly1_X#S0=is$J}%q)<|ZgR`g&2t%fS}WKNIb(KdHJ?$qcY1M-fdc zvf88}B`VaQG0@I7Z{%5kLK|3|jXvGWeLzt?dRam^_2BE3{ta=TZvk}X)je2{-gi10*zdYQLFX8YJX-7k6o6=jC6E;!I5T^Zx(^r=-xU%9s)UUh)}b786FDW` z0_d^9Oep;OFS>%nbOLaH6j;?h&$Qt}joBTu%B;0me8W#lW~QtL(@7_yEhqi6BsTT_SH1W*?}5E$Y!o|YP$Cu1R>>uuB32(YX5}LZ{7^^-elW-d(oCT+V(s!ab;2CsfgF0nAZ=qO zzQMn|0C?u^hk#z-d*4%?`DY zS~X`K)s<=aH+=>(bHjI1nI=1jfT3nM41y@iL+gGfqAOu5h>FYuI?;((9jj-sP4>S} zj|O!*ya}odz{6H;Q|^cI?i{j~mpz~JROd~^O>pM;@aeg5XiGPrJa@britQ872D9!;61X>|7Chhz~Sl3`bXmh zsEs=qVoQ?EPZ{D@3Cm;!pKntc{7y#7bf4UOZH;*5{jAUQ$TjjplAF$q@y2DOtlmm1 z{$6&8Ia_5K`0O3lON|>veGk?e3J2@sMFcm$Rms~|hU~NM58keE{W|Ht1`=5H%=-EI z`SuFCMvbNS*EWzu#En*DhvoTvh2foxGez->EZ&uWOh2$fLBBlD)f{4z#_8521Mjqa zMT<>X!mR03n)y;LEPOEooBX?|~CvihdH*RDRZ z`z0lQplZY4c4!4xOBxB;D6KN>-KOxGdBtxwgNvZ|T1?L&vff7$k8~kvDHmY;DO&Sp99_ts{oq64(c>wJ@hIvtmY@65fK+a!CZ^ zNkP~ddi&Cbs@-i|eO%6a@4)ph1FdxVvXielExjsI-t{@GlCSv?mya6odrMW8=GE(0%x>Xv4hgpHE?qA@W0DY5BHT zvVz`?BTAs5K^c_ zBe;|9_Bj~!CGftGoT%xub&0_g%5_VMEE)sAfn%Qz*hB^V)fO)(_q90^{7}D(?jl-v z^}OtAyvY+!q>z7?5&BmP{zJXVEzJo*pkV`H$&@Mx)9VHty2nCT!3|0{M+ISGsLk1K zA|cVN#y=ROIMFoVu6CZ_=#Kxm*?~^YAyvjF4pl|wqq3}D8*BR#<$`%dgUBSFdUy)@ z7mM>MZ09K@oiIHf$gLQ|X~kpcxUDkPsPCq|4aH4kWO@o-x6Fm^k!}iA{Pu#d!UfGR5>)?26kt#RXp!(XRFx!rmHo{KSoLGe;O_AFK%(g zJOT3qI#u%d4=VVEY9;n?Wt6`T^vaa^H)aQy{)-a+uN?cUY5W(fm$YQXss0_Rq`to8 zvvju!=&DnH(i^zTYs&GeRzQw5eQG1oW+m(y8me|^&Tp=OcwXcA4S}T2;akg(D$2xt z(6dHfT2j||{^K(7#Lk_n?h@J6iO2yg`vUF;jpkf?aP_lI$f!fIr>g_sRYzJ zL-Xpm`J_O9-hE+i>LI&ECcm?otM16^L;mMI2WB;gP#eYL@3%8~FK(F{{MFcQ4^^ru z(!buc^%mT?$z0(%bX!X_C&JUUy=Ok|_heqVQetP?Sk120yt<^sh!m(d0>sb zXH6W~qy6jq=SrL+0u7&)yBy(c^O}>sg_7z!%w}|VwO5z7bQ#*zC~-Q8+opO$88B1O0!WpyKv1+v&2R@%-v*KMPiB1v^amK_}Ywgyhi7@0?bCyc)Su6gbY5v71 zXBS$#_ z$ro;29!M)`$3GDb=4^UaSO)Th5K!3w2;LcSfke@Nn)ZgmFwuG#U5?Go1kVCn}f+!sc%fsWt4c1B9Qz;B(sVT`Zzbo$d3zq*eDN%n}JeHrZoJ8KV zVc?JLuR$pz^sGxs;Lj%Ar78i2-8g}Fd{PwjJ`a6XL%F~IUCF2qId@Vyk&u{=d!%w3 z2dVHe2?C-;-%%laDNo=`&!U^cVB}NfBHSK{s%uPz31?UqTAHd|ML$R2c7g0C3l(fj z1IsT=@fg+eq$ddZc($@ql2-jalfN2@J9f59$nh%aqsr2`trs~D!4Yzlb+!UWTdL%IVg7K zv1YdRVSbj&x?|Jz*o zYd$@Cbv1eV!;sa1p|0mIY27f1Vw(HbXZ86b@K5MX#6puxhU+D7dhV8#Pt7KJ7#mx> zkpu`RBtP=a6K-CW4tM2wU6;JqMcT`TLu@3P>m2$z1fGZD17$NmdfoGlnF()gzvcn} zleS{jz97|Ga?-TYL_V;h!sAC$peIfq*uat<}`sR2}13-@d^fet-4kXB%kfA^%cz-l5tvUJavYj6i9D{B z>6oG?TO|a`41B)Af}~ZsonBnXce$7y#6N+qcg{KnYM;0M+YI^|P5bxH{!aR6^ZqBv zTv-pm#{G>*Kf8v%bw|n1&aeCH)IuXS{?pC*oJW$B%YOvyhPKH_`8~E;){7tiWE0m< z)v#vC`$Yb_B&xCzIJ&hSC6Ji$%r)in$u>*x%6a|-%;Gr4H|*{5v_mwPU07L|K6Qrl z?T+9@R}F#Cz2EJnkK*ZwYkCVBB>txbLBgx|LZUdqGH%jegY@Ha8uyI5(p$e$wR#ee z$imc!pKg(67M}euzZkZN*E$f}8bxFu2_w)tXiTvL2lvLOt<{Zx|0X*3PqM#d1t9`M zH%49zC4Iv(s|M608JA5G_oMZJ>jJNV8@?m67smnNC7$#js-rLVbd#+}lYsH+6E)?O zrpKKU7T*peyvmI0eJ@WOGrpz~OLtPUp1u)i!;YRF+z31lHh%V|9+LBZJ7XDJP8k&b z5vwHjvu5q-A_2@CcWI==HzA-amwhSSxWOqnt0n4!#(?8zaqM%_agMT#QS#*U9&|$% z->arVd6Ze8Caz7}nei%S8RU=VUBrZ!AB9Uhs5+_37*z>;P@@EX!(4zKvzIa-U#(41 zg$uj{mkIu-!#o3#(T6>U`#?qs1Mf3Kcud_Go)Q^4^6<}1%rfQl=V0HIhSi#f7ZR3` z;<-=7PCBZe$nm45OkrPo95ipuhA?q~Z$CWo+PE)iW5jL%B%61~dpc4+az7L=*2FRC z)>=W=i-v}yr^CJN8$iCBOko!{YHzudqwmC0#at+r;ll z;!mGyDzxLbkWxPly~(q)Ew%jl#uhsH>P2qNLVXC3_S9t&hOJGn)5tmsuSpvX$BeOv z;ghFsub(mT{2^xq?!KfxiqsCHr@&<@NcwI52@8kB_@kjEWz=7;6u3)OXydoj;g@ac zVup1{ixRS@Z8<1m`3Ru@HM-X%)a!F*_I`y&j4f!~eS71WSo^bC4hzAvo!$Ao@ver! zK6~U$HK!=v*D>xRA9}B3N-xK&{d{@#a2AfYb=U8)^fuO?+3zC<@p0p38E-i$X;mr- zkS&pCODXtK1AU9M0$^6^aaTVbUz_7hD$5Jdr}2Xt>@mFoWtc#?dL9cm0uLe85OW9D zOoY$jK@{>-2#g>*V~*hsPFg<$dFS0&9?@8Z9U}sX0vv%whl9}b$Fy+hT+7USO-*~j z9ToWuchuQdA>;;R{nl-EO{O9#^hBm^F z5AG~=M3bW3Qbnht=Pt%%QT!~p#_og3NE{mqX5B-dxJbbtVKn7Q_k!BA(D z*2*^z)<1&&+0Ms-NS|Q?ZKh#xUN^_HcJf!_TL>s+Y%RM0k3Q3UVAB8W;b!jB7&F5U zP^X*eZ9GakWL5}Fb9QTxT^`wN#)g^wCr>&4>r}}%xs@p4YF6Va5x`w*~UN)YS1_HFt1+}*-nVB9;8rC zG1&P9nnUGnPd-%+k=|P}=O|4F!j{sSm?DJu)7v>@yBlfx)Av|pQ!jJcTU*#cGUx!> z5J$8sKU3PRJGSHv;jC-TL%+E{5b{6&s42Iah>tkoXhXSG#PW~P5g6`Y0x7SQi~`=# zPKO247}tY^9S2tc<>%=)DkIfvThe@P!`i2ZwvQg8$75xIilv$Js(0Y+i-X4N5zt<3 zIkU*xTd{G52cF}3Y9Iy8bsk91bzC4^t4GIp<^3#YR;cYvNCV>cso9V7*_x-hlBD1{ zn(;(^KXORT&Q|Zw*YG@CU{peCLLet=&{vQ#a5DgonU2OLfX3Ay>jFXUOWixu)rqr5 zm_lkm;_~1%to0g=WzzC6Q|3D+f@i+GEa8%u1c-Q8bnwGVH?36}oEwY!L$kuwAXN1- z>`Y!~n5X)(oV>@{ync{_?Va_plYffAa5_&1J1gnlsMY(Mv_D7L*@^}Zmxojf-e#UX zpq;sFc2e<}fJ*C;myWfkbvZ^k1)S%BtO}0D;!ia^kn9_j2lO(AS#Nv3q)TI2eSxrRV9eqRcy60#>3PemjQBD_c9 ziwhVO;?{XJy`Q$rc!9GHhi!N--+%8ra}unUQDJ$?13q#|G$mM~Hz5;P#@8_OESqTF z;2N8GE;i)>L|AMeV>sB9d*iNA?bf5~*N5E54!Lmc2VY-`YL%kLyW0s&pFYplts`W( z>;c5|xk2BO0@4YUAdpW{wJEj`iCpty-{M}(Jv_6sbp1CMdoEt_9r_$xFKfVM$K&2N zAfP{YxUBzP8&EB)@RHCL+*1@cXegZly^=yKfyRpBK>S%c(Rtx68*Ct8H>kzE&>bi< z_t!(m4buF!ATyAc96~;>_zZWjvqJBEIX7VJio7JcXwCMPV)8eNGqtuncL23Huw}Po z%eKi&GbH87Y4g|IH9^K=&)*dPh_NxOrME`XqlTl+y~a>;F&Mc0FNS=7!qU;?z2YtQ zu|lNMC-c46{XIQZo(Qay8DWo+6&U`^2Njt~PCj(5MtK#ky;g%3_Q&eKQ+^U#0Tksg)6&A)Ev5N?5DAfNLYZs>14LJqW~OaKFW-wuvqHTgCvo?l zqjl-}9iB00fZ*Q6Z*PwEu?F37Z#kyE--!Ewe}iLkz}lJ>)oX$~Zd;-K%jIo5s+kU+ zO}4&UWL{Rh;z!(mhwwX(z}kiGp&hS@4%NVAuA#=Gc&op`#moa*PFvbSPPuIff-a@J zjVRq0e8(}w=pGy>wR`yND9{iY;>lw3BNu05ME;-e9OZ2jh;Z;g{BuD4g0SCvITZOC6f zN4aIs*?NL-=jLbgW7F}u?AGmiF7G4M6_w^{_`kYl{Q!Gj?m7fd!& zc)|h?=_ifsdeImezUBAfYLr)zu3n9i=OLi-G@{ma+d$Q-?^+qi`;A|D{SPV#wt#t;^te z%l|Dv^M%3zUFmO?WChSkbQcK$U3u`|fE!rnZw4BC;eQDZtteCyQRee)B-*d!^M8G| z8Du3jJCM96$)J|UPrnJo)bDB8#F1nlg>l&?ec`|-u^7f=2(bfYO} zw#knH>dv5Z*ZPe-uz|}qK!>4*++>t!YNky~60}pz$bQ@S9sZtCgLWMS{6SZLV-VY) zvJiQe?J!JxWtO!M>`v-+(&sVRxWNvkGJU3qMv^F?zO zSCT}&Su5T9UPt$+n)y8q%-P6}AuKfiBX;GO0!P9PDfn+2+@g)S`#%sC;!C zgj4Un??D46HU-vBq^*f03@{o>sT$!sCZ^NSJIYzlsL{OfkAQ_K-|3`Kmg@$i6gW$1 z%`c9}JaI;3r1^(2IqItk73db`SRGs4(VJM{OT9ms4e(K23DPC`xlaxydmw+ht_Pbx zZNR2loAb@+^Ct$#7N?TXT8 zhxaPvhFTf>n<^Cg$lX4|#9N66U?JWzS^vXWAcC(XmLB))ZOPJ3fW>=eko0Q-Bj{04U;pHf%8pAQP2$dhJiPg-s;IX@2ct8!yuu;8>=y4?AE$@1b+@S-mQv_VOGVfr>47;bV0psz5A+aqn!__ z(bGbe*S*5-F|@HI**pb?#05^46aMF$*cvZAMbQs0h3N7oZkrlD*)DVTKuPwc-gk8! znwoq%A_)(-$>8i;a89ji;L?@{rYPH|h4Tm)>QA83z?&F&*Lwz({xh~*gHnK%)LTFR zgi)!tvne)eus4bo1tQNN?~Oo|WcktRKqeY4jPkkf!SQ?Yo<}#y^dR8%jP&)=7ar=% zvnUO)Gh1ao22{m^V9B>dHI$T;=yB$;N-D(gsoTvW#ZRL~P5Q^>{DFFhG6C z9S=0#!c&HWWVsoF9YC3$aSQG6NLnI;d&M1;& zus^W5Kiguo6OAY7jKJ-Y;Cop(C% z2_(yv*ja%tA+q7o+hlt7%z=EkqYG(?&ZJQ$1+V+yn#caZOf*A-*Q$LwE0s%G))`I+ zQ+(Eg{g=J;I;Qka%dktU${8c8@!-y>b<+Fu_XN;qLabOK%626^h*XZia*?Ql%HOOF zJd}q=F#jiHnV3j9Sca^ZG&OR3?T|(J(-?YMq|pX@|II0v7#)RZm5h zS|dQtpIeMP!sM0L16IY@2_mmCd9fR_cy7|f@4B~~5fm>Q zPp&#|9=KE3f0ldnZPbl5Ls69@uW*ijvJ~E<_^Z(;F( zV&jQ#`2b(zaQ}-m^I6jRQs(kscLV+pJkGzB{(<|NL&XA7$NLJ6rOu|B1Xlhvk{BLl z-N3TVKP^LJj; z6TT%Oc!SY#0?+cct)sdtw*IT;52~lP$^3C>W-)Au(!`7zVQ(NUhB0Q{TN6fnNy78u z1aa>*YW!f+HL-xSl$p=xtx1@F)D%YIt|rgku~{C&p(<^7-8Kn0aevt3BYs=e5h+Yw zVa*OnX6!Gjuc92dH5pzIrrTQD#L?+K3U{)U#>e zipW;tIdOvECf01JHGvnl!DhC~X@|9d=%&FCUuKd#_<%}K9`r9&q$8J}fY9zozqfw` zFTZsoE&rH8cgY&3`n{H4sli$S5|UZvrDAyCDT+l6PQf%?U*8lCwITxjNMnb8>Gsx| zDZ{k)cvPOLpSFgjkCXW_Kbs)9BqmK1 z3L%5~cs`>_Mryl?%+f84IP;om{A{$1c)^>r%xKbE#)uae$kW8ctRLQ4Cvd0j1*I3d6chLr*Rxh=$g=W=Uu$%S{#ky_YmnKo1{5=VkzXIv!p0G$3 zlzYsWe7Kaszeo3|aTfF^#cH{s{j!`ncAhQ|ISEMtnv)@-5v+4vDRW`yz1 zAyY9&=<`W<>CPwfB?1IOg+KSX_+&e4BI54Wd3_BvLoLBGLy#quKCfXL(K)(9J%{~P)f zzXS4`XFMzN@R)%;6>;ES5Fxps_-?Uid*inSeLnuLrEy4wha>gGy(bx1)C0nsR(2&c zI~83Hzv_y3Oen9qsD{hyHOj@Yly;hgnPM^YesClRvNr%eXk2jff<}OU#6@%lC_31F#A>qCtqr01hD4qcymuYRMtN1unrIA&FDCkcc{x68_b@h0VvfK zX9WgOCOgO6g6l(Xpk%49wc@x{QranOFAZF{R%5^wG8Qwm@SyteuJGnChrqvP%%P zPbTVsqEoK?bv)(aLu$s7urGSh9#n(xW1=U*nvtQV+#>7u8t^jubrF6zn6w>iPY-PD zN(IZ5s9VtgaIJr-vTkqB+tzHqE`gdM_A zD03fFCWW?z*}hno-3+7AR9E*}?LY~8pAF8wjDSU0G9w4dI5l{m9jhZ7<_=F41bAW; zHBzSsu(+~?IFS722+#BObR3NiqdN*N`wP#VEi_$Cus%tGl3-AogBb3<07G_D?9_$e zhh@UI)Pr1fZvTVG&dYZ+TUH(FQ)2I zMfXYsYkJ>_!9yY!Gj z7PH!m`F}EUbtOU>0;VNH` zq?Jl@*L^eJ1N8l>{qwJ7BaN@lRH9Knm_A1Dw%~(!9C|+o4fJnua z1J$m@6I9fBvTI+SxbxuwYGud2D;;@)|iUqL=2QvkLIS*FNH)shk1 zV{Ziaw#CnN42Sj;l(04X<>pD^kN$3>TrTp{M&DD8)`#zrZ%U|xBmvl%3c$z1-d+uGPPcYfhURI04u5OHWC^Z_Ab;9XQIS*} zaQ~ZadhQdlHZ^pwAmtIh)QwSS9b7xtBYJWX?4ofxN)0HW@n~EiD00v`Z~+wAj~9Oc zX_z4}^0RXo>K~B)-@IUl`EPSR&K#@aq%4+g*X%L4*LtJv?NY9mTo4B4StA1`62t+# z=|)g8lYboAxu=XwW3uF=@%Pu<9L{b3dHl*MotHtP=gogieRn>xE%#@+*8-t?@I$ZB zqG;^EW|^0y{LK#ecfG4-{9?Xz%k@RE-&p*dznCmf6nAQfQCc)sx_Erg2H$C#sTz^h zsbLn9u=7B|k%fmIR|_-9!K7QqO`JazW^}!2j?DUGy!VNo{1hi*eXnM^sLiL44s$H1 zNk!lAO*hYUxXFX9eXO#3j=$x_@1A?a*8Gz-YyL($jU%9@1A>C=S97o*1<08Ka;9gq z`IGCVp}ThL!X*`kK$SR7GRlu~9KUCA;D-cjKtz4Y=G@hJAb!?692hzD*dgKujS{GW2@sxqN#s~tFRTBlxJB0R8b z+!?m;qRF)3JsrCoFM&_82nAk0Obv4 z)&Pg^)zqW}7u@||T5*)_B!84PLrH5$#bY{6Le9VQdEPd+T~YwIqB(d?SxQ$v!ks7) zWl2c*6v|5yIR(*x*=o`8L6Hk62#uuP$Bn_GDHN0+q41~QgzXs@RDty6${)@GC6c6) zwZOiE=sx3;IuAy`KfRIN!l&Rn%FA{Q*GH>;Hbc;N(5B~tAN%|3wPIUA5|O>Vp!6qK zZ5S)gQSrPtSMK$2=~O;Q2xr4l6-MmYeoH40xgHq#^NP}ylRGf-z3(EeiHdBpF>84L zo?Ey72Da^^8)5lVA$MWV=U^LgI+Ynd-{2P4|NfEb5*fqmNCXC>DLEp~JV z8CAmeA6a-x;*VnD6{Y{v*tteEb!K508KhFRgKeiOjzSkzYY|Zp5D-#pC8L5-uu=$# zx|E=VfC>T01rV*a4iH-u5F}AEA!31)OSnXmST7(DNRg080))yXz%d|(ByyA4!ArF> zYt8)4kF&mf$#?cX?|$C*;B5e)c?Ez~C7!506IrwIE@|(xPqX$96nQxxABv;a`mGCc zahSZI+6YsahhGwv8I9Wt>6Z_8Mit!!EIXUOI!R3=MgR7SN}96-kp}e(A91U-}s1hW9Dtay^#L96PTrH z63vpa))r?@eaK0^Lp!K6eO{{`Pdc(&7J7&7kJ~QFi}7VCkG& z>sm9j_wQ~0dduEFK|OFOlUy*X7`*nLCNH}WjX3ROZ%bYAi}a-ZV~?{!TSR}^wPjh* zubgsChSVoMoTpc%P%}Z7GcD$IssUov;~)_R=APE9UodiO<`k|U3B;gcPC}GV;s&}8 z5#zGbHtBIak%$=!k7l(rM~dDX?G$coz+!ni+WjgdkHP6tdEuQrlB>W=Cey^P?9J}9 zSCXqWqU8wgp>X#*9D|9V>JIU9)sCw+thafuEG#8iGc5D7*mAqG2K^@~5q;6Ufb;o@ zAI`78F^2t*; z#s()I-3w9F(BwRFZnZ5zTTc&R)u=r!E3*4aioQ)cyg6#2`D2>AqL&1ONo0F~_3paf z5<&tGql-hUd20@pbBlI@AuxmQpFnG?kRithLs;H?k)FwyX^4`kF0#)p^c95p%}5EG zieysdg{gZ+rYbjFkfMcI41>o+(&53Gl$~p{R=!QMRb_J%?jET>8<_3TG*Cd;o9)(~ zT3?HInXm8}MTNj#PvsiLVK4{Gi%~J?HZ?1@KC#b@VsnXLIZy^N9CG8yYGQamxxRE% zFjgMD7J>J)E$E$=Dg`o8oTNmEA7lkWDM?ebm$sMOkEU8`$W8OU@3SwPl#%i}xB6<< zFc@yb)H;Q7vtX6n$5jm8l4c!qDFEOlGc;*Vr+xErX@gNhu^r&A9-$7dXaUUWMcfB5 z%jD@oN-<$(@fDo_;_2V&d%(34j(0!J34foj9ZSuQysm|+r7iH)XTocJ8|T!0^3hQe zjIX?%+bEXtszlc~m<|eGcy0sV=wtvki|H)Q?;6-X2yG6e(QM4jlIUZ+(`rndA`%K+ z8obp0{HA=j+M&_D(aQc1XsTooTEjE~C4@vmK+h0$-$;kXO=gh2@Psj0QsvY`Dh1S<^${5Q9XjF#PS3~gSc%AS%WcWw+)Hm5;z2&rnp znN$yZlh#A9IP5|~8rMr9yIy(H&w4lODh_*d01u+gP>K4?(m6sptKs5iwDmDRtZ7TQ zU~##*k|F7NOrXJYIy}=tevRCABD@9_~1O| zxJyK|%An->q3PxKdmFV)gkY@-U!C4{Tr$D0#7m(w;ufPQn6F)jGix8*a6%FqN2~@& zF^wKSuRJPGg|h;ITVLP;CTU33&!h+hqUsD4WQ=$n4q?-qQFeyPmyc!u@1LLgOy_@DSkh-jB?+fWD*3~kQ*&cN z15Lw;S(WS^=;?nbYqOC8e;iwr)HYi_UV|Qg407IhwBVyxV}LjM%WH-1nAhp#zgw2u z+5N{7I&Omx&Edcbi>d0D1TjpY@_zpjZR@r9`EPd@w=y9;uKVfr#p2%cTxKGqo`+%= z)1@z?6x~0ccjVxkVbsY_4}S99(ek=jtJXnaimuoMfC%Mb*}2fYji4D{f}beOQV-g7 zhfY&6YQsA0i_88xyo1H+QF@})+m^A5;JDqd8X^U~bGX7!o8{*~{p+4+(WE4+KDNkS zWZJimYw&6?82Zejp*R5TTKQX+xV2k1^!ra0J4%szYIeJM8bNX|@WuF~FLeTySm9f^w9#Y$E4frh@wP1^V^L#%PoH-v%ZGj@M6cg~ji=05rY6a+Y%0tutG0Z;GiW^>o9 z-V(ERe(Nf~pO_v23$O7pmztT@r2dwN8BC8GHvl$KJV+(h?sfU6Vlysh@+QA}d5p^? zO0BU_imnso?=Q1zH;B0?0-dSyoNvBM|JgCEA{aGh*zYguuw|>GmfEtpKM{|~l^qks z%L5U0he*(bH{C0YM~6A1o}TG$D^BF<#&h&I@A!mZACa47go;u}k?{G(0z*6fK;xsF zJOIoTlQ#1Dh#LdI@T;`wrrUqwg{SUgxlrslE zWMS>#2AK^ZR`blCiIC&)v=yQ9?Z@!a#w%TS2dgxsTWrx%U}F2Q%W|$QMxGQDpCxO&nXK5%;Krv2p_f&(n~aG`I;ooV^vzZ>F1gYl|sz z!d29*uT0Za$lHO6;~2lY=@N&FLN}hM{yqkRCSqK9=?l%30(%|TI2Ms8h>fEWCusYw z!(^xWNz;w7?dwP-@<`|w?6*Dx!xit!KRRm%>W!gY?96q85Xv@j(9H)7Po0Uwx4hx< zd1YgzT1QNfw8Gsh9X??0asFay>wI&0>ZwD#q9S^EtSy8gEV(2L7RY Date: Wed, 26 Mar 2025 09:36:23 -0400 Subject: [PATCH 08/41] Create opik-observability.mdx --- docs/how-to/opik-observability.mdx | 116 +++++++++++++++++++++++++++++ 1 file changed, 116 insertions(+) create mode 100644 docs/how-to/opik-observability.mdx diff --git a/docs/how-to/opik-observability.mdx b/docs/how-to/opik-observability.mdx new file mode 100644 index 000000000..49e0c6495 --- /dev/null +++ b/docs/how-to/opik-observability.mdx @@ -0,0 +1,116 @@ +--- +title: Opik Integration +description: Learn how to use Comet Opik to debug, evaluate, and monitor your CrewAI applications with comprehensive tracing, automated evaluations, and production-ready dashboards. +icon: insights +--- + +# Opik Overview + +With [Comet Opik](https://www.comet.com/docs/opik/), debug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards. + +Opik provides comprehensive support for every stage of your CrewAI application development: + +- **Log Traces and Spans**: Automatically track LLM calls and application logic to debug and analyze development and production systems. Manually or programmatically annotate, view, and compare responses across projects. +- **Evaluate Your LLM Application's Performance**: Evaluate against a custom test set and run built-in evaluation metrics or define your own metrics in the SDK or UI. +- **Test Within Your CI/CD Pipeline**: Establish reliable performance baselines with Opik's LLM unit tests, built on PyTest. Run online evaluations for continuous monitoring in production. +- **Monitor & Analyze Production Data**: Understand your models' performance on unseen data in production and generate datasets for new dev iterations. + +## Setup +Comet provides a hosted version of the Opik platform, or you can run the platform locally. + +To use the hosted version, simply [create a Comet account](https://www.comet.com/signup?utm_medium=github&utm_source=crewai_docs) and grab you API Key. + +To run the Opik platform locally, see our [installation guide](https://www.comet.com/docs/opik/self-host/overview/) for more information. + +For this guide we will use CrewAI’s quickstart example. + + + + ```shell + %pip install crewai crewai-tools opik --upgrade + ``` + + + ```python + import opik + opik.configure(use_local=False) + ``` + + First, we set up our API keys for our LLM-provider as environment variables: + ```python + import os + import getpass + + if "OPENAI_API_KEY" not in os.environ: + os.environ["OPENAI_API_KEY"] = getpass.getpass("Enter your OpenAI API key: ") + ``` + + + The first step is to create our project. We will use an example from CrewAI’s documentation: + ```python + from crewai import Agent, Crew, Task, Process + + +class YourCrewName: + def agent_one(self) -> Agent: + return Agent( + role="Data Analyst", + goal="Analyze data trends in the market", + backstory="An experienced data analyst with a background in economics", + verbose=True, + ) + + def agent_two(self) -> Agent: + return Agent( + role="Market Researcher", + goal="Gather information on market dynamics", + backstory="A diligent researcher with a keen eye for detail", + verbose=True, + ) + + def task_one(self) -> Task: + return Task( + name="Collect Data Task", + description="Collect recent market data and identify trends.", + expected_output="A report summarizing key trends in the market.", + agent=self.agent_one(), + ) + + def task_two(self) -> Task: + return Task( + name="Market Research Task", + description="Research factors affecting market dynamics.", + expected_output="An analysis of factors influencing the market.", + agent=self.agent_two(), + ) + + def crew(self) -> Crew: + return Crew( + agents=[self.agent_one(), self.agent_two()], + tasks=[self.task_one(), self.task_two()], + process=Process.sequential, + verbose=True, + ) + + ``` + Now we can import Opik’s tracker and run our crew: + ```python + from opik.integrations.crewai import track_crewai + + track_crewai(project_name="crewai-integration-demo") + + my_crew = YourCrewName().crew() + result = my_crew.kickoff() + + print(result) + ``` + After running your CrewAI application, visit the Opik app to view: + - LLM traces, spans, and their metadata + - Agent interactions and task execution flow + - Performance metrics like latency and token usage + - Evaluation metrics (built-in or custom) + + + Opik agent monitoring example with CrewAI + +<\Steps> From 7def3a8acc4d88f8414f05d71c214086c8f098dd Mon Sep 17 00:00:00 2001 From: Abby Morgan <86856445+anmorgan24@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:42:17 -0400 Subject: [PATCH 09/41] Update opik-observability.mdx Add resources --- docs/how-to/opik-observability.mdx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/how-to/opik-observability.mdx b/docs/how-to/opik-observability.mdx index 49e0c6495..c1f8c9226 100644 --- a/docs/how-to/opik-observability.mdx +++ b/docs/how-to/opik-observability.mdx @@ -114,3 +114,10 @@ class YourCrewName: Opik agent monitoring example with CrewAI <\Steps> + +## Resources + +- [🦉 Opik Documentation](https://www.comet.com/docs/opik/) +- [👉 Opik + CrewAI Colab](https://colab.research.google.com/github/comet-ml/opik/blob/main/apps/opik-documentation/documentation/docs/cookbook/crewai.ipynb) +- [🐦 X](https://x.com/cometml) +- [💬 Slack](https://slack.comet.com/) From 66124d9afb586db76cde6b5080647b1f32daaa4e Mon Sep 17 00:00:00 2001 From: Abby Morgan <86856445+anmorgan24@users.noreply.github.com> Date: Wed, 26 Mar 2025 09:57:32 -0400 Subject: [PATCH 10/41] Update opik-observability.mdx --- docs/how-to/opik-observability.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/opik-observability.mdx b/docs/how-to/opik-observability.mdx index c1f8c9226..b434411da 100644 --- a/docs/how-to/opik-observability.mdx +++ b/docs/how-to/opik-observability.mdx @@ -18,7 +18,7 @@ Opik provides comprehensive support for every stage of your CrewAI application d ## Setup Comet provides a hosted version of the Opik platform, or you can run the platform locally. -To use the hosted version, simply [create a Comet account](https://www.comet.com/signup?utm_medium=github&utm_source=crewai_docs) and grab you API Key. +To use the hosted version, simply [create a free Comet account](https://www.comet.com/signup?utm_medium=github&utm_source=crewai_docs) and grab you API Key. To run the Opik platform locally, see our [installation guide](https://www.comet.com/docs/opik/self-host/overview/) for more information. From 8df8255f1806ea462bd9c2b154f40f568afcadc5 Mon Sep 17 00:00:00 2001 From: Abby Morgan <86856445+anmorgan24@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:04:53 -0400 Subject: [PATCH 11/41] Update opik-observability.mdx Fix typo --- docs/how-to/opik-observability.mdx | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/how-to/opik-observability.mdx b/docs/how-to/opik-observability.mdx index b434411da..0e2822f87 100644 --- a/docs/how-to/opik-observability.mdx +++ b/docs/how-to/opik-observability.mdx @@ -113,6 +113,7 @@ class YourCrewName: Opik agent monitoring example with CrewAI + <\Steps> ## Resources From 22c8e5f43354e4f0c58f4f50edfbe1affdc9caf9 Mon Sep 17 00:00:00 2001 From: Abby Morgan <86856445+anmorgan24@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:06:36 -0400 Subject: [PATCH 12/41] Update opik-observability.mdx Fix typo --- docs/how-to/opik-observability.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/opik-observability.mdx b/docs/how-to/opik-observability.mdx index 0e2822f87..4fb5d2752 100644 --- a/docs/how-to/opik-observability.mdx +++ b/docs/how-to/opik-observability.mdx @@ -114,7 +114,7 @@ class YourCrewName: Opik agent monitoring example with CrewAI -<\Steps> + ## Resources From a25a27c3d35e03698217571aab14372c6b880c9b Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Wed, 26 Mar 2025 11:35:12 -0300 Subject: [PATCH 13/41] Add exclude option to `to_serializable()` (#2479) --- src/crewai/flow/state_utils.py | 28 ++++++++++++++++++++++++---- tests/flow/test_state_utils.py | 22 +++++++++++++++++++++- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/crewai/flow/state_utils.py b/src/crewai/flow/state_utils.py index eaf0f21ce..533bc5e00 100644 --- a/src/crewai/flow/state_utils.py +++ b/src/crewai/flow/state_utils.py @@ -1,4 +1,5 @@ import json +import uuid from datetime import date, datetime from typing import Any, Dict, List, Union @@ -32,7 +33,7 @@ def export_state(flow: Flow) -> dict[str, Serializable]: def to_serializable( - obj: Any, max_depth: int = 5, _current_depth: int = 0 + obj: Any, exclude: set[str] | None = None, max_depth: int = 5, _current_depth: int = 0 ) -> Serializable: """Converts a Python object into a JSON-compatible representation. @@ -42,6 +43,7 @@ def to_serializable( Args: obj (Any): Object to transform. + exclude (set[str], optional): Set of keys to exclude from the result. max_depth (int, optional): Maximum recursion depth. Defaults to 5. Returns: @@ -50,21 +52,39 @@ def to_serializable( if _current_depth >= max_depth: return repr(obj) + if exclude is None: + exclude = set() + if isinstance(obj, (str, int, float, bool, type(None))): return obj + elif isinstance(obj, uuid.UUID): + return str(obj) elif isinstance(obj, (date, datetime)): return obj.isoformat() elif isinstance(obj, (list, tuple, set)): - return [to_serializable(item, max_depth, _current_depth + 1) for item in obj] + return [ + to_serializable( + item, max_depth=max_depth, _current_depth=_current_depth + 1 + ) + for item in obj + ] elif isinstance(obj, dict): return { _to_serializable_key(key): to_serializable( - value, max_depth, _current_depth + 1 + obj=value, + exclude=exclude, + max_depth=max_depth, + _current_depth=_current_depth + 1, ) for key, value in obj.items() + if key not in exclude } elif isinstance(obj, BaseModel): - return to_serializable(obj.model_dump(), max_depth, _current_depth + 1) + return to_serializable( + obj=obj.model_dump(exclude=exclude), + max_depth=max_depth, + _current_depth=_current_depth + 1, + ) else: return repr(obj) diff --git a/tests/flow/test_state_utils.py b/tests/flow/test_state_utils.py index 1b135f36b..48564f297 100644 --- a/tests/flow/test_state_utils.py +++ b/tests/flow/test_state_utils.py @@ -6,7 +6,7 @@ import pytest from pydantic import BaseModel from crewai.flow import Flow -from crewai.flow.state_utils import export_state, to_string +from crewai.flow.state_utils import export_state, to_serializable, to_string class Address(BaseModel): @@ -148,3 +148,23 @@ def test_depth_limit(mock_flow): } } } + + +def test_exclude_keys(): + result = to_serializable({"key1": "value1", "key2": "value2"}, exclude={"key1"}) + assert result == {"key2": "value2"} + + model = Person( + name="John Doe", + age=30, + address=Address(street="123 Main St", city="Tech City", country="Pythonia"), + birthday=date(1994, 1, 1), + skills=["Python", "Testing"], + ) + result = to_serializable(model, exclude={"address"}) + assert result == { + "name": "John Doe", + "age": 30, + "birthday": "1994-01-01", + "skills": ["Python", "Testing"], + } From ac848f9ff4a148e25fb8fd8e1abff9d0e1a1a9a3 Mon Sep 17 00:00:00 2001 From: Abby Morgan <86856445+anmorgan24@users.noreply.github.com> Date: Wed, 26 Mar 2025 10:46:59 -0400 Subject: [PATCH 14/41] Update opik-observability.mdx Changed icon to meteor as per tony's request --- docs/how-to/opik-observability.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/how-to/opik-observability.mdx b/docs/how-to/opik-observability.mdx index 4fb5d2752..a1d128b8f 100644 --- a/docs/how-to/opik-observability.mdx +++ b/docs/how-to/opik-observability.mdx @@ -1,7 +1,7 @@ --- title: Opik Integration description: Learn how to use Comet Opik to debug, evaluate, and monitor your CrewAI applications with comprehensive tracing, automated evaluations, and production-ready dashboards. -icon: insights +icon: meteor --- # Opik Overview From 12a815e5db087215dfe44a5a87505ebeef02f47e Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 16:48:40 +0000 Subject: [PATCH 15/41] Fix #2351: Sanitize collection names to meet ChromaDB requirements Co-Authored-By: Joe Moura --- src/crewai/agent.py | 4 +- src/crewai/utilities/__init__.py | 2 + src/crewai/utilities/string_utils.py | 45 +++ tests/utilities/test_string_utils.py | 46 ++- uv.lock | 415 +++++++++++++-------------- 5 files changed, 286 insertions(+), 226 deletions(-) diff --git a/src/crewai/agent.py b/src/crewai/agent.py index 1680f4e8e..b92a83d14 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -142,8 +142,8 @@ class Agent(BaseAgent): self.embedder = crew_embedder if self.knowledge_sources: - full_pattern = re.compile(r"[^a-zA-Z0-9\-_\r\n]|(\.\.)") - knowledge_agent_name = f"{re.sub(full_pattern, '_', self.role)}" + from crewai.utilities import sanitize_collection_name + knowledge_agent_name = sanitize_collection_name(self.role) if isinstance(self.knowledge_sources, list) and all( isinstance(k, BaseKnowledgeSource) for k in self.knowledge_sources ): diff --git a/src/crewai/utilities/__init__.py b/src/crewai/utilities/__init__.py index dd6d9fa44..f2badd2d4 100644 --- a/src/crewai/utilities/__init__.py +++ b/src/crewai/utilities/__init__.py @@ -7,6 +7,7 @@ from .parser import YamlParser from .printer import Printer from .prompts import Prompts from .rpm_controller import RPMController +from .string_utils import sanitize_collection_name from .exceptions.context_window_exceeding_exception import ( LLMContextLengthExceededException, ) @@ -25,4 +26,5 @@ __all__ = [ "YamlParser", "LLMContextLengthExceededException", "EmbeddingConfigurator", + "sanitize_collection_name", ] diff --git a/src/crewai/utilities/string_utils.py b/src/crewai/utilities/string_utils.py index 9a1857781..6da07b20d 100644 --- a/src/crewai/utilities/string_utils.py +++ b/src/crewai/utilities/string_utils.py @@ -80,3 +80,48 @@ def interpolate_only( result = result.replace(placeholder, value) return result + + +from typing import Optional + + +def sanitize_collection_name(name: Optional[str]) -> str: + """ + Sanitize a collection name to meet ChromaDB requirements: + 1. 3-63 characters long + 2. Starts and ends with alphanumeric character + 3. Contains only alphanumeric characters, underscores, or hyphens + 4. No consecutive periods + 5. Not a valid IPv4 address + + Args: + name: The original collection name to sanitize + + Returns: + A sanitized collection name that meets ChromaDB requirements + """ + if not name: + return "default_collection" + + # Replace spaces and invalid characters with underscores + sanitized = re.sub(r"[^a-zA-Z0-9_-]", "_", name) + + # Ensure it starts with alphanumeric + if not sanitized[0].isalnum(): + sanitized = "a" + sanitized + + # Ensure it ends with alphanumeric + if not sanitized[-1].isalnum(): + sanitized = sanitized[:-1] + "z" + + # Ensure length is between 3-63 characters + if len(sanitized) < 3: + # Add padding with alphanumeric character at the end + sanitized = sanitized + "x" * (3 - len(sanitized)) + if len(sanitized) > 63: + sanitized = sanitized[:63] + # Ensure it still ends with alphanumeric after truncation + if not sanitized[-1].isalnum(): + sanitized = sanitized[:-1] + "z" + + return sanitized diff --git a/tests/utilities/test_string_utils.py b/tests/utilities/test_string_utils.py index 441aae8c0..04a0dcb56 100644 --- a/tests/utilities/test_string_utils.py +++ b/tests/utilities/test_string_utils.py @@ -1,8 +1,9 @@ +import unittest from typing import Any, Dict, List, Union import pytest -from crewai.utilities.string_utils import interpolate_only +from crewai.utilities.string_utils import interpolate_only, sanitize_collection_name class TestInterpolateOnly: @@ -185,3 +186,46 @@ class TestInterpolateOnly: interpolate_only(template, inputs) assert "inputs dictionary cannot be empty" in str(excinfo.value).lower() + + +class TestStringUtils(unittest.TestCase): + def test_sanitize_collection_name_long_name(self): + """Test sanitizing a very long collection name.""" + long_name = "This is an extremely long role name that will definitely exceed the ChromaDB collection name limit of 63 characters and cause an error when used as a collection name" + sanitized = sanitize_collection_name(long_name) + self.assertLessEqual(len(sanitized), 63) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) + + def test_sanitize_collection_name_special_chars(self): + """Test sanitizing a name with special characters.""" + special_chars = "Agent@123!#$%^&*()" + sanitized = sanitize_collection_name(special_chars) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) + + def test_sanitize_collection_name_short_name(self): + """Test sanitizing a very short name.""" + short_name = "A" + sanitized = sanitize_collection_name(short_name) + self.assertGreaterEqual(len(sanitized), 3) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + + def test_sanitize_collection_name_bad_ends(self): + """Test sanitizing a name with non-alphanumeric start/end.""" + bad_ends = "_Agent_" + sanitized = sanitize_collection_name(bad_ends) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + + def test_sanitize_collection_name_none(self): + """Test sanitizing a None value.""" + sanitized = sanitize_collection_name(None) + self.assertEqual(sanitized, "default_collection") + + +if __name__ == "__main__": + unittest.main() diff --git a/uv.lock b/uv.lock index 3a3f30bab..2bbe20efd 100644 --- a/uv.lock +++ b/uv.lock @@ -1,42 +1,18 @@ version = 1 requires-python = ">=3.10, <3.13" resolution-markers = [ - "python_full_version < '3.11' and platform_system == 'Darwin' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'darwin'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform == 'darwin') or (python_full_version < '3.11' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'darwin')", - "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_system == 'Darwin' and sys_platform == 'linux'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'linux'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_system == 'Darwin' and sys_platform != 'darwin') or (python_full_version < '3.11' and platform_system == 'Darwin' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version < '3.11' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform != 'darwin') or (python_full_version < '3.11' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and platform_system == 'Darwin' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'darwin'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform == 'darwin') or (python_full_version == '3.11.*' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'darwin')", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_system == 'Darwin' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'linux'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_system == 'Darwin' and sys_platform != 'darwin') or (python_full_version == '3.11.*' and platform_system == 'Darwin' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform != 'darwin') or (python_full_version == '3.11.*' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_system == 'Darwin' and sys_platform == 'darwin'", - "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'darwin'", - "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform == 'darwin') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'darwin')", - "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Darwin' and sys_platform == 'linux'", - "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'linux'", - "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'linux'", - "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_system == 'Darwin' and sys_platform != 'darwin') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_system == 'Darwin' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux'", - "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform != 'darwin') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version >= '3.12.4' and platform_system == 'Darwin' and sys_platform == 'darwin'", - "python_full_version >= '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'darwin'", - "(python_full_version >= '3.12.4' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform == 'darwin') or (python_full_version >= '3.12.4' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'darwin')", - "python_full_version >= '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Darwin' and sys_platform == 'linux'", - "python_full_version >= '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform == 'linux'", - "python_full_version >= '3.12.4' and platform_machine == 'aarch64' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform == 'linux'", - "(python_full_version >= '3.12.4' and platform_machine != 'aarch64' and platform_system == 'Darwin' and sys_platform != 'darwin') or (python_full_version >= '3.12.4' and platform_system == 'Darwin' and sys_platform != 'darwin' and sys_platform != 'linux')", - "python_full_version >= '3.12.4' and platform_machine == 'aarch64' and platform_system == 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux'", - "(python_full_version >= '3.12.4' and platform_machine != 'aarch64' and platform_system != 'Darwin' and sys_platform != 'darwin') or (python_full_version >= '3.12.4' and platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and sys_platform == 'darwin'", + "python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12' and python_full_version < '3.12.4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12' and python_full_version < '3.12.4' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version >= '3.12.4' and sys_platform == 'darwin'", + "python_full_version >= '3.12.4' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.12.4' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.12.4' and sys_platform != 'darwin' and sys_platform != 'linux')", ] [[package]] @@ -66,7 +42,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.11.11" +version = "3.10.10" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -75,56 +51,55 @@ dependencies = [ { name = "attrs" }, { name = "frozenlist" }, { name = "multidict" }, - { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fe/ed/f26db39d29cd3cb2f5a3374304c713fe5ab5a0e4c8ee25a0c45cc6adf844/aiohttp-3.11.11.tar.gz", hash = "sha256:bb49c7f1e6ebf3821a42d81d494f538107610c3a705987f53068546b0e90303e", size = 7669618 } +sdist = { url = "https://files.pythonhosted.org/packages/17/7e/16e57e6cf20eb62481a2f9ce8674328407187950ccc602ad07c685279141/aiohttp-3.10.10.tar.gz", hash = "sha256:0631dd7c9f0822cc61c88586ca76d5b5ada26538097d0f1df510b082bad3411a", size = 7542993 } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/7d/ff2e314b8f9e0b1df833e2d4778eaf23eae6b8cc8f922495d110ddcbf9e1/aiohttp-3.11.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a60804bff28662cbcf340a4d61598891f12eea3a66af48ecfdc975ceec21e3c8", size = 708550 }, - { url = "https://files.pythonhosted.org/packages/09/b8/aeb4975d5bba233d6f246941f5957a5ad4e3def8b0855a72742e391925f2/aiohttp-3.11.11-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4b4fa1cb5f270fb3eab079536b764ad740bb749ce69a94d4ec30ceee1b5940d5", size = 468430 }, - { url = "https://files.pythonhosted.org/packages/9c/5b/5b620279b3df46e597008b09fa1e10027a39467387c2332657288e25811a/aiohttp-3.11.11-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:731468f555656767cda219ab42e033355fe48c85fbe3ba83a349631541715ba2", size = 455593 }, - { url = "https://files.pythonhosted.org/packages/d8/75/0cdf014b816867d86c0bc26f3d3e3f194198dbf33037890beed629cd4f8f/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb23d8bb86282b342481cad4370ea0853a39e4a32a0042bb52ca6bdde132df43", size = 1584635 }, - { url = "https://files.pythonhosted.org/packages/df/2f/95b8f4e4dfeb57c1d9ad9fa911ede35a0249d75aa339edd2c2270dc539da/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f047569d655f81cb70ea5be942ee5d4421b6219c3f05d131f64088c73bb0917f", size = 1632363 }, - { url = "https://files.pythonhosted.org/packages/39/cb/70cf69ea7c50f5b0021a84f4c59c3622b2b3b81695f48a2f0e42ef7eba6e/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd7659baae9ccf94ae5fe8bfaa2c7bc2e94d24611528395ce88d009107e00c6d", size = 1668315 }, - { url = "https://files.pythonhosted.org/packages/2f/cc/3a3fc7a290eabc59839a7e15289cd48f33dd9337d06e301064e1e7fb26c5/aiohttp-3.11.11-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af01e42ad87ae24932138f154105e88da13ce7d202a6de93fafdafb2883a00ef", size = 1589546 }, - { url = "https://files.pythonhosted.org/packages/15/b4/0f7b0ed41ac6000e283e7332f0f608d734b675a8509763ca78e93714cfb0/aiohttp-3.11.11-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5854be2f3e5a729800bac57a8d76af464e160f19676ab6aea74bde18ad19d438", size = 1544581 }, - { url = "https://files.pythonhosted.org/packages/58/b9/4d06470fd85c687b6b0e31935ef73dde6e31767c9576d617309a2206556f/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6526e5fb4e14f4bbf30411216780c9967c20c5a55f2f51d3abd6de68320cc2f3", size = 1529256 }, - { url = "https://files.pythonhosted.org/packages/61/a2/6958b1b880fc017fd35f5dfb2c26a9a50c755b75fd9ae001dc2236a4fb79/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:85992ee30a31835fc482468637b3e5bd085fa8fe9392ba0bdcbdc1ef5e9e3c55", size = 1536592 }, - { url = "https://files.pythonhosted.org/packages/0f/dd/b974012a9551fd654f5bb95a6dd3f03d6e6472a17e1a8216dd42e9638d6c/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:88a12ad8ccf325a8a5ed80e6d7c3bdc247d66175afedbe104ee2aaca72960d8e", size = 1607446 }, - { url = "https://files.pythonhosted.org/packages/e0/d3/6c98fd87e638e51f074a3f2061e81fcb92123bcaf1439ac1b4a896446e40/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0a6d3fbf2232e3a08c41eca81ae4f1dff3d8f1a30bae415ebe0af2d2458b8a33", size = 1628809 }, - { url = "https://files.pythonhosted.org/packages/a8/2e/86e6f85cbca02be042c268c3d93e7f35977a0e127de56e319bdd1569eaa8/aiohttp-3.11.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84a585799c58b795573c7fa9b84c455adf3e1d72f19a2bf498b54a95ae0d194c", size = 1564291 }, - { url = "https://files.pythonhosted.org/packages/0b/8d/1f4ef3503b767717f65e1f5178b0173ab03cba1a19997ebf7b052161189f/aiohttp-3.11.11-cp310-cp310-win32.whl", hash = "sha256:bfde76a8f430cf5c5584553adf9926534352251d379dcb266ad2b93c54a29745", size = 416601 }, - { url = "https://files.pythonhosted.org/packages/ad/86/81cb83691b5ace3d9aa148dc42bacc3450d749fc88c5ec1973573c1c1779/aiohttp-3.11.11-cp310-cp310-win_amd64.whl", hash = "sha256:0fd82b8e9c383af11d2b26f27a478640b6b83d669440c0a71481f7c865a51da9", size = 442007 }, - { url = "https://files.pythonhosted.org/packages/34/ae/e8806a9f054e15f1d18b04db75c23ec38ec954a10c0a68d3bd275d7e8be3/aiohttp-3.11.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:ba74ec819177af1ef7f59063c6d35a214a8fde6f987f7661f4f0eecc468a8f76", size = 708624 }, - { url = "https://files.pythonhosted.org/packages/c7/e0/313ef1a333fb4d58d0c55a6acb3cd772f5d7756604b455181049e222c020/aiohttp-3.11.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4af57160800b7a815f3fe0eba9b46bf28aafc195555f1824555fa2cfab6c1538", size = 468507 }, - { url = "https://files.pythonhosted.org/packages/a9/60/03455476bf1f467e5b4a32a465c450548b2ce724eec39d69f737191f936a/aiohttp-3.11.11-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffa336210cf9cd8ed117011085817d00abe4c08f99968deef0013ea283547204", size = 455571 }, - { url = "https://files.pythonhosted.org/packages/be/f9/469588603bd75bf02c8ffb8c8a0d4b217eed446b49d4a767684685aa33fd/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81b8fe282183e4a3c7a1b72f5ade1094ed1c6345a8f153506d114af5bf8accd9", size = 1685694 }, - { url = "https://files.pythonhosted.org/packages/88/b9/1b7fa43faf6c8616fa94c568dc1309ffee2b6b68b04ac268e5d64b738688/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af41686ccec6a0f2bdc66686dc0f403c41ac2089f80e2214a0f82d001052c03", size = 1743660 }, - { url = "https://files.pythonhosted.org/packages/2a/8b/0248d19dbb16b67222e75f6aecedd014656225733157e5afaf6a6a07e2e8/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70d1f9dde0e5dd9e292a6d4d00058737052b01f3532f69c0c65818dac26dc287", size = 1785421 }, - { url = "https://files.pythonhosted.org/packages/c4/11/f478e071815a46ca0a5ae974651ff0c7a35898c55063305a896e58aa1247/aiohttp-3.11.11-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:249cc6912405917344192b9f9ea5cd5b139d49e0d2f5c7f70bdfaf6b4dbf3a2e", size = 1675145 }, - { url = "https://files.pythonhosted.org/packages/26/5d/284d182fecbb5075ae10153ff7374f57314c93a8681666600e3a9e09c505/aiohttp-3.11.11-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0eb98d90b6690827dcc84c246811feeb4e1eea683c0eac6caed7549be9c84665", size = 1619804 }, - { url = "https://files.pythonhosted.org/packages/1b/78/980064c2ad685c64ce0e8aeeb7ef1e53f43c5b005edcd7d32e60809c4992/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ec82bf1fda6cecce7f7b915f9196601a1bd1a3079796b76d16ae4cce6d0ef89b", size = 1654007 }, - { url = "https://files.pythonhosted.org/packages/21/8d/9e658d63b1438ad42b96f94da227f2e2c1d5c6001c9e8ffcc0bfb22e9105/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:9fd46ce0845cfe28f108888b3ab17abff84ff695e01e73657eec3f96d72eef34", size = 1650022 }, - { url = "https://files.pythonhosted.org/packages/85/fd/a032bf7f2755c2df4f87f9effa34ccc1ef5cea465377dbaeef93bb56bbd6/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd176afcf8f5d2aed50c3647d4925d0db0579d96f75a31e77cbaf67d8a87742d", size = 1732899 }, - { url = "https://files.pythonhosted.org/packages/c5/0c/c2b85fde167dd440c7ba50af2aac20b5a5666392b174df54c00f888c5a75/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ec2aa89305006fba9ffb98970db6c8221541be7bee4c1d027421d6f6df7d1ce2", size = 1755142 }, - { url = "https://files.pythonhosted.org/packages/bc/78/91ae1a3b3b3bed8b893c5d69c07023e151b1c95d79544ad04cf68f596c2f/aiohttp-3.11.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:92cde43018a2e17d48bb09c79e4d4cb0e236de5063ce897a5e40ac7cb4878773", size = 1692736 }, - { url = "https://files.pythonhosted.org/packages/77/89/a7ef9c4b4cdb546fcc650ca7f7395aaffbd267f0e1f648a436bec33c9b95/aiohttp-3.11.11-cp311-cp311-win32.whl", hash = "sha256:aba807f9569455cba566882c8938f1a549f205ee43c27b126e5450dc9f83cc62", size = 416418 }, - { url = "https://files.pythonhosted.org/packages/fc/db/2192489a8a51b52e06627506f8ac8df69ee221de88ab9bdea77aa793aa6a/aiohttp-3.11.11-cp311-cp311-win_amd64.whl", hash = "sha256:ae545f31489548c87b0cced5755cfe5a5308d00407000e72c4fa30b19c3220ac", size = 442509 }, - { url = "https://files.pythonhosted.org/packages/69/cf/4bda538c502f9738d6b95ada11603c05ec260807246e15e869fc3ec5de97/aiohttp-3.11.11-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e595c591a48bbc295ebf47cb91aebf9bd32f3ff76749ecf282ea7f9f6bb73886", size = 704666 }, - { url = "https://files.pythonhosted.org/packages/46/7b/87fcef2cad2fad420ca77bef981e815df6904047d0a1bd6aeded1b0d1d66/aiohttp-3.11.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3ea1b59dc06396b0b424740a10a0a63974c725b1c64736ff788a3689d36c02d2", size = 464057 }, - { url = "https://files.pythonhosted.org/packages/5a/a6/789e1f17a1b6f4a38939fbc39d29e1d960d5f89f73d0629a939410171bc0/aiohttp-3.11.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8811f3f098a78ffa16e0ea36dffd577eb031aea797cbdba81be039a4169e242c", size = 455996 }, - { url = "https://files.pythonhosted.org/packages/b7/dd/485061fbfef33165ce7320db36e530cd7116ee1098e9c3774d15a732b3fd/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd7227b87a355ce1f4bf83bfae4399b1f5bb42e0259cb9405824bd03d2f4336a", size = 1682367 }, - { url = "https://files.pythonhosted.org/packages/e9/d7/9ec5b3ea9ae215c311d88b2093e8da17e67b8856673e4166c994e117ee3e/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d40f9da8cabbf295d3a9dae1295c69975b86d941bc20f0a087f0477fa0a66231", size = 1736989 }, - { url = "https://files.pythonhosted.org/packages/d6/fb/ea94927f7bfe1d86178c9d3e0a8c54f651a0a655214cce930b3c679b8f64/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ffb3dc385f6bb1568aa974fe65da84723210e5d9707e360e9ecb51f59406cd2e", size = 1793265 }, - { url = "https://files.pythonhosted.org/packages/40/7f/6de218084f9b653026bd7063cd8045123a7ba90c25176465f266976d8c82/aiohttp-3.11.11-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8f5f7515f3552d899c61202d99dcb17d6e3b0de777900405611cd747cecd1b8", size = 1691841 }, - { url = "https://files.pythonhosted.org/packages/77/e2/992f43d87831cbddb6b09c57ab55499332f60ad6fdbf438ff4419c2925fc/aiohttp-3.11.11-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3499c7ffbfd9c6a3d8d6a2b01c26639da7e43d47c7b4f788016226b1e711caa8", size = 1619317 }, - { url = "https://files.pythonhosted.org/packages/96/74/879b23cdd816db4133325a201287c95bef4ce669acde37f8f1b8669e1755/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8e2bf8029dbf0810c7bfbc3e594b51c4cc9101fbffb583a3923aea184724203c", size = 1641416 }, - { url = "https://files.pythonhosted.org/packages/30/98/b123f6b15d87c54e58fd7ae3558ff594f898d7f30a90899718f3215ad328/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b6212a60e5c482ef90f2d788835387070a88d52cf6241d3916733c9176d39eab", size = 1646514 }, - { url = "https://files.pythonhosted.org/packages/d7/38/257fda3dc99d6978ab943141d5165ec74fd4b4164baa15e9c66fa21da86b/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d119fafe7b634dbfa25a8c597718e69a930e4847f0b88e172744be24515140da", size = 1702095 }, - { url = "https://files.pythonhosted.org/packages/0c/f4/ddab089053f9fb96654df5505c0a69bde093214b3c3454f6bfdb1845f558/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:6fba278063559acc730abf49845d0e9a9e1ba74f85f0ee6efd5803f08b285853", size = 1734611 }, - { url = "https://files.pythonhosted.org/packages/c3/d6/f30b2bc520c38c8aa4657ed953186e535ae84abe55c08d0f70acd72ff577/aiohttp-3.11.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:92fc484e34b733704ad77210c7957679c5c3877bd1e6b6d74b185e9320cc716e", size = 1694576 }, - { url = "https://files.pythonhosted.org/packages/bc/97/b0a88c3f4c6d0020b34045ee6d954058abc870814f6e310c4c9b74254116/aiohttp-3.11.11-cp312-cp312-win32.whl", hash = "sha256:9f5b3c1ed63c8fa937a920b6c1bec78b74ee09593b3f5b979ab2ae5ef60d7600", size = 411363 }, - { url = "https://files.pythonhosted.org/packages/7f/23/cc36d9c398980acaeeb443100f0216f50a7cfe20c67a9fd0a2f1a5a846de/aiohttp-3.11.11-cp312-cp312-win_amd64.whl", hash = "sha256:1e69966ea6ef0c14ee53ef7a3d68b564cc408121ea56c0caa2dc918c1b2f553d", size = 437666 }, + { url = "https://files.pythonhosted.org/packages/3d/dd/3d40c0e67e79c5c42671e3e268742f1ff96c6573ca43823563d01abd9475/aiohttp-3.10.10-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:be7443669ae9c016b71f402e43208e13ddf00912f47f623ee5994e12fc7d4b3f", size = 586969 }, + { url = "https://files.pythonhosted.org/packages/75/64/8de41b5555e5b43ef6d4ed1261891d33fe45ecc6cb62875bfafb90b9ab93/aiohttp-3.10.10-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b06b7843929e41a94ea09eb1ce3927865387e3e23ebe108e0d0d09b08d25be9", size = 399367 }, + { url = "https://files.pythonhosted.org/packages/96/36/27bd62ea7ce43906d1443a73691823fc82ffb8fa03276b0e2f7e1037c286/aiohttp-3.10.10-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:333cf6cf8e65f6a1e06e9eb3e643a0c515bb850d470902274239fea02033e9a8", size = 390720 }, + { url = "https://files.pythonhosted.org/packages/e8/4d/d516b050d811ce0dd26325c383013c104ffa8b58bd361b82e52833f68e78/aiohttp-3.10.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:274cfa632350225ce3fdeb318c23b4a10ec25c0e2c880eff951a3842cf358ac1", size = 1228820 }, + { url = "https://files.pythonhosted.org/packages/53/94/964d9327a3e336d89aad52260836e4ec87fdfa1207176550fdf384eaffe7/aiohttp-3.10.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9e5e4a85bdb56d224f412d9c98ae4cbd032cc4f3161818f692cd81766eee65a", size = 1264616 }, + { url = "https://files.pythonhosted.org/packages/0c/20/70ce17764b685ca8f5bf4d568881b4e1f1f4ea5e8170f512fdb1a33859d2/aiohttp-3.10.10-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b606353da03edcc71130b52388d25f9a30a126e04caef1fd637e31683033abd", size = 1298402 }, + { url = "https://files.pythonhosted.org/packages/d1/d1/5248225ccc687f498d06c3bca5af2647a361c3687a85eb3aedcc247ee1aa/aiohttp-3.10.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab5a5a0c7a7991d90446a198689c0535be89bbd6b410a1f9a66688f0880ec026", size = 1222205 }, + { url = "https://files.pythonhosted.org/packages/f2/a3/9296b27cc5d4feadf970a14d0694902a49a985f3fae71b8322a5f77b0baa/aiohttp-3.10.10-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:578a4b875af3e0daaf1ac6fa983d93e0bbfec3ead753b6d6f33d467100cdc67b", size = 1193804 }, + { url = "https://files.pythonhosted.org/packages/d9/07/f3760160feb12ac51a6168a6da251a4a8f2a70733d49e6ceb9b3e6ee2f03/aiohttp-3.10.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8105fd8a890df77b76dd3054cddf01a879fc13e8af576805d667e0fa0224c35d", size = 1193544 }, + { url = "https://files.pythonhosted.org/packages/7e/4c/93a70f9a4ba1c30183a6dd68bfa79cddbf9a674f162f9c62e823a74a5515/aiohttp-3.10.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3bcd391d083f636c06a68715e69467963d1f9600f85ef556ea82e9ef25f043f7", size = 1193047 }, + { url = "https://files.pythonhosted.org/packages/ff/a3/36a1e23ff00c7a0cd696c5a28db05db25dc42bfc78c508bd78623ff62a4a/aiohttp-3.10.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fbc6264158392bad9df19537e872d476f7c57adf718944cc1e4495cbabf38e2a", size = 1247201 }, + { url = "https://files.pythonhosted.org/packages/55/ae/95399848557b98bb2c402d640b2276ce3a542b94dba202de5a5a1fe29abe/aiohttp-3.10.10-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:e48d5021a84d341bcaf95c8460b152cfbad770d28e5fe14a768988c461b821bc", size = 1264102 }, + { url = "https://files.pythonhosted.org/packages/38/f5/02e5c72c1b60d7cceb30b982679a26167e84ac029fd35a93dd4da52c50a3/aiohttp-3.10.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2609e9ab08474702cc67b7702dbb8a80e392c54613ebe80db7e8dbdb79837c68", size = 1215760 }, + { url = "https://files.pythonhosted.org/packages/30/17/1463840bad10d02d0439068f37ce5af0b383884b0d5838f46fb027e233bf/aiohttp-3.10.10-cp310-cp310-win32.whl", hash = "sha256:84afcdea18eda514c25bc68b9af2a2b1adea7c08899175a51fe7c4fb6d551257", size = 362678 }, + { url = "https://files.pythonhosted.org/packages/dd/01/a0ef707d93e867a43abbffee3a2cdf30559910750b9176b891628c7ad074/aiohttp-3.10.10-cp310-cp310-win_amd64.whl", hash = "sha256:9c72109213eb9d3874f7ac8c0c5fa90e072d678e117d9061c06e30c85b4cf0e6", size = 381097 }, + { url = "https://files.pythonhosted.org/packages/72/31/3c351d17596194e5a38ef169a4da76458952b2497b4b54645b9d483cbbb0/aiohttp-3.10.10-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c30a0eafc89d28e7f959281b58198a9fa5e99405f716c0289b7892ca345fe45f", size = 586501 }, + { url = "https://files.pythonhosted.org/packages/a4/a8/a559d09eb08478cdead6b7ce05b0c4a133ba27fcdfa91e05d2e62867300d/aiohttp-3.10.10-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:258c5dd01afc10015866114e210fb7365f0d02d9d059c3c3415382ab633fcbcb", size = 398993 }, + { url = "https://files.pythonhosted.org/packages/c5/47/7736d4174613feef61d25332c3bd1a4f8ff5591fbd7331988238a7299485/aiohttp-3.10.10-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:15ecd889a709b0080f02721255b3f80bb261c2293d3c748151274dfea93ac871", size = 390647 }, + { url = "https://files.pythonhosted.org/packages/27/21/e9ba192a04b7160f5a8952c98a1de7cf8072ad150fa3abd454ead1ab1d7f/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f3935f82f6f4a3820270842e90456ebad3af15810cf65932bd24da4463bc0a4c", size = 1306481 }, + { url = "https://files.pythonhosted.org/packages/cf/50/f364c01c8d0def1dc34747b2470969e216f5a37c7ece00fe558810f37013/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:413251f6fcf552a33c981c4709a6bba37b12710982fec8e558ae944bfb2abd38", size = 1344652 }, + { url = "https://files.pythonhosted.org/packages/1d/c2/74f608e984e9b585649e2e83883facad6fa3fc1d021de87b20cc67e8e5ae/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d1720b4f14c78a3089562b8875b53e36b51c97c51adc53325a69b79b4b48ebcb", size = 1378498 }, + { url = "https://files.pythonhosted.org/packages/9f/a7/05a48c7c0a7a80a5591b1203bf1b64ca2ed6a2050af918d09c05852dc42b/aiohttp-3.10.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:679abe5d3858b33c2cf74faec299fda60ea9de62916e8b67e625d65bf069a3b7", size = 1292718 }, + { url = "https://files.pythonhosted.org/packages/7d/78/a925655018747e9790350180330032e27d6e0d7ed30bde545fae42f8c49c/aiohttp-3.10.10-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:79019094f87c9fb44f8d769e41dbb664d6e8fcfd62f665ccce36762deaa0e911", size = 1251776 }, + { url = "https://files.pythonhosted.org/packages/47/9d/85c6b69f702351d1236594745a4fdc042fc43f494c247a98dac17e004026/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe2fb38c2ed905a2582948e2de560675e9dfbee94c6d5ccdb1301c6d0a5bf092", size = 1271716 }, + { url = "https://files.pythonhosted.org/packages/7f/a7/55fc805ff9b14af818903882ece08e2235b12b73b867b521b92994c52b14/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a3f00003de6eba42d6e94fabb4125600d6e484846dbf90ea8e48a800430cc142", size = 1266263 }, + { url = "https://files.pythonhosted.org/packages/1f/ec/d2be2ca7b063e4f91519d550dbc9c1cb43040174a322470deed90b3d3333/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1bbb122c557a16fafc10354b9d99ebf2f2808a660d78202f10ba9d50786384b9", size = 1321617 }, + { url = "https://files.pythonhosted.org/packages/c9/a3/b29f7920e1cd0a9a68a45dd3eb16140074d2efb1518d2e1f3e140357dc37/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:30ca7c3b94708a9d7ae76ff281b2f47d8eaf2579cd05971b5dc681db8caac6e1", size = 1339227 }, + { url = "https://files.pythonhosted.org/packages/8a/81/34b67235c47e232d807b4bbc42ba9b927c7ce9476872372fddcfd1e41b3d/aiohttp-3.10.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df9270660711670e68803107d55c2b5949c2e0f2e4896da176e1ecfc068b974a", size = 1299068 }, + { url = "https://files.pythonhosted.org/packages/04/1f/26a7fe11b6ad3184f214733428353c89ae9fe3e4f605a657f5245c5e720c/aiohttp-3.10.10-cp311-cp311-win32.whl", hash = "sha256:aafc8ee9b742ce75044ae9a4d3e60e3d918d15a4c2e08a6c3c3e38fa59b92d94", size = 362223 }, + { url = "https://files.pythonhosted.org/packages/10/91/85dcd93f64011434359ce2666bece981f08d31bc49df33261e625b28595d/aiohttp-3.10.10-cp311-cp311-win_amd64.whl", hash = "sha256:362f641f9071e5f3ee6f8e7d37d5ed0d95aae656adf4ef578313ee585b585959", size = 381576 }, + { url = "https://files.pythonhosted.org/packages/ae/99/4c5aefe5ad06a1baf206aed6598c7cdcbc7c044c46801cd0d1ecb758cae3/aiohttp-3.10.10-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9294bbb581f92770e6ed5c19559e1e99255e4ca604a22c5c6397b2f9dd3ee42c", size = 583536 }, + { url = "https://files.pythonhosted.org/packages/a9/36/8b3bc49b49cb6d2da40ee61ff15dbcc44fd345a3e6ab5bb20844df929821/aiohttp-3.10.10-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:a8fa23fe62c436ccf23ff930149c047f060c7126eae3ccea005f0483f27b2e28", size = 395693 }, + { url = "https://files.pythonhosted.org/packages/e1/77/0aa8660dcf11fa65d61712dbb458c4989de220a844bd69778dff25f2d50b/aiohttp-3.10.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5c6a5b8c7926ba5d8545c7dd22961a107526562da31a7a32fa2456baf040939f", size = 390898 }, + { url = "https://files.pythonhosted.org/packages/38/d2/b833d95deb48c75db85bf6646de0a697e7fb5d87bd27cbade4f9746b48b1/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007ec22fbc573e5eb2fb7dec4198ef8f6bf2fe4ce20020798b2eb5d0abda6138", size = 1312060 }, + { url = "https://files.pythonhosted.org/packages/aa/5f/29fd5113165a0893de8efedf9b4737e0ba92dfcd791415a528f947d10299/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9627cc1a10c8c409b5822a92d57a77f383b554463d1884008e051c32ab1b3742", size = 1350553 }, + { url = "https://files.pythonhosted.org/packages/ad/cc/f835f74b7d344428469200105236d44606cfa448be1e7c95ca52880d9bac/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:50edbcad60d8f0e3eccc68da67f37268b5144ecc34d59f27a02f9611c1d4eec7", size = 1392646 }, + { url = "https://files.pythonhosted.org/packages/bf/fe/1332409d845ca601893bbf2d76935e0b93d41686e5f333841c7d7a4a770d/aiohttp-3.10.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a45d85cf20b5e0d0aa5a8dca27cce8eddef3292bc29d72dcad1641f4ed50aa16", size = 1306310 }, + { url = "https://files.pythonhosted.org/packages/e4/a1/25a7633a5a513278a9892e333501e2e69c83e50be4b57a62285fb7a008c3/aiohttp-3.10.10-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0b00807e2605f16e1e198f33a53ce3c4523114059b0c09c337209ae55e3823a8", size = 1260255 }, + { url = "https://files.pythonhosted.org/packages/f2/39/30eafe89e0e2a06c25e4762844c8214c0c0cd0fd9ffc3471694a7986f421/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f2d4324a98062be0525d16f768a03e0bbb3b9fe301ceee99611dc9a7953124e6", size = 1271141 }, + { url = "https://files.pythonhosted.org/packages/5b/fc/33125df728b48391ef1fcb512dfb02072158cc10d041414fb79803463020/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:438cd072f75bb6612f2aca29f8bd7cdf6e35e8f160bc312e49fbecab77c99e3a", size = 1280244 }, + { url = "https://files.pythonhosted.org/packages/3b/61/e42bf2c2934b5caa4e2ec0b5e5fd86989adb022b5ee60c2572a9d77cf6fe/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:baa42524a82f75303f714108fea528ccacf0386af429b69fff141ffef1c534f9", size = 1316805 }, + { url = "https://files.pythonhosted.org/packages/18/32/f52a5e2ae9ad3bba10e026a63a7a23abfa37c7d97aeeb9004eaa98df3ce3/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a7d8d14fe962153fc681f6366bdec33d4356f98a3e3567782aac1b6e0e40109a", size = 1343930 }, + { url = "https://files.pythonhosted.org/packages/05/be/6a403b464dcab3631fe8e27b0f1d906d9e45c5e92aca97ee007e5a895560/aiohttp-3.10.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1277cd707c465cd09572a774559a3cc7c7a28802eb3a2a9472588f062097205", size = 1306186 }, + { url = "https://files.pythonhosted.org/packages/8e/fd/bb50fe781068a736a02bf5c7ad5f3ab53e39f1d1e63110da6d30f7605edc/aiohttp-3.10.10-cp312-cp312-win32.whl", hash = "sha256:59bb3c54aa420521dc4ce3cc2c3fe2ad82adf7b09403fa1f48ae45c0cbde6628", size = 359289 }, + { url = "https://files.pythonhosted.org/packages/70/9e/5add7e240f77ef67c275c82cc1d08afbca57b77593118c1f6e920ae8ad3f/aiohttp-3.10.10-cp312-cp312-win_amd64.whl", hash = "sha256:0e1b370d8007c4ae31ee6db7f9a2fe801a42b146cec80a86766e7ad5c4a259cf", size = 379313 }, ] [[package]] @@ -345,7 +320,7 @@ name = "build" version = "1.2.2.post1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "os_name == 'nt'" }, + { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, { name = "packaging" }, { name = "pyproject-hooks" }, @@ -580,7 +555,7 @@ name = "click" version = "8.1.8" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } wheels = [ @@ -768,7 +743,7 @@ dev = [ [[package]] name = "crewai-tools" -version = "0.37.0" +version = "0.38.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "chromadb" }, @@ -783,9 +758,9 @@ dependencies = [ { name = "pytube" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ef/a9/813ef7b721d11ac962c2a3cf4c98196d3ca8bca5bb0fa5e01da0af51ac23/crewai_tools-0.37.0.tar.gz", hash = "sha256:23c8428761809e30d164be32c2a02850c4648e4371e9934eb58842590bca9659", size = 722104 } +sdist = { url = "https://files.pythonhosted.org/packages/85/3f/d3b5697b4c6756cec65316c9ea9ccd9054f7b73670d1580befd3632ba031/crewai_tools-0.38.1.tar.gz", hash = "sha256:6abe75b3b339d53a9cf4e2d80124d863ff62a82b36753c30bec64318881876b2", size = 737620 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/b3/6bf9b066f628875c383689ab72d21968e1108ebece887491dbf051ee39c5/crewai_tools-0.37.0-py3-none-any.whl", hash = "sha256:df5c9efade5c1f4fcfdf6ac8af13c422be7127a3083a5cda75d8f314c652bb10", size = 548490 }, + { url = "https://files.pythonhosted.org/packages/2b/2b/a6c9007647ffbb6a3c204b3ef26806030d6b041e3e012d4cec43c21335d6/crewai_tools-0.38.1-py3-none-any.whl", hash = "sha256:d9d3a88060f1f30c8f4ea044f6dd564a50d0a22b8a018a6fcec202b36246b9d8", size = 561414 }, ] [[package]] @@ -1746,7 +1721,7 @@ wheels = [ [[package]] name = "httpx" -version = "0.27.0" +version = "0.27.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -1755,9 +1730,9 @@ dependencies = [ { name = "idna" }, { name = "sniffio" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/2d/3da5bdf4408b8b2800061c339f240c1802f2e82d55e50bd39c5a881f47f0/httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5", size = 126413 } +sdist = { url = "https://files.pythonhosted.org/packages/78/82/08f8c936781f67d9e6b9eeb8a0c8b4e406136ea4c3d1f89a5db71d42e0e6/httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2", size = 144189 } wheels = [ - { url = "https://files.pythonhosted.org/packages/41/7b/ddacf6dcebb42466abd03f368782142baa82e08fc0c1f8eaa05b4bae87d5/httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5", size = 75590 }, + { url = "https://files.pythonhosted.org/packages/56/95/9377bcb415797e44274b51d46e3249eba641711cf3348050f76ee7b15ffc/httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0", size = 76395 }, ] [package.optional-dependencies] @@ -2519,7 +2494,7 @@ version = "1.6.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "click" }, - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, { name = "ghp-import" }, { name = "jinja2" }, { name = "markdown" }, @@ -2700,7 +2675,7 @@ version = "2.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pygments" }, - { name = "pywin32", marker = "platform_system == 'Windows'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, { name = "tqdm" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3a/93/80ac75c20ce54c785648b4ed363c88f148bf22637e10c9863db4fbe73e74/mpire-2.10.2.tar.gz", hash = "sha256:f66a321e93fadff34585a4bfa05e95bd946cf714b442f51c529038eb45773d97", size = 271270 } @@ -2947,7 +2922,7 @@ name = "nvidia-cudnn-cu12" version = "9.1.0.70" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/9f/fd/713452cd72343f682b1c7b9321e23829f00b842ceaedcda96e742ea0b0b3/nvidia_cudnn_cu12-9.1.0.70-py3-none-manylinux2014_x86_64.whl", hash = "sha256:165764f44ef8c61fcdfdfdbe769d687e06374059fbb388b6c89ecb0e28793a6f", size = 664752741 }, @@ -2974,9 +2949,9 @@ name = "nvidia-cusolver-cu12" version = "11.4.5.107" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }, - { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }, - { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }, + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/bc/1d/8de1e5c67099015c834315e333911273a8c6aaba78923dd1d1e25fc5f217/nvidia_cusolver_cu12-11.4.5.107-py3-none-manylinux1_x86_64.whl", hash = "sha256:8a7ec542f0412294b15072fa7dab71d31334014a69f953004ea7a118206fe0dd", size = 124161928 }, @@ -2987,7 +2962,7 @@ name = "nvidia-cusparse-cu12" version = "12.1.0.106" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/65/5b/cfaeebf25cd9fdec14338ccb16f6b2c4c7fa9163aefcf057d86b9cc248bb/nvidia_cusparse_cu12-12.1.0.106-py3-none-manylinux1_x86_64.whl", hash = "sha256:f3b50f42cf363f86ab21f720998517a659a48131e8d538dc02f8768237bd884c", size = 195958278 }, @@ -3087,7 +3062,7 @@ wheels = [ [[package]] name = "openai" -version = "1.61.0" +version = "1.68.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -3099,9 +3074,9 @@ dependencies = [ { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/2a/b3fa8790be17d632f59d4f50257b909a3f669036e5195c1ae55737274620/openai-1.61.0.tar.gz", hash = "sha256:216f325a24ed8578e929b0f1b3fb2052165f3b04b0461818adaa51aa29c71f8a", size = 350174 } +sdist = { url = "https://files.pythonhosted.org/packages/3f/6b/6b002d5d38794645437ae3ddb42083059d556558493408d39a0fcea608bc/openai-1.68.2.tar.gz", hash = "sha256:b720f0a95a1dbe1429c0d9bb62096a0d98057bcda82516f6e8af10284bdd5b19", size = 413429 } wheels = [ - { url = "https://files.pythonhosted.org/packages/93/76/70c5ad6612b3e4c89fa520266bbf2430a89cae8bd87c1e2284698af5927e/openai-1.61.0-py3-none-any.whl", hash = "sha256:e8c512c0743accbdbe77f3429a1490d862f8352045de8dc81969301eb4a4f666", size = 460623 }, + { url = "https://files.pythonhosted.org/packages/fd/34/cebce15f64eb4a3d609a83ac3568d43005cc9a1cba9d7fde5590fd415423/openai-1.68.2-py3-none-any.whl", hash = "sha256:24484cb5c9a33b58576fdc5acf0e5f92603024a4e39d0b99793dfa1eb14c2b36", size = 606073 }, ] [[package]] @@ -3526,7 +3501,7 @@ name = "portalocker" version = "2.10.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pywin32", marker = "platform_system == 'Windows'" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ed/d3/c6c64067759e87af98cc668c1cc75171347d0f1577fab7ca3749134e3cd4/portalocker-2.10.1.tar.gz", hash = "sha256:ef1bf844e878ab08aee7e40184156e1151f228f103aa5c6bd0724cc330960f8f", size = 40891 } wheels = [ @@ -3833,77 +3808,71 @@ wheels = [ [[package]] name = "pydantic" -version = "2.10.4" +version = "2.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, { name = "pydantic-core" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/70/7e/fb60e6fee04d0ef8f15e4e01ff187a196fa976eb0f0ab524af4599e5754c/pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06", size = 762094 } +sdist = { url = "https://files.pythonhosted.org/packages/a9/b7/d9e3f12af310e1120c21603644a1cd86f59060e040ec5c3a80b8f05fae30/pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f", size = 769917 } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/26/3e1bbe954fde7ee22a6e7d31582c642aad9e84ffe4b5fb61e63b87cd326f/pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d", size = 431765 }, + { url = "https://files.pythonhosted.org/packages/df/e4/ba44652d562cbf0bf320e0f3810206149c8a4e99cdbf66da82e97ab53a15/pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12", size = 434928 }, ] [[package]] name = "pydantic-core" -version = "2.27.2" +version = "2.23.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } +sdist = { url = "https://files.pythonhosted.org/packages/e2/aa/6b6a9b9f8537b872f552ddd46dd3da230367754b6f707b8e1e963f515ea3/pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863", size = 402156 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, - { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, - { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, - { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, - { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, - { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, - { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, - { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, - { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, - { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, - { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, - { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, - { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, - { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, - { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, - { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, - { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, - { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, - { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, - { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, - { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, - { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, - { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, - { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, - { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, - { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, - { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, - { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, - { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, - { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, - { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, - { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, - { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, - { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, - { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, - { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, - { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, - { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, - { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, - { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, - { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, - { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, - { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, - { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, - { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, - { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, - { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, - { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, - { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, - { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, + { url = "https://files.pythonhosted.org/packages/5c/8b/d3ae387f66277bd8104096d6ec0a145f4baa2966ebb2cad746c0920c9526/pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b", size = 1867835 }, + { url = "https://files.pythonhosted.org/packages/46/76/f68272e4c3a7df8777798282c5e47d508274917f29992d84e1898f8908c7/pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166", size = 1776689 }, + { url = "https://files.pythonhosted.org/packages/cc/69/5f945b4416f42ea3f3bc9d2aaec66c76084a6ff4ff27555bf9415ab43189/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb", size = 1800748 }, + { url = "https://files.pythonhosted.org/packages/50/ab/891a7b0054bcc297fb02d44d05c50e68154e31788f2d9d41d0b72c89fdf7/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916", size = 1806469 }, + { url = "https://files.pythonhosted.org/packages/31/7c/6e3fa122075d78f277a8431c4c608f061881b76c2b7faca01d317ee39b5d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07", size = 2002246 }, + { url = "https://files.pythonhosted.org/packages/ad/6f/22d5692b7ab63fc4acbc74de6ff61d185804a83160adba5e6cc6068e1128/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232", size = 2659404 }, + { url = "https://files.pythonhosted.org/packages/11/ac/1e647dc1121c028b691028fa61a4e7477e6aeb5132628fde41dd34c1671f/pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2", size = 2053940 }, + { url = "https://files.pythonhosted.org/packages/91/75/984740c17f12c3ce18b5a2fcc4bdceb785cce7df1511a4ce89bca17c7e2d/pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f", size = 1921437 }, + { url = "https://files.pythonhosted.org/packages/a0/74/13c5f606b64d93f0721e7768cd3e8b2102164866c207b8cd6f90bb15d24f/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3", size = 1966129 }, + { url = "https://files.pythonhosted.org/packages/18/03/9c4aa5919457c7b57a016c1ab513b1a926ed9b2bb7915bf8e506bf65c34b/pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071", size = 2110908 }, + { url = "https://files.pythonhosted.org/packages/92/2c/053d33f029c5dc65e5cf44ff03ceeefb7cce908f8f3cca9265e7f9b540c8/pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119", size = 1735278 }, + { url = "https://files.pythonhosted.org/packages/de/81/7dfe464eca78d76d31dd661b04b5f2036ec72ea8848dd87ab7375e185c23/pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f", size = 1917453 }, + { url = "https://files.pythonhosted.org/packages/5d/30/890a583cd3f2be27ecf32b479d5d615710bb926d92da03e3f7838ff3e58b/pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8", size = 1865160 }, + { url = "https://files.pythonhosted.org/packages/1d/9a/b634442e1253bc6889c87afe8bb59447f106ee042140bd57680b3b113ec7/pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d", size = 1776777 }, + { url = "https://files.pythonhosted.org/packages/75/9a/7816295124a6b08c24c96f9ce73085032d8bcbaf7e5a781cd41aa910c891/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e", size = 1799244 }, + { url = "https://files.pythonhosted.org/packages/a9/8f/89c1405176903e567c5f99ec53387449e62f1121894aa9fc2c4fdc51a59b/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607", size = 1805307 }, + { url = "https://files.pythonhosted.org/packages/d5/a5/1a194447d0da1ef492e3470680c66048fef56fc1f1a25cafbea4bc1d1c48/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd", size = 2000663 }, + { url = "https://files.pythonhosted.org/packages/13/a5/1df8541651de4455e7d587cf556201b4f7997191e110bca3b589218745a5/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea", size = 2655941 }, + { url = "https://files.pythonhosted.org/packages/44/31/a3899b5ce02c4316865e390107f145089876dff7e1dfc770a231d836aed8/pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e", size = 2052105 }, + { url = "https://files.pythonhosted.org/packages/1b/aa/98e190f8745d5ec831f6d5449344c48c0627ac5fed4e5340a44b74878f8e/pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b", size = 1919967 }, + { url = "https://files.pythonhosted.org/packages/ae/35/b6e00b6abb2acfee3e8f85558c02a0822e9a8b2f2d812ea8b9079b118ba0/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0", size = 1964291 }, + { url = "https://files.pythonhosted.org/packages/13/46/7bee6d32b69191cd649bbbd2361af79c472d72cb29bb2024f0b6e350ba06/pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64", size = 2109666 }, + { url = "https://files.pythonhosted.org/packages/39/ef/7b34f1b122a81b68ed0a7d0e564da9ccdc9a2924c8d6c6b5b11fa3a56970/pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f", size = 1732940 }, + { url = "https://files.pythonhosted.org/packages/2f/76/37b7e76c645843ff46c1d73e046207311ef298d3f7b2f7d8f6ac60113071/pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3", size = 1916804 }, + { url = "https://files.pythonhosted.org/packages/74/7b/8e315f80666194b354966ec84b7d567da77ad927ed6323db4006cf915f3f/pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231", size = 1856459 }, + { url = "https://files.pythonhosted.org/packages/14/de/866bdce10ed808323d437612aca1ec9971b981e1c52e5e42ad9b8e17a6f6/pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee", size = 1770007 }, + { url = "https://files.pythonhosted.org/packages/dc/69/8edd5c3cd48bb833a3f7ef9b81d7666ccddd3c9a635225214e044b6e8281/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87", size = 1790245 }, + { url = "https://files.pythonhosted.org/packages/80/33/9c24334e3af796ce80d2274940aae38dd4e5676298b4398eff103a79e02d/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8", size = 1801260 }, + { url = "https://files.pythonhosted.org/packages/a5/6f/e9567fd90104b79b101ca9d120219644d3314962caa7948dd8b965e9f83e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327", size = 1996872 }, + { url = "https://files.pythonhosted.org/packages/2d/ad/b5f0fe9e6cfee915dd144edbd10b6e9c9c9c9d7a56b69256d124b8ac682e/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2", size = 2661617 }, + { url = "https://files.pythonhosted.org/packages/06/c8/7d4b708f8d05a5cbfda3243aad468052c6e99de7d0937c9146c24d9f12e9/pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36", size = 2071831 }, + { url = "https://files.pythonhosted.org/packages/89/4d/3079d00c47f22c9a9a8220db088b309ad6e600a73d7a69473e3a8e5e3ea3/pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126", size = 1917453 }, + { url = "https://files.pythonhosted.org/packages/e9/88/9df5b7ce880a4703fcc2d76c8c2d8eb9f861f79d0c56f4b8f5f2607ccec8/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e", size = 1968793 }, + { url = "https://files.pythonhosted.org/packages/e3/b9/41f7efe80f6ce2ed3ee3c2dcfe10ab7adc1172f778cc9659509a79518c43/pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24", size = 2116872 }, + { url = "https://files.pythonhosted.org/packages/63/08/b59b7a92e03dd25554b0436554bf23e7c29abae7cce4b1c459cd92746811/pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84", size = 1738535 }, + { url = "https://files.pythonhosted.org/packages/88/8d/479293e4d39ab409747926eec4329de5b7129beaedc3786eca070605d07f/pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9", size = 1917992 }, + { url = "https://files.pythonhosted.org/packages/13/a9/5d582eb3204464284611f636b55c0a7410d748ff338756323cb1ce721b96/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5", size = 1857135 }, + { url = "https://files.pythonhosted.org/packages/2c/57/faf36290933fe16717f97829eabfb1868182ac495f99cf0eda9f59687c9d/pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec", size = 1740583 }, + { url = "https://files.pythonhosted.org/packages/91/7c/d99e3513dc191c4fec363aef1bf4c8af9125d8fa53af7cb97e8babef4e40/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480", size = 1793637 }, + { url = "https://files.pythonhosted.org/packages/29/18/812222b6d18c2d13eebbb0f7cdc170a408d9ced65794fdb86147c77e1982/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068", size = 1941963 }, + { url = "https://files.pythonhosted.org/packages/0f/36/c1f3642ac3f05e6bb4aec3ffc399fa3f84895d259cf5f0ce3054b7735c29/pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801", size = 1915332 }, + { url = "https://files.pythonhosted.org/packages/f7/ca/9c0854829311fb446020ebb540ee22509731abad886d2859c855dd29b904/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728", size = 1957926 }, + { url = "https://files.pythonhosted.org/packages/c0/1c/7836b67c42d0cd4441fcd9fafbf6a027ad4b79b6559f80cf11f89fd83648/pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433", size = 2100342 }, + { url = "https://files.pythonhosted.org/packages/a9/f9/b6bcaf874f410564a78908739c80861a171788ef4d4f76f5009656672dfe/pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753", size = 1920344 }, ] [[package]] @@ -5039,19 +5008,19 @@ dependencies = [ { name = "fsspec" }, { name = "jinja2" }, { name = "networkx" }, - { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, - { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "sympy" }, - { name = "triton", marker = "platform_machine == 'x86_64' and platform_system == 'Linux'" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, { name = "typing-extensions" }, ] wheels = [ @@ -5098,7 +5067,7 @@ name = "tqdm" version = "4.66.5" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "colorama", marker = "platform_system == 'Windows'" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/83/6ba9844a41128c62e810fddddd72473201f3eacde02046066142a2d96cc5/tqdm-4.66.5.tar.gz", hash = "sha256:e1020aef2e5096702d8a025ac7d16b1577279c9d63f8375b63083e9a5f0fcbad", size = 169504 } wheels = [ @@ -5140,7 +5109,7 @@ name = "triton" version = "3.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filelock", marker = "(platform_machine != 'aarch64' and platform_system != 'Darwin') or (platform_system != 'Darwin' and platform_system != 'Linux' and sys_platform != 'linux')" }, + { name = "filelock", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/45/27/14cc3101409b9b4b9241d2ba7deaa93535a217a211c86c4cc7151fb12181/triton-3.0.0-1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e1efef76935b2febc365bfadf74bcb65a6f959a9872e5bddf44cc9e0adce1e1a", size = 209376304 }, @@ -5535,64 +5504,64 @@ wheels = [ [[package]] name = "yarl" -version = "1.18.3" +version = "1.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, { name = "multidict" }, { name = "propcache" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b7/9d/4b94a8e6d2b51b599516a5cb88e5bc99b4d8d4583e468057eaa29d5f0918/yarl-1.18.3.tar.gz", hash = "sha256:ac1801c45cbf77b6c99242eeff4fffb5e4e73a800b5c4ad4fc0be5def634d2e1", size = 181062 } +sdist = { url = "https://files.pythonhosted.org/packages/23/52/e9766cc6c2eab7dd1e9749c52c9879317500b46fb97d4105223f86679f93/yarl-1.16.0.tar.gz", hash = "sha256:b6f687ced5510a9a2474bbae96a4352e5ace5fa34dc44a217b0537fec1db00b4", size = 176548 } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/98/e005bc608765a8a5569f58e650961314873c8469c333616eb40bff19ae97/yarl-1.18.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7df647e8edd71f000a5208fe6ff8c382a1de8edfbccdbbfe649d263de07d8c34", size = 141458 }, - { url = "https://files.pythonhosted.org/packages/df/5d/f8106b263b8ae8a866b46d9be869ac01f9b3fb7f2325f3ecb3df8003f796/yarl-1.18.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c69697d3adff5aa4f874b19c0e4ed65180ceed6318ec856ebc423aa5850d84f7", size = 94365 }, - { url = "https://files.pythonhosted.org/packages/56/3e/d8637ddb9ba69bf851f765a3ee288676f7cf64fb3be13760c18cbc9d10bd/yarl-1.18.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:602d98f2c2d929f8e697ed274fbadc09902c4025c5a9963bf4e9edfc3ab6f7ed", size = 92181 }, - { url = "https://files.pythonhosted.org/packages/76/f9/d616a5c2daae281171de10fba41e1c0e2d8207166fc3547252f7d469b4e1/yarl-1.18.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c654d5207c78e0bd6d749f6dae1dcbbfde3403ad3a4b11f3c5544d9906969dde", size = 315349 }, - { url = "https://files.pythonhosted.org/packages/bb/b4/3ea5e7b6f08f698b3769a06054783e434f6d59857181b5c4e145de83f59b/yarl-1.18.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5094d9206c64181d0f6e76ebd8fb2f8fe274950a63890ee9e0ebfd58bf9d787b", size = 330494 }, - { url = "https://files.pythonhosted.org/packages/55/f1/e0fc810554877b1b67420568afff51b967baed5b53bcc983ab164eebf9c9/yarl-1.18.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:35098b24e0327fc4ebdc8ffe336cee0a87a700c24ffed13161af80124b7dc8e5", size = 326927 }, - { url = "https://files.pythonhosted.org/packages/a9/42/b1753949b327b36f210899f2dd0a0947c0c74e42a32de3f8eb5c7d93edca/yarl-1.18.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3236da9272872443f81fedc389bace88408f64f89f75d1bdb2256069a8730ccc", size = 319703 }, - { url = "https://files.pythonhosted.org/packages/f0/6d/e87c62dc9635daefb064b56f5c97df55a2e9cc947a2b3afd4fd2f3b841c7/yarl-1.18.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2c08cc9b16f4f4bc522771d96734c7901e7ebef70c6c5c35dd0f10845270bcd", size = 310246 }, - { url = "https://files.pythonhosted.org/packages/e3/ef/e2e8d1785cdcbd986f7622d7f0098205f3644546da7919c24b95790ec65a/yarl-1.18.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:80316a8bd5109320d38eef8833ccf5f89608c9107d02d2a7f985f98ed6876990", size = 319730 }, - { url = "https://files.pythonhosted.org/packages/fc/15/8723e22345bc160dfde68c4b3ae8b236e868f9963c74015f1bc8a614101c/yarl-1.18.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1e1cc06da1491e6734f0ea1e6294ce00792193c463350626571c287c9a704db", size = 321681 }, - { url = "https://files.pythonhosted.org/packages/86/09/bf764e974f1516efa0ae2801494a5951e959f1610dd41edbfc07e5e0f978/yarl-1.18.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:fea09ca13323376a2fdfb353a5fa2e59f90cd18d7ca4eaa1fd31f0a8b4f91e62", size = 324812 }, - { url = "https://files.pythonhosted.org/packages/f6/4c/20a0187e3b903c97d857cf0272d687c1b08b03438968ae8ffc50fe78b0d6/yarl-1.18.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e3b9fd71836999aad54084906f8663dffcd2a7fb5cdafd6c37713b2e72be1760", size = 337011 }, - { url = "https://files.pythonhosted.org/packages/c9/71/6244599a6e1cc4c9f73254a627234e0dad3883ece40cc33dce6265977461/yarl-1.18.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:757e81cae69244257d125ff31663249b3013b5dc0a8520d73694aed497fb195b", size = 338132 }, - { url = "https://files.pythonhosted.org/packages/af/f5/e0c3efaf74566c4b4a41cb76d27097df424052a064216beccae8d303c90f/yarl-1.18.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b1771de9944d875f1b98a745bc547e684b863abf8f8287da8466cf470ef52690", size = 331849 }, - { url = "https://files.pythonhosted.org/packages/8a/b8/3d16209c2014c2f98a8f658850a57b716efb97930aebf1ca0d9325933731/yarl-1.18.3-cp310-cp310-win32.whl", hash = "sha256:8874027a53e3aea659a6d62751800cf6e63314c160fd607489ba5c2edd753cf6", size = 84309 }, - { url = "https://files.pythonhosted.org/packages/fd/b7/2e9a5b18eb0fe24c3a0e8bae994e812ed9852ab4fd067c0107fadde0d5f0/yarl-1.18.3-cp310-cp310-win_amd64.whl", hash = "sha256:93b2e109287f93db79210f86deb6b9bbb81ac32fc97236b16f7433db7fc437d8", size = 90484 }, - { url = "https://files.pythonhosted.org/packages/40/93/282b5f4898d8e8efaf0790ba6d10e2245d2c9f30e199d1a85cae9356098c/yarl-1.18.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8503ad47387b8ebd39cbbbdf0bf113e17330ffd339ba1144074da24c545f0069", size = 141555 }, - { url = "https://files.pythonhosted.org/packages/6d/9c/0a49af78df099c283ca3444560f10718fadb8a18dc8b3edf8c7bd9fd7d89/yarl-1.18.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:02ddb6756f8f4517a2d5e99d8b2f272488e18dd0bfbc802f31c16c6c20f22193", size = 94351 }, - { url = "https://files.pythonhosted.org/packages/5a/a1/205ab51e148fdcedad189ca8dd587794c6f119882437d04c33c01a75dece/yarl-1.18.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:67a283dd2882ac98cc6318384f565bffc751ab564605959df4752d42483ad889", size = 92286 }, - { url = "https://files.pythonhosted.org/packages/ed/fe/88b690b30f3f59275fb674f5f93ddd4a3ae796c2b62e5bb9ece8a4914b83/yarl-1.18.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d980e0325b6eddc81331d3f4551e2a333999fb176fd153e075c6d1c2530aa8a8", size = 340649 }, - { url = "https://files.pythonhosted.org/packages/07/eb/3b65499b568e01f36e847cebdc8d7ccb51fff716dbda1ae83c3cbb8ca1c9/yarl-1.18.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b643562c12680b01e17239be267bc306bbc6aac1f34f6444d1bded0c5ce438ca", size = 356623 }, - { url = "https://files.pythonhosted.org/packages/33/46/f559dc184280b745fc76ec6b1954de2c55595f0ec0a7614238b9ebf69618/yarl-1.18.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c017a3b6df3a1bd45b9fa49a0f54005e53fbcad16633870104b66fa1a30a29d8", size = 354007 }, - { url = "https://files.pythonhosted.org/packages/af/ba/1865d85212351ad160f19fb99808acf23aab9a0f8ff31c8c9f1b4d671fc9/yarl-1.18.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:75674776d96d7b851b6498f17824ba17849d790a44d282929c42dbb77d4f17ae", size = 344145 }, - { url = "https://files.pythonhosted.org/packages/94/cb/5c3e975d77755d7b3d5193e92056b19d83752ea2da7ab394e22260a7b824/yarl-1.18.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ccaa3a4b521b780a7e771cc336a2dba389a0861592bbce09a476190bb0c8b4b3", size = 336133 }, - { url = "https://files.pythonhosted.org/packages/19/89/b77d3fd249ab52a5c40859815765d35c91425b6bb82e7427ab2f78f5ff55/yarl-1.18.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2d06d3005e668744e11ed80812e61efd77d70bb7f03e33c1598c301eea20efbb", size = 347967 }, - { url = "https://files.pythonhosted.org/packages/35/bd/f6b7630ba2cc06c319c3235634c582a6ab014d52311e7d7c22f9518189b5/yarl-1.18.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:9d41beda9dc97ca9ab0b9888cb71f7539124bc05df02c0cff6e5acc5a19dcc6e", size = 346397 }, - { url = "https://files.pythonhosted.org/packages/18/1a/0b4e367d5a72d1f095318344848e93ea70da728118221f84f1bf6c1e39e7/yarl-1.18.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ba23302c0c61a9999784e73809427c9dbedd79f66a13d84ad1b1943802eaaf59", size = 350206 }, - { url = "https://files.pythonhosted.org/packages/b5/cf/320fff4367341fb77809a2d8d7fe75b5d323a8e1b35710aafe41fdbf327b/yarl-1.18.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6748dbf9bfa5ba1afcc7556b71cda0d7ce5f24768043a02a58846e4a443d808d", size = 362089 }, - { url = "https://files.pythonhosted.org/packages/57/cf/aadba261d8b920253204085268bad5e8cdd86b50162fcb1b10c10834885a/yarl-1.18.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0b0cad37311123211dc91eadcb322ef4d4a66008d3e1bdc404808992260e1a0e", size = 366267 }, - { url = "https://files.pythonhosted.org/packages/54/58/fb4cadd81acdee6dafe14abeb258f876e4dd410518099ae9a35c88d8097c/yarl-1.18.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0fb2171a4486bb075316ee754c6d8382ea6eb8b399d4ec62fde2b591f879778a", size = 359141 }, - { url = "https://files.pythonhosted.org/packages/9a/7a/4c571597589da4cd5c14ed2a0b17ac56ec9ee7ee615013f74653169e702d/yarl-1.18.3-cp311-cp311-win32.whl", hash = "sha256:61b1a825a13bef4a5f10b1885245377d3cd0bf87cba068e1d9a88c2ae36880e1", size = 84402 }, - { url = "https://files.pythonhosted.org/packages/ae/7b/8600250b3d89b625f1121d897062f629883c2f45339623b69b1747ec65fa/yarl-1.18.3-cp311-cp311-win_amd64.whl", hash = "sha256:b9d60031cf568c627d028239693fd718025719c02c9f55df0a53e587aab951b5", size = 91030 }, - { url = "https://files.pythonhosted.org/packages/33/85/bd2e2729752ff4c77338e0102914897512e92496375e079ce0150a6dc306/yarl-1.18.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1dd4bdd05407ced96fed3d7f25dbbf88d2ffb045a0db60dbc247f5b3c5c25d50", size = 142644 }, - { url = "https://files.pythonhosted.org/packages/ff/74/1178322cc0f10288d7eefa6e4a85d8d2e28187ccab13d5b844e8b5d7c88d/yarl-1.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7c33dd1931a95e5d9a772d0ac5e44cac8957eaf58e3c8da8c1414de7dd27c576", size = 94962 }, - { url = "https://files.pythonhosted.org/packages/be/75/79c6acc0261e2c2ae8a1c41cf12265e91628c8c58ae91f5ff59e29c0787f/yarl-1.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:25b411eddcfd56a2f0cd6a384e9f4f7aa3efee14b188de13048c25b5e91f1640", size = 92795 }, - { url = "https://files.pythonhosted.org/packages/6b/32/927b2d67a412c31199e83fefdce6e645247b4fb164aa1ecb35a0f9eb2058/yarl-1.18.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:436c4fc0a4d66b2badc6c5fc5ef4e47bb10e4fd9bf0c79524ac719a01f3607c2", size = 332368 }, - { url = "https://files.pythonhosted.org/packages/19/e5/859fca07169d6eceeaa4fde1997c91d8abde4e9a7c018e371640c2da2b71/yarl-1.18.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e35ef8683211db69ffe129a25d5634319a677570ab6b2eba4afa860f54eeaf75", size = 342314 }, - { url = "https://files.pythonhosted.org/packages/08/75/76b63ccd91c9e03ab213ef27ae6add2e3400e77e5cdddf8ed2dbc36e3f21/yarl-1.18.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84b2deecba4a3f1a398df819151eb72d29bfeb3b69abb145a00ddc8d30094512", size = 341987 }, - { url = "https://files.pythonhosted.org/packages/1a/e1/a097d5755d3ea8479a42856f51d97eeff7a3a7160593332d98f2709b3580/yarl-1.18.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e5a1fea0fd4f5bfa7440a47eff01d9822a65b4488f7cff83155a0f31a2ecba", size = 336914 }, - { url = "https://files.pythonhosted.org/packages/0b/42/e1b4d0e396b7987feceebe565286c27bc085bf07d61a59508cdaf2d45e63/yarl-1.18.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0e883008013c0e4aef84dcfe2a0b172c4d23c2669412cf5b3371003941f72bb", size = 325765 }, - { url = "https://files.pythonhosted.org/packages/7e/18/03a5834ccc9177f97ca1bbb245b93c13e58e8225276f01eedc4cc98ab820/yarl-1.18.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5a3f356548e34a70b0172d8890006c37be92995f62d95a07b4a42e90fba54272", size = 344444 }, - { url = "https://files.pythonhosted.org/packages/c8/03/a713633bdde0640b0472aa197b5b86e90fbc4c5bc05b727b714cd8a40e6d/yarl-1.18.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ccd17349166b1bee6e529b4add61727d3f55edb7babbe4069b5764c9587a8cc6", size = 340760 }, - { url = "https://files.pythonhosted.org/packages/eb/99/f6567e3f3bbad8fd101886ea0276c68ecb86a2b58be0f64077396cd4b95e/yarl-1.18.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b958ddd075ddba5b09bb0be8a6d9906d2ce933aee81100db289badbeb966f54e", size = 346484 }, - { url = "https://files.pythonhosted.org/packages/8e/a9/84717c896b2fc6cb15bd4eecd64e34a2f0a9fd6669e69170c73a8b46795a/yarl-1.18.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c7d79f7d9aabd6011004e33b22bc13056a3e3fb54794d138af57f5ee9d9032cb", size = 359864 }, - { url = "https://files.pythonhosted.org/packages/1e/2e/d0f5f1bef7ee93ed17e739ec8dbcb47794af891f7d165fa6014517b48169/yarl-1.18.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4891ed92157e5430874dad17b15eb1fda57627710756c27422200c52d8a4e393", size = 364537 }, - { url = "https://files.pythonhosted.org/packages/97/8a/568d07c5d4964da5b02621a517532adb8ec5ba181ad1687191fffeda0ab6/yarl-1.18.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ce1af883b94304f493698b00d0f006d56aea98aeb49d75ec7d98cd4a777e9285", size = 357861 }, - { url = "https://files.pythonhosted.org/packages/7d/e3/924c3f64b6b3077889df9a1ece1ed8947e7b61b0a933f2ec93041990a677/yarl-1.18.3-cp312-cp312-win32.whl", hash = "sha256:f91c4803173928a25e1a55b943c81f55b8872f0018be83e3ad4938adffb77dd2", size = 84097 }, - { url = "https://files.pythonhosted.org/packages/34/45/0e055320daaabfc169b21ff6174567b2c910c45617b0d79c68d7ab349b02/yarl-1.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:7e2ee16578af3b52ac2f334c3b1f92262f47e02cc6193c598502bd46f5cd1477", size = 90399 }, - { url = "https://files.pythonhosted.org/packages/f5/4b/a06e0ec3d155924f77835ed2d167ebd3b211a7b0853da1cf8d8414d784ef/yarl-1.18.3-py3-none-any.whl", hash = "sha256:b57f4f58099328dfb26c6a771d09fb20dbbae81d20cfb66141251ea063bd101b", size = 45109 }, + { url = "https://files.pythonhosted.org/packages/df/30/00b17348655202e4bd24f8d79cd062888e5d3bdbf2ba726615c5d21b54a5/yarl-1.16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32468f41242d72b87ab793a86d92f885355bcf35b3355aa650bfa846a5c60058", size = 140016 }, + { url = "https://files.pythonhosted.org/packages/a5/15/9b7b85b72b81f180689257b2bb6e54d5d0764a399679aa06d5dec8ca6e2e/yarl-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:234f3a3032b505b90e65b5bc6652c2329ea7ea8855d8de61e1642b74b4ee65d2", size = 92953 }, + { url = "https://files.pythonhosted.org/packages/31/41/91848bbb76789336d3b786ff144030001b5027b17729b3afa32da668f5b0/yarl-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8a0296040e5cddf074c7f5af4a60f3fc42c0237440df7bcf5183be5f6c802ed5", size = 90793 }, + { url = "https://files.pythonhosted.org/packages/6c/99/f1ada764e350ab054e14902f3f68589a7d77469ac47fbc512aa1a78a2f35/yarl-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de6c14dd7c7c0badba48157474ea1f03ebee991530ba742d381b28d4f314d6f3", size = 313155 }, + { url = "https://files.pythonhosted.org/packages/75/fd/998ccdb489ca97d9073d882265203a2fae4c5bff30eb9b8a0bbbed7aef2b/yarl-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b140e532fe0266003c936d017c1ac301e72ee4a3fd51784574c05f53718a55d8", size = 328624 }, + { url = "https://files.pythonhosted.org/packages/2d/5d/395bbae1f509f64e6d26b7ffffff178d70c5480f15af735dfb0afb8f0dc5/yarl-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:019f5d58093402aa8f6661e60fd82a28746ad6d156f6c5336a70a39bd7b162b9", size = 325163 }, + { url = "https://files.pythonhosted.org/packages/1d/25/65601d336189d122483f5ff0276b08278fa4778f833458cfcac5c6eddc87/yarl-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c42998fd1cbeb53cd985bff0e4bc25fbe55fd6eb3a545a724c1012d69d5ec84", size = 318076 }, + { url = "https://files.pythonhosted.org/packages/50/bb/0c9692ec457c1ed023654a9fba6d0c69a20c79b56275d972f6a24ab18547/yarl-1.16.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c7c30fb38c300fe8140df30a046a01769105e4cf4282567a29b5cdb635b66c4", size = 309551 }, + { url = "https://files.pythonhosted.org/packages/a5/2f/d0ced2050a203241a3f2e05c5bb86038b071f216897defd824dd85333f9e/yarl-1.16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e49e0fd86c295e743fd5be69b8b0712f70a686bc79a16e5268386c2defacaade", size = 317678 }, + { url = "https://files.pythonhosted.org/packages/46/93/b7359aa2bd0567eca72491cd20059744ed6ee00f08cd58c861243f656a90/yarl-1.16.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:b9ca7b9147eb1365c8bab03c003baa1300599575effad765e0b07dd3501ea9af", size = 317003 }, + { url = "https://files.pythonhosted.org/packages/87/18/77ef4d45d19ecafad0f7c07d5cf13a757a90122383494bc5a3e8ee68e2f2/yarl-1.16.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:27e11db3f1e6a51081a981509f75617b09810529de508a181319193d320bc5c7", size = 322795 }, + { url = "https://files.pythonhosted.org/packages/28/a9/b38880bf79665d1c8a3d4c09d6f7a686a50f8c74caf07603a2b8e5314038/yarl-1.16.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8994c42f4ca25df5380ddf59f315c518c81df6a68fed5bb0c159c6cb6b92f120", size = 337022 }, + { url = "https://files.pythonhosted.org/packages/e9/79/865788b297fc17117e3ff6ea74d5f864185085d61adc3364444732095254/yarl-1.16.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:542fa8e09a581bcdcbb30607c7224beff3fdfb598c798ccd28a8184ffc18b7eb", size = 338357 }, + { url = "https://files.pythonhosted.org/packages/bd/5e/c5cba528448f73c7035c9d3c07261b54312d8caa8372eeeff5e1f07e43ec/yarl-1.16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2bd6a51010c7284d191b79d3b56e51a87d8e1c03b0902362945f15c3d50ed46b", size = 330470 }, + { url = "https://files.pythonhosted.org/packages/1a/e4/90757595d81ec328ad94afa62d0724903a6c72b76e0ee9c9af9d8a399dd2/yarl-1.16.0-cp310-cp310-win32.whl", hash = "sha256:178ccb856e265174a79f59721031060f885aca428983e75c06f78aa24b91d929", size = 82967 }, + { url = "https://files.pythonhosted.org/packages/01/5a/b82ec5e7557b0d938b9475cbb5dcbb1f98c8601101188d79e423dc215cd0/yarl-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe8bba2545427418efc1929c5c42852bdb4143eb8d0a46b09de88d1fe99258e7", size = 89159 }, + { url = "https://files.pythonhosted.org/packages/0a/00/b29affe83de95e403f8a2a669b5a33f1e7dfe686264008100052eb0b05fd/yarl-1.16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d8643975a0080f361639787415a038bfc32d29208a4bf6b783ab3075a20b1ef3", size = 140120 }, + { url = "https://files.pythonhosted.org/packages/3f/22/bcc9799950281a5d4f646536854839ccdbb965e900827ef0750680f81faf/yarl-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:676d96bafc8c2d0039cea0cd3fd44cee7aa88b8185551a2bb93354668e8315c2", size = 92956 }, + { url = "https://files.pythonhosted.org/packages/33/0f/1b76d853d9d921d68bd9991648be17d34e7ac51e2e20e7658f8ee7e2e2ad/yarl-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d9525f03269e64310416dbe6c68d3b23e5d34aaa8f47193a1c45ac568cecbc49", size = 90891 }, + { url = "https://files.pythonhosted.org/packages/61/19/3666d990c24aae98c748e2c262adc9b3a71e38834df007ac5317f4bbd789/yarl-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b37d5ec034e668b22cf0ce1074d6c21fd2a08b90d11b1b73139b750a8b0dd97", size = 338857 }, + { url = "https://files.pythonhosted.org/packages/a0/3d/54acbb3cdfcfea03d6a3535cff1e060a2de23e419a4e3955c9661171b8a8/yarl-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4f32c4cb7386b41936894685f6e093c8dfaf0960124d91fe0ec29fe439e201d0", size = 354005 }, + { url = "https://files.pythonhosted.org/packages/15/98/cd9fe3938422c88775c94578a6c145aca89ff8368ff64e6032213ac12403/yarl-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b8e265a0545637492a7e12fd7038370d66c9375a61d88c5567d0e044ded9202", size = 351195 }, + { url = "https://files.pythonhosted.org/packages/e2/13/b6eff6ea1667aee948ecd6b1c8fb6473234f8e48f49af97be93251869c51/yarl-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:789a3423f28a5fff46fbd04e339863c169ece97c827b44de16e1a7a42bc915d2", size = 342789 }, + { url = "https://files.pythonhosted.org/packages/fe/05/d98e65ea74a7e44bb033b2cf5bcc16edc1d5212bdc5ca7fbb5e380d89f8e/yarl-1.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f1d1f45e3e8d37c804dca99ab3cf4ab3ed2e7a62cd82542924b14c0a4f46d243", size = 336478 }, + { url = "https://files.pythonhosted.org/packages/7d/47/43de2e94b75f36d84733a35c807d0e33aaf084e98f32e2cbc685102f4ba4/yarl-1.16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:621280719c4c5dad4c1391160a9b88925bb8b0ff6a7d5af3224643024871675f", size = 346008 }, + { url = "https://files.pythonhosted.org/packages/e2/de/9c2f900ec5e2f2e20329cfe7dcd9452e326d08cb5ecd098c2d4e9987b65c/yarl-1.16.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ed097b26f18a1f5ff05f661dc36528c5f6735ba4ce8c9645e83b064665131349", size = 343745 }, + { url = "https://files.pythonhosted.org/packages/56/cd/b014dce22e37b77caa37f998c6c47434fd78d01e7be07119629f369f5ee1/yarl-1.16.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2f1fe2b2e3ee418862f5ebc0c0083c97f6f6625781382f828f6d4e9b614eba9b", size = 349705 }, + { url = "https://files.pythonhosted.org/packages/07/17/bb191a26f7189423964e008ccb5146ce5258454ef3979f9d4c6860d282c7/yarl-1.16.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:87dd10bc0618991c66cee0cc65fa74a45f4ecb13bceec3c62d78ad2e42b27a16", size = 360767 }, + { url = "https://files.pythonhosted.org/packages/19/09/7d777369e151991b708a5b35280ea7444621d65af5f0545bcdce5d840867/yarl-1.16.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4199db024b58a8abb2cfcedac7b1292c3ad421684571aeb622a02f242280e8d6", size = 364755 }, + { url = "https://files.pythonhosted.org/packages/00/32/7558997d1d2e53dab15f6db5db49fc6b412b63ede3cb8314e5dd7cff14fe/yarl-1.16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:99a9dcd4b71dd5f5f949737ab3f356cfc058c709b4f49833aeffedc2652dac56", size = 357087 }, + { url = "https://files.pythonhosted.org/packages/28/20/c49a95a30c57224e5fb0fc83235295684b041300ce508b71821cb042527d/yarl-1.16.0-cp311-cp311-win32.whl", hash = "sha256:a9394c65ae0ed95679717d391c862dece9afacd8fa311683fc8b4362ce8a410c", size = 83030 }, + { url = "https://files.pythonhosted.org/packages/75/e3/2a746721d6f32886d9bafccdb80174349f180ccae0a287f25ba4312a2618/yarl-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:5b9101f528ae0f8f65ac9d64dda2bb0627de8a50344b2f582779f32fda747c1d", size = 89616 }, + { url = "https://files.pythonhosted.org/packages/3a/be/82f696c8ce0395c37f62b955202368086e5cc114d5bb9cb1b634cff5e01d/yarl-1.16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4ffb7c129707dd76ced0a4a4128ff452cecf0b0e929f2668ea05a371d9e5c104", size = 141230 }, + { url = "https://files.pythonhosted.org/packages/38/60/45caaa748b53c4b0964f899879fcddc41faa4e0d12c6f0ae3311e8c151ff/yarl-1.16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1a5e9d8ce1185723419c487758d81ac2bde693711947032cce600ca7c9cda7d6", size = 93515 }, + { url = "https://files.pythonhosted.org/packages/54/bd/33aaca2f824dc1d630729e16e313797e8b24c8f7b6803307e5394274e443/yarl-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d743e3118b2640cef7768ea955378c3536482d95550222f908f392167fe62059", size = 91441 }, + { url = "https://files.pythonhosted.org/packages/af/fa/1ce8ca85489925aabdb8d2e7bbeaf74e7d3e6ac069779d6d6b9c7c62a8ed/yarl-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26768342f256e6e3c37533bf9433f5f15f3e59e3c14b2409098291b3efaceacb", size = 330871 }, + { url = "https://files.pythonhosted.org/packages/f1/2a/a8110a225e498b87315827f8b61d24de35f86041834cf8c9c5544380c46b/yarl-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d1b0796168b953bca6600c5f97f5ed407479889a36ad7d17183366260f29a6b9", size = 340641 }, + { url = "https://files.pythonhosted.org/packages/d0/64/20cd1cb1f60b3ff49e7d75c1a2083352e7c5939368aafa960712c9e53797/yarl-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:858728086914f3a407aa7979cab743bbda1fe2bdf39ffcd991469a370dd7414d", size = 340245 }, + { url = "https://files.pythonhosted.org/packages/77/a8/7f38bbefb22eb925a68ad1d8193b05f51515614a6c0ebcadf26e9ae5e5ad/yarl-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5570e6d47bcb03215baf4c9ad7bf7c013e56285d9d35013541f9ac2b372593e7", size = 336054 }, + { url = "https://files.pythonhosted.org/packages/b4/a6/ac633ea3ea0c4eb1057e6800db1d077e77493b4b3449a4a97b2fbefadef4/yarl-1.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66ea8311422a7ba1fc79b4c42c2baa10566469fe5a78500d4e7754d6e6db8724", size = 324405 }, + { url = "https://files.pythonhosted.org/packages/93/cd/4fc87ce9b0df7afb610ffb904f4aef25f59e0ad40a49da19a475facf98b7/yarl-1.16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:649bddcedee692ee8a9b7b6e38582cb4062dc4253de9711568e5620d8707c2a3", size = 342235 }, + { url = "https://files.pythonhosted.org/packages/9f/bc/38bae4b716da1206849d88e167d3d2c5695ae9b418a3915220947593e5ca/yarl-1.16.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:3a91654adb7643cb21b46f04244c5a315a440dcad63213033826549fa2435f71", size = 340835 }, + { url = "https://files.pythonhosted.org/packages/dc/0f/b9efbc0075916a450cbad41299dff3bdd3393cb1d8378bb831c4a6a836e1/yarl-1.16.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b439cae82034ade094526a8f692b9a2b5ee936452de5e4c5f0f6c48df23f8604", size = 344323 }, + { url = "https://files.pythonhosted.org/packages/87/6d/dc483ea1574005f14ef4c5f5f726cf60327b07ac83bd417d98db23e5285f/yarl-1.16.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:571f781ae8ac463ce30bacebfaef2c6581543776d5970b2372fbe31d7bf31a07", size = 355112 }, + { url = "https://files.pythonhosted.org/packages/10/22/3b7c3728d26b3cc295c51160ae4e2612ab7d3f9df30beece44bf72861730/yarl-1.16.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:aa7943f04f36d6cafc0cf53ea89824ac2c37acbdb4b316a654176ab8ffd0f968", size = 361506 }, + { url = "https://files.pythonhosted.org/packages/ad/8d/b7b5d43cf22a020b564ddf7502d83df150d797e34f18f6bf5fe0f12cbd91/yarl-1.16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1a5cf32539373ff39d97723e39a9283a7277cbf1224f7aef0c56c9598b6486c3", size = 355746 }, + { url = "https://files.pythonhosted.org/packages/d9/a6/a2098bf3f09d38eb540b2b192e180d9d41c2ff64b692783db2188f0a55e3/yarl-1.16.0-cp312-cp312-win32.whl", hash = "sha256:a5b6c09b9b4253d6a208b0f4a2f9206e511ec68dce9198e0fbec4f160137aa67", size = 82675 }, + { url = "https://files.pythonhosted.org/packages/ed/a6/0a54b382cfc336e772b72681d6816a99222dc2d21876e649474973b8d244/yarl-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1208ca14eed2fda324042adf8d6c0adf4a31522fa95e0929027cd487875f0240", size = 88986 }, + { url = "https://files.pythonhosted.org/packages/fb/f7/87a32867ddc1a9817018bfd6109ee57646a543acf0d272843d8393e575f9/yarl-1.16.0-py3-none-any.whl", hash = "sha256:e6980a558d8461230c457218bd6c92dfc1d10205548215c2c21d79dc8d0a96f3", size = 43746 }, ] [[package]] From df25703cc25356f76f3d1db2344474ae2917fbc9 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 17:05:01 +0000 Subject: [PATCH 16/41] Address PR review: Add constants, IPv4 validation, error handling, and expanded tests Co-Authored-By: Joe Moura --- src/crewai/agent.py | 9 ++++-- src/crewai/utilities/__init__.py | 3 +- src/crewai/utilities/string_utils.py | 40 +++++++++++++++++++++----- tests/utilities/test_string_utils.py | 42 ++++++++++++++++++++++++++-- 4 files changed, 81 insertions(+), 13 deletions(-) diff --git a/src/crewai/agent.py b/src/crewai/agent.py index b92a83d14..d8b6860e3 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -142,8 +142,13 @@ class Agent(BaseAgent): self.embedder = crew_embedder if self.knowledge_sources: - from crewai.utilities import sanitize_collection_name - knowledge_agent_name = sanitize_collection_name(self.role) + try: + from crewai.utilities import sanitize_collection_name + knowledge_agent_name = sanitize_collection_name(self.role) + except Exception as e: + self._logger.warning(f"Error sanitizing collection name: {e}") + knowledge_agent_name = "default_agent" + if isinstance(self.knowledge_sources, list) and all( isinstance(k, BaseKnowledgeSource) for k in self.knowledge_sources ): diff --git a/src/crewai/utilities/__init__.py b/src/crewai/utilities/__init__.py index f2badd2d4..946c4390a 100644 --- a/src/crewai/utilities/__init__.py +++ b/src/crewai/utilities/__init__.py @@ -7,7 +7,7 @@ from .parser import YamlParser from .printer import Printer from .prompts import Prompts from .rpm_controller import RPMController -from .string_utils import sanitize_collection_name +from .string_utils import sanitize_collection_name, is_ipv4_pattern from .exceptions.context_window_exceeding_exception import ( LLMContextLengthExceededException, ) @@ -27,4 +27,5 @@ __all__ = [ "LLMContextLengthExceededException", "EmbeddingConfigurator", "sanitize_collection_name", + "is_ipv4_pattern", ] diff --git a/src/crewai/utilities/string_utils.py b/src/crewai/utilities/string_utils.py index 6da07b20d..05a637383 100644 --- a/src/crewai/utilities/string_utils.py +++ b/src/crewai/utilities/string_utils.py @@ -84,6 +84,28 @@ def interpolate_only( from typing import Optional +# Constants for ChromaDB collection name requirements +MIN_LENGTH = 3 +MAX_LENGTH = 63 +DEFAULT_COLLECTION = "default_collection" + +# Compiled regex patterns for better performance +INVALID_CHARS_PATTERN = re.compile(r"[^a-zA-Z0-9_-]") +IPV4_PATTERN = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") + + +def is_ipv4_pattern(name: str) -> bool: + """ + Check if a string matches an IPv4 address pattern. + + Args: + name: The string to check + + Returns: + True if the string matches an IPv4 pattern, False otherwise + """ + return bool(IPV4_PATTERN.match(name)) + def sanitize_collection_name(name: Optional[str]) -> str: """ @@ -101,10 +123,14 @@ def sanitize_collection_name(name: Optional[str]) -> str: A sanitized collection name that meets ChromaDB requirements """ if not name: - return "default_collection" + return DEFAULT_COLLECTION + + # Handle IPv4 pattern + if is_ipv4_pattern(name): + name = f"ip_{name}" # Replace spaces and invalid characters with underscores - sanitized = re.sub(r"[^a-zA-Z0-9_-]", "_", name) + sanitized = INVALID_CHARS_PATTERN.sub("_", name) # Ensure it starts with alphanumeric if not sanitized[0].isalnum(): @@ -114,12 +140,12 @@ def sanitize_collection_name(name: Optional[str]) -> str: if not sanitized[-1].isalnum(): sanitized = sanitized[:-1] + "z" - # Ensure length is between 3-63 characters - if len(sanitized) < 3: + # Ensure length is between MIN_LENGTH-MAX_LENGTH characters + if len(sanitized) < MIN_LENGTH: # Add padding with alphanumeric character at the end - sanitized = sanitized + "x" * (3 - len(sanitized)) - if len(sanitized) > 63: - sanitized = sanitized[:63] + sanitized = sanitized + "x" * (MIN_LENGTH - len(sanitized)) + if len(sanitized) > MAX_LENGTH: + sanitized = sanitized[:MAX_LENGTH] # Ensure it still ends with alphanumeric after truncation if not sanitized[-1].isalnum(): sanitized = sanitized[:-1] + "z" diff --git a/tests/utilities/test_string_utils.py b/tests/utilities/test_string_utils.py index 04a0dcb56..2e2cf2e0c 100644 --- a/tests/utilities/test_string_utils.py +++ b/tests/utilities/test_string_utils.py @@ -3,7 +3,12 @@ from typing import Any, Dict, List, Union import pytest -from crewai.utilities.string_utils import interpolate_only, sanitize_collection_name +from crewai.utilities import is_ipv4_pattern, sanitize_collection_name +from crewai.utilities.string_utils import ( + MAX_LENGTH, + MIN_LENGTH, + interpolate_only, +) class TestInterpolateOnly: @@ -193,7 +198,7 @@ class TestStringUtils(unittest.TestCase): """Test sanitizing a very long collection name.""" long_name = "This is an extremely long role name that will definitely exceed the ChromaDB collection name limit of 63 characters and cause an error when used as a collection name" sanitized = sanitize_collection_name(long_name) - self.assertLessEqual(len(sanitized), 63) + self.assertLessEqual(len(sanitized), MAX_LENGTH) self.assertTrue(sanitized[0].isalnum()) self.assertTrue(sanitized[-1].isalnum()) self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) @@ -210,7 +215,7 @@ class TestStringUtils(unittest.TestCase): """Test sanitizing a very short name.""" short_name = "A" sanitized = sanitize_collection_name(short_name) - self.assertGreaterEqual(len(sanitized), 3) + self.assertGreaterEqual(len(sanitized), MIN_LENGTH) self.assertTrue(sanitized[0].isalnum()) self.assertTrue(sanitized[-1].isalnum()) @@ -226,6 +231,37 @@ class TestStringUtils(unittest.TestCase): sanitized = sanitize_collection_name(None) self.assertEqual(sanitized, "default_collection") + def test_sanitize_collection_name_ipv4_pattern(self): + """Test sanitizing an IPv4 address.""" + ipv4 = "192.168.1.1" + sanitized = sanitize_collection_name(ipv4) + self.assertTrue(sanitized.startswith("ip_")) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) + + def test_is_ipv4_pattern(self): + """Test IPv4 pattern detection.""" + self.assertTrue(is_ipv4_pattern("192.168.1.1")) + self.assertFalse(is_ipv4_pattern("not.an.ip.address")) + + def test_sanitize_collection_name_properties(self): + """Test that sanitized collection names always meet ChromaDB requirements.""" + test_cases = [ + "A" * 100, # Very long name + "_start_with_underscore", + "end_with_underscore_", + "contains@special#characters", + "192.168.1.1", # IPv4 address + "a" * 2, # Too short + ] + for test_case in test_cases: + sanitized = sanitize_collection_name(test_case) + self.assertGreaterEqual(len(sanitized), MIN_LENGTH) + self.assertLessEqual(len(sanitized), MAX_LENGTH) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + if __name__ == "__main__": unittest.main() From 6b14ffcffb92abfd211b6068c39928c411ff35d0 Mon Sep 17 00:00:00 2001 From: lucasgomide Date: Tue, 25 Mar 2025 15:06:00 -0300 Subject: [PATCH 17/41] fix: delegate collection name sanitization to knowledge store --- src/crewai/agent.py | 9 +- .../knowledge/storage/knowledge_storage.py | 5 +- src/crewai/utilities/__init__.py | 3 - src/crewai/utilities/chromadb.py | 62 +++ src/crewai/utilities/string_utils.py | 71 ---- tests/agent_test.py | 32 ++ ...with_knowledge_sources_extensive_role.yaml | 382 ++++++++++++++++++ tests/utilities/test_chromadb_utils.py | 81 ++++ tests/utilities/test_string_utils.py | 82 +--- 9 files changed, 563 insertions(+), 164 deletions(-) create mode 100644 src/crewai/utilities/chromadb.py create mode 100644 tests/cassettes/test_agent_with_knowledge_sources_extensive_role.yaml create mode 100644 tests/utilities/test_chromadb_utils.py diff --git a/src/crewai/agent.py b/src/crewai/agent.py index d8b6860e3..a40841db1 100644 --- a/src/crewai/agent.py +++ b/src/crewai/agent.py @@ -142,20 +142,13 @@ class Agent(BaseAgent): self.embedder = crew_embedder if self.knowledge_sources: - try: - from crewai.utilities import sanitize_collection_name - knowledge_agent_name = sanitize_collection_name(self.role) - except Exception as e: - self._logger.warning(f"Error sanitizing collection name: {e}") - knowledge_agent_name = "default_agent" - if isinstance(self.knowledge_sources, list) and all( isinstance(k, BaseKnowledgeSource) for k in self.knowledge_sources ): self.knowledge = Knowledge( sources=self.knowledge_sources, embedder=self.embedder, - collection_name=knowledge_agent_name, + collection_name=self.role, storage=self.knowledge_storage or None, ) except (TypeError, ValueError) as e: diff --git a/src/crewai/knowledge/storage/knowledge_storage.py b/src/crewai/knowledge/storage/knowledge_storage.py index 72240e2b6..37b22ed24 100644 --- a/src/crewai/knowledge/storage/knowledge_storage.py +++ b/src/crewai/knowledge/storage/knowledge_storage.py @@ -98,8 +98,11 @@ class KnowledgeStorage(BaseKnowledgeStorage): else "knowledge" ) if self.app: + from crewai.utilities.chromadb import sanitize_collection_name + self.collection = self.app.get_or_create_collection( - name=collection_name, embedding_function=self.embedder + name=sanitize_collection_name(collection_name), + embedding_function=self.embedder, ) else: raise Exception("Vector Database Client not initialized") diff --git a/src/crewai/utilities/__init__.py b/src/crewai/utilities/__init__.py index 946c4390a..dd6d9fa44 100644 --- a/src/crewai/utilities/__init__.py +++ b/src/crewai/utilities/__init__.py @@ -7,7 +7,6 @@ from .parser import YamlParser from .printer import Printer from .prompts import Prompts from .rpm_controller import RPMController -from .string_utils import sanitize_collection_name, is_ipv4_pattern from .exceptions.context_window_exceeding_exception import ( LLMContextLengthExceededException, ) @@ -26,6 +25,4 @@ __all__ = [ "YamlParser", "LLMContextLengthExceededException", "EmbeddingConfigurator", - "sanitize_collection_name", - "is_ipv4_pattern", ] diff --git a/src/crewai/utilities/chromadb.py b/src/crewai/utilities/chromadb.py new file mode 100644 index 000000000..d993a5896 --- /dev/null +++ b/src/crewai/utilities/chromadb.py @@ -0,0 +1,62 @@ +import re +from typing import Optional + +MIN_COLLECTION_LENGTH = 3 +MAX_COLLECTION_LENGTH = 63 +DEFAULT_COLLECTION = "default_collection" + +# Compiled regex patterns for better performance +INVALID_CHARS_PATTERN = re.compile(r"[^a-zA-Z0-9_-]") +IPV4_PATTERN = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") + + +def is_ipv4_pattern(name: str) -> bool: + """ + Check if a string matches an IPv4 address pattern. + + Args: + name: The string to check + + Returns: + True if the string matches an IPv4 pattern, False otherwise + """ + return bool(IPV4_PATTERN.match(name)) + + +def sanitize_collection_name(name: Optional[str]) -> str: + """ + Sanitize a collection name to meet ChromaDB requirements: + 1. 3-63 characters long + 2. Starts and ends with alphanumeric character + 3. Contains only alphanumeric characters, underscores, or hyphens + 4. No consecutive periods + 5. Not a valid IPv4 address + + Args: + name: The original collection name to sanitize + + Returns: + A sanitized collection name that meets ChromaDB requirements + """ + if not name: + return DEFAULT_COLLECTION + + if is_ipv4_pattern(name): + name = f"ip_{name}" + + sanitized = INVALID_CHARS_PATTERN.sub("_", name) + + if not sanitized[0].isalnum(): + sanitized = "a" + sanitized + + if not sanitized[-1].isalnum(): + sanitized = sanitized[:-1] + "z" + + if len(sanitized) < MIN_COLLECTION_LENGTH: + sanitized = sanitized + "x" * (MIN_COLLECTION_LENGTH - len(sanitized)) + if len(sanitized) > MAX_COLLECTION_LENGTH: + sanitized = sanitized[:MAX_COLLECTION_LENGTH] + if not sanitized[-1].isalnum(): + sanitized = sanitized[:-1] + "z" + + return sanitized diff --git a/src/crewai/utilities/string_utils.py b/src/crewai/utilities/string_utils.py index 05a637383..9a1857781 100644 --- a/src/crewai/utilities/string_utils.py +++ b/src/crewai/utilities/string_utils.py @@ -80,74 +80,3 @@ def interpolate_only( result = result.replace(placeholder, value) return result - - -from typing import Optional - -# Constants for ChromaDB collection name requirements -MIN_LENGTH = 3 -MAX_LENGTH = 63 -DEFAULT_COLLECTION = "default_collection" - -# Compiled regex patterns for better performance -INVALID_CHARS_PATTERN = re.compile(r"[^a-zA-Z0-9_-]") -IPV4_PATTERN = re.compile(r"^(\d{1,3}\.){3}\d{1,3}$") - - -def is_ipv4_pattern(name: str) -> bool: - """ - Check if a string matches an IPv4 address pattern. - - Args: - name: The string to check - - Returns: - True if the string matches an IPv4 pattern, False otherwise - """ - return bool(IPV4_PATTERN.match(name)) - - -def sanitize_collection_name(name: Optional[str]) -> str: - """ - Sanitize a collection name to meet ChromaDB requirements: - 1. 3-63 characters long - 2. Starts and ends with alphanumeric character - 3. Contains only alphanumeric characters, underscores, or hyphens - 4. No consecutive periods - 5. Not a valid IPv4 address - - Args: - name: The original collection name to sanitize - - Returns: - A sanitized collection name that meets ChromaDB requirements - """ - if not name: - return DEFAULT_COLLECTION - - # Handle IPv4 pattern - if is_ipv4_pattern(name): - name = f"ip_{name}" - - # Replace spaces and invalid characters with underscores - sanitized = INVALID_CHARS_PATTERN.sub("_", name) - - # Ensure it starts with alphanumeric - if not sanitized[0].isalnum(): - sanitized = "a" + sanitized - - # Ensure it ends with alphanumeric - if not sanitized[-1].isalnum(): - sanitized = sanitized[:-1] + "z" - - # Ensure length is between MIN_LENGTH-MAX_LENGTH characters - if len(sanitized) < MIN_LENGTH: - # Add padding with alphanumeric character at the end - sanitized = sanitized + "x" * (MIN_LENGTH - len(sanitized)) - if len(sanitized) > MAX_LENGTH: - sanitized = sanitized[:MAX_LENGTH] - # Ensure it still ends with alphanumeric after truncation - if not sanitized[-1].isalnum(): - sanitized = sanitized[:-1] + "z" - - return sanitized diff --git a/tests/agent_test.py b/tests/agent_test.py index b5b3aae93..9abc84137 100644 --- a/tests/agent_test.py +++ b/tests/agent_test.py @@ -1621,6 +1621,38 @@ def test_agent_with_knowledge_sources(): assert "red" in result.raw.lower() +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_agent_with_knowledge_sources_extensive_role(): + content = "Brandon's favorite color is red and he likes Mexican food." + string_source = StringKnowledgeSource(content=content) + + with patch( + "crewai.knowledge.storage.knowledge_storage.KnowledgeStorage" + ) as MockKnowledge: + mock_knowledge_instance = MockKnowledge.return_value + mock_knowledge_instance.sources = [string_source] + mock_knowledge_instance.query.return_value = [{"content": content}] + + agent = Agent( + role="Information Agent with extensive role description that is longer than 80 characters", + goal="Provide information based on knowledge sources", + backstory="You have access to specific knowledge sources.", + llm=LLM(model="gpt-4o-mini"), + knowledge_sources=[string_source], + ) + + task = Task( + description="What is Brandon's favorite color?", + expected_output="Brandon's favorite color.", + agent=agent, + ) + + crew = Crew(agents=[agent], tasks=[task]) + result = crew.kickoff() + + assert "red" in result.raw.lower() + + @pytest.mark.vcr(filter_headers=["authorization"]) def test_agent_with_knowledge_sources_works_with_copy(): content = "Brandon's favorite color is red and he likes Mexican food." diff --git a/tests/cassettes/test_agent_with_knowledge_sources_extensive_role.yaml b/tests/cassettes/test_agent_with_knowledge_sources_extensive_role.yaml new file mode 100644 index 000000000..bfa969b12 --- /dev/null +++ b/tests/cassettes/test_agent_with_knowledge_sources_extensive_role.yaml @@ -0,0 +1,382 @@ +interactions: +- request: + body: '{"input": ["Brandon''s favorite color is red and he likes Mexican food."], + "model": "text-embedding-3-small", "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '137' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.68.2 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.68.2 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"ixDvvEJkB70jXsC6Pg/KPAdQsDvIoCG9/4t0PYfART2AAR69rN0fvQxnQ71CrxU9WrN2vIMlDj1Bfsi74rsOPQebvjxPpCW9lviavZnMaLwySwc9smv6OyUG1bx2lU09phEbvBCLs7xvmHs7sdVdPCrxrrsjXsC8VkT4O5Uxsbyet8K6IdWAu+5GO729vYm9oKplu8dQ/zvn0qE9fN0tPBfg97yqyyc9yFUTPUlCBDq/zwE8MJ5ePOQYFb3gqZY8fJKfvPn9mTzfLbu8dWkUvZ3WFzvr1zw8x1B/PCttiryJhy+9G092PLE/QT2Ocom84gadPAcForySo129F/9MPfxsGDvXbhO9+GJpvH1U9byjOLk9gOJIvG9N7bwIFxo9h5QMvcopYTsZEUU9YsLAvF1gHz0l5386NPMbPM9+njxrZye95gs4PIqZpzxGaaK7VmNNvac91LyIofA8qCOTvTkphLsLCr08e4AnO8e6Yrxh+1Y8fVT1vFmHPT3NbCY9T6SlvN5MkDztNEO8bCl9vCiUqDvHuuK8IWsdvKGQJL1i4ZU8GveDvW9swjy/GhA9AoQrufTHMbxWzbC8OqDLuzPCzruA4sg85K4xvePNBjz+yR69BtRUveUqDTwX/8y8i1v9PNqsxDxRASy7s5zHvGH71r13Eam8YuGVO54hJj15uT29fQlnvH9rAb0GanE7odsyPUmNEr1Lnwo7edgSPdQrzrtMFlK9fjq0OYtbfbyONF+7uPEEvJLu6zwXtL68cH66PKrLp7yDuyq9QmQHvEFfcz02uoU7Ct6DvCttijyVEly9DIaYvNRKozzmoVS9AvtyPUXO8bzDtUe9GOULPcOWcj0Q1sG8WmhovPR8o7y0goY72JrMO+wIir2K5LU8rCiuPLgdPrvtnqY8v0bJvAYf4zwCZda6LORRPCVRYzwIzIs8KqagvITnYz1ZW4Q81LQGO+1/UT3KKeG8T++zuao1i7yc8Ni8gC3XvAGjAL02mzC8HFSKvFrSSzy7YIM9HJ8YPMulvLzPMxC9GkISvX6FQjy810q7FtOTvVpo6LvIVZO8JZxxPTHPKz0tfwI9X1NCvbrkJ7296cK8nKVKPdyf5ztQZvu7/jOCPRrYrrxlmyI7tBijPOpbYbuOcgk8KXVTPOuMLj1hRuU8hFHHvI9/bbyh2zI8u2ADPT/1CL0AcjO9ta4/PG8C3zxvbEK9u6uRvAMABz3gqRY9TUefOpnM6Dwxz6s8N8dpvJb4mrx7yzW9junQvPwCtTwFPji8vEEuvQ393zvtYPw8re8XvccKBT2jopw81lwbvYoDCz0Oebs7KJQoPD3Eu7xZhz09LE41vSNeQDz+qsm7Kjy9PHImz7s9eS29Mw3dPL29Cb2oIxO94D+zu+2epjxvt9C8zi78vIdW4jx9c0q8oF/XvK6FtDzyH508ebm9vLjSL70jMoc8oBRJPVKpQD1as3a9SPLhPIfAxbscVAq7+BfbPPlIqDz5kzY8rhtRuhNFwLzNmN88fu+lvFAggTyktBQ8oPXzvFRwqryCqTK9wCf0u2Rq1bxBfsg7v5HXOihJGjy79h+9aNnTvLGKTzqsKK48Fx6iPAWomzwtypA8uPEEPZHhhzyyIGw8lq2Mu1XshTuSDcG79m9GPCoQhDqktJQ8Cwq9PAKEK72Mq586RmmiO9FS7LxnYgw9Gm7LvMFYQTt2dvi8Cwq9PB9ZJT3U/5S8u6uRvM4u/Ly/zwE9idI9POqrAz1hsMi8gcPzPHiNBL2OUzQ8h1ZivDitKD3U/xS9+BdbvN4BAry2C0Y7sakkPTscJ72vtgG91cHqvF2M2LyycI684ruOvKOinDs2m7A8Hvwevc23NLsXHiK9FcEbvE9ZFzyVmxS9V5SavDFlSLqZNsy8LycXvbjxBL1JQgQ6LC9gvPGJADqR4Qc9A0sVPNtU2bwhAbq90u0cvVvkw7y0ggY9D/UWvcop4bxWRHg85oL/vFjfqLuzBis8uDyTPYNwHLzdG0O8M1jrPOLnxzcMhpg7ca+HPBsEaD2E5+O8gC3XPIDiyLxd9rs85SoNvaPO1TwVDCq9ozi5O8QxI7wrbYo8W+TDPPDCFjwjfRU9PGc1PFgqNz30Pnm8T6SlPH0oPDxvAl+8qvdgvY4IpjzOLvy7JdobPVQlHL30qNw8AhrIOyHVgL38t6a7ELfsvMq//buniGI9w7VHuwregzwbBGg8N8dpvDyyw7poJOI8F0rbPBXBGz0jyKO6JbvGPAKEK70W05M9G7lZPSx67jxy+pW8F//MPJw7Z7qBExa9+zvLO3L6lTxGaaK73J/nu/GE7Lq5/mg8Rmkivc23NL3nHTC9MYSdvLO7nDw4rSg8rmbfO7QYozzC1Bw9LORRO/7JnjyaHIu852g+Pb/7Orz3zEy9wh8rvBG8ALyOcgk9FVe4vA2TfDxk1Dg8pGRyOlZjTTzDS+Q7XUHKvMsPIDyE5+M7MOlsvbIg7LwTGQe9s1E5PWvebr13xpo8JQbVPJpnmbvbcy49SMaovBr3A70NTYK7482GPHbgW7zHUP+74rsOPIxgkT0seu47ZjE/uQJl1rz0EkC9L3KlPBqNID1tDzy97Z6mPBpuSzwJYqg8FoiFvKPtKrw/i6W8EIuzPHJx3Toq8a67gRMWPZ2Gdb0K3gM9M5YVPJhVIbvsU5g72OXaufPmhjtMrO486eQZPLHV3Tw5dJI994G+uwbUVDytOqa8JuwTvdsooLyouS89yw+gO9u+PLwFPji90SYzPE86QryBExY9vb2Juk38kDqVx008v0ZJPGS14zseKFi8BT64uyN9FbxnQ7c8/CGKPJb4GrwAkYi8mL+EPI5yCT2EMnK89KhcOl9TwjswNPu7F//MvE9ZFzsJJP48hYIUPEwW0rwMOwo8axwZvR9ZpTtCr5U8Ca22PEU41TznhxO8PZiCPOqrAzwSM8g8zQJDu5aoeDwY5Qu8cibPO8Qxo7wi4mS8U4prvSEgjzpKIy89RYPjO3kjIT0+D0q8bosXu8vEETzBwqS8E2QVvVBmezxZppI8CikSvRMZBzu/sKw8WrN2PPMxFbx+hcI7WHVFPJM+Dr2WrYw8M5aVu8SbBjk2UCI9nYsJvaD1czun8sW8NzHNu2lVr7y6L7a8DNEmvW8C3zyDcBw9//XXOse6Yrz7pS69h8DFu/xsmLyW+Bo9Vq7bPLlOi704Fwy9QcnWvFC2nTvqq4M9SUIEvV3XZrx5T9o70SYzPYvF4DvBWEG8+BdbPOPI8rwRvIC7YsLAOr+R17wmoYW9kGWsvF1ByrzINj48ECHQvE2xgj2Pynu8qqzSvIU3hrvtYPw8BtRUPPegEz0GH2O7GRFFPTXUxrx9vtg8aI5FveUqjb3Gjim7q0cDPGDPnTuMqx+9RAycPLYqG7yFzSI74udHvdDbJLwU29w87FOYuxBs3rwpddO8Uz9du6qs0rwySwe9qG6hvP4UrbxyvGu8PXktvWuytbnhisG8HpI7vdBFiDzO4+28OoH2Oyrxrrqnp7c7SFzFPPGJALxIxqi7euV2Or9GSbzxz/o8i3pSvd3QtDqya3o8btalPPgX27xo2dM7QcnWPN/irLsoKkW8niEmPZEsljx7FkQ8zuiBOocLVLzCPoC8DuMePN6XnruZgVq8MRo6PewICryBw/M8X1PCvKRphjx5T9q6SdggvWDPHT2TiZw8g7sqvUF+yLso3za85XWbvRpuyzu9U6Y8eU/au2IsJLm0GCO9PyFCu4xgkTyLxWA8IUzIvOl6tjzarMQ8vb0JPTJLB7yFzSI9F//MOg1NAjxtDzy7DDuKPFgqtzyFghQ9nYuJPAKEKzsfWSU9PlrYu8cKhTspwOE7Pg9KO+cdsLv+yR68idK9PBzqpjwwU1A8tfnNPBtP9rx1tKI85uxiuz8hwrso/ou7DLLRu4tbfbw3x2m8hWM/vRACe7wvCEK93RvDuzhimrspC/A76qsDO3bgW70o37Y8oduyuySPDTz/i/S8SPLht29swrw4+DY9roW0PHma6Luu/Hu9TPd8PObsYjy/Rsk8ixDvPNT/FL0AcrM7UGuPuzqB9jzSOCu9ZQWGu2+3UDwmN6I8OBcMvZao+DzK3lK8x7piOyFMyDwpC3A8PvB0vGetGryrjX08ofqHPCWc8Tu4hyE70JAWvNyfZzwxOY+8sfQyPAyyUbrHuuK8OXQSvXQ4xzzjzYY8cFIBO4ffGr1SyBU9/0DmPObsYjwbuVm9VjeUvKfT8DyniGI7Gm5LO1mmkjt1tCI9Pg9KPSFMSDwajSA78rW5PDBTUD0hII88a2enPCMTMjxCZIe8KP4LPb3pwjzxOd681P+UvKuN/boM0Sa6p4jiPFPV+bvxiYA9v2UePbWuv7yHKqm7tWOxPGf4qLvZFqi7LORRPAMAh7yLEO88xMe/O4t60jpo2dM62768vAlD0zwUJms7ENbBu79GyTy7YIO8QvojPWuT4DvmoVS86RDTvM7ogbw9Lh88sT/BO4rkNTzcn2e8lZsUvKqAGTwHupM7KhCEvKce/7ssmcM8xHwxvOvXPD28Ilm8SKfTvKphxLrmN3G8lviaO+l6NrwhII88vLh1PI5yCTwekru8g7sqvEKvlTzO4+28gqkyOywDp71TP128CfhEPGTzjbxSqUA7VmNNvdGdejzr9pE8pGkGPFmHPTtas3Y8qoCZPCNewDxGaSI8ofoHvaMMAD0945A8Q0WyO/MxlTzBwqQ8h8DFOdh7dzqh2zI8DLJRPJ4hJj3HCgU86qbvPCMyhzySDcG7nrdCvIoDizullb+8E5DOvAbU1Lzmgv+8niGmvPdVhTxuIbQ8DpiQPD5aWLyLetK7As85PHAzrLqPz487tsA3PTWJuLsXlWm2UGuPvGkKITuxEwg93O8JvMAndDwM0SY8ejWZO+PNhjzDS+S8AaMAPO77LDx5BEy7gcgHPThDRbzJ/ac8nA8uvUEU5Tuzu5y87RVuu1bNsDwl5/87a0jSuzDp7LxXSQw8HWaCPDCeXj2ATCy9pLQUPFAggbw3fNu87FMYO/SoXLxPOkI93gECvNkWKLysKC69dbQivWFG5bqjohy7IWsdvCoQhDwP9Ra8xtk3vGLCQD2810o8Bh9jPG5AiTxbuAq9zxS7OyVR47uCXqS8vIw8vEinUzxsLpE8oBRJvJUS3DwQQKW8nYZ1PIsvRDwcVAo8TKzuux5Hrbw9xLs7sROIPFtOJ7wtypA9a95uPETBjTxPWRe8IUxIPFh1Rb2cDy68b2xCvNzqdTmQGp68B7oTPV4nCT3+MwI917mhPGtIUj1zB3o8LJnDu1J9hzyVXeq88h8dOYCXuryDJY68T9BevPegEzzr9hG9fjo0uwp0IL21Y7G8CBcaPX46tDvtyt+8VvlpPGLCwLxdFZE83kwQvN/DV7yrkpE85BiVvKAUyTsa9wO8KqYgPImHrzzz5ga9Dnm7utmACzwKdCA9btalvBpuS7oG1NS8ditqvJAanjygqmU8Bh9jPItb/byJhy+9KVb+vDkphLwhTMi8vNfKvM7jbbs3fNs8SKdTPFbNMLv5/Rk82WG2u90bwziaHIs6kSwWPVjfqLviu4670jgrvY40Xz1eJwm9tBgjvEEzurbbVFk8sdVdPJ/oj7ucWry8S+oYvRBApbz9TUM7nA8uvCx67jxZW4S7YISPvMFYQTvYe3c83RtDvMsPoDzy1I68T6SlvNKiDjwlUeM8aaA9vcSbBj2ByAe8PXktPUFfc7zKv/08433kvPuGWb2M9q27qqzSPJUxsbqDBjm8lV1qOqODx7vYe/c8/l+7O/s7S7ovciW9iOz+Or29CbtbuIo91Qx5PKMZZDzUdty71Qz5PKBf17uSWM+8HpK7O+QYlbzZFig8h1biPIjxkjuYCpO8kLA6PAYfYzsDAIe7fqQXPUwW0jyRd6Q7r5esvClW/jyVEtw7VkT4OslnCzwQt+y8PqVmO/IfHTxlm6I7w7VHu4js/jyaZxm81qcpPCaCsLzDS+Q7LHruO3rldr3uGgI9aLp+PA4uLTz0XU688A0lO0ZpojyR4Ye67WB8PK+2AT0o/gu8snAOPV32uzpRASy9Ae4OvH6klz1xr4e87WD8O1bNsDvZgIu88O7PvExh4LsqpiA9IpfWvKzdnzuHdbc8oF9XvJocCz15bi886qbvPB5z5jy79p+8N3zbPLVE3DpFg+M7EIszOw39XzwRnas8yWcLPOkvqLzZyxm82748u7n+6LxlBQa9divqPGVQFDyBExa8EAJ7PM4u/DyHC9S8290RvSl10zrKSLa8+ZO2PKzdn7wTGYc8LMX8PKHbMrzZgAu8DUhuPKCqZT31JDg8KXXTvJKjXbxSXjI7iuQ1PF1gn7uMQTy8RYPjPCHVgLxpCqE8MywyvGve7ryR4Qe833hJPMq/fTvmN3G7ArDkuk/Q3jznh5M8DNGmPJVdajvsUxi8XUFKPEa0MLwtf4I6tWMxvO1/UbzBWME6vEGuPDuGCj2OvRc9c1ecu04OiTxSXrI7F2mwvP4zArxrHJm8flmJvImHrzkxZUi9Ul6yvJwuA7vWp6m8YDmBOxUMKj2eIaY8VNoNvO6wHryWrQw8px7/Os0CQ7tas3a7ozi5O64b0bkQbF48cvqVvInSvTssA6c8y3kDOkb/PjtT9E69iy/EPP/1V7cIFxq8E2SVPKjYhDkXSts8/n6QPLtgAz3tnia8UsgVPOc8hbyyJYC9uU4LPXTtOLttxK08/AI1Pe3ptDsfDhc84KkWPSqmILz0x7E8YncyPPTz6rmjg0e87kY7O3bg2zyHC9Q8yt7SPADclryt7xe78rU5vKfyRTw3x+k8lRLcO04OCT0J+ES8ExmHu+eHkzscNbU8idI9ubxBrjx5BEw8ENZBvMDc5Ty4aEw8YDkBPLIg7Lwg78G8u6uRu7g8kzwe3cm8BagbvIcLVDmE5+M7A5ajO8THvzy2Khu9ui+2PNFS7DxlUJQ7NgWUOxDWwbxlUJQ6nKXKvIimBDyZF/e8UqlAvNLtnDzS7Zw7F//MvKRphrxSfQe8D6oIO0JkB73jyPK7B5s+vOPI8rxo2VM8nC6DPB+ks7waQhK8OgovvCttCj3wdwi9292RuwJl1rocVIq8uLPavB9ZJT24s9o8MOlsPfB3iLz+qsk7mkjEvJXHzbyzBiu8E2SVPLVEXLz6WqC7o1eOO11BSjz3NjA99F3OPJ/oD7wc6qY8raQJPTHuAL2Takc8BT64u+YLuDuATCw97crfOh7dyTulSrE6hwvUu5XHzbtSXrI7cpAyvPscdj1rHBk8uPEEvQw7Crg4Fwy9df+wOjHugDxiwkC9tBiju7n+6DzPyaw7o87VvEs1p7xgOQG9YZHzvAByM71RTLo6ND4qvd5MkDyOCKa7UQGsu2AarDviu468gJc6PYB4ZbzldZu8rwEQvIrktbtdjNi8PqVmuxMZB70il9Y8wtScvLiz2rtTP90617mhuVZE+Lyn03A7ZVAUOwdvhTyZ6z285zwFPbVjsTwevnS8F7Q+PKRk8ry4hyE9OuvZu4CXurzHCoU8w5Zyu70ImLrbCUs86S8oPJAanjxs4wI9tIIGPHYr6rwVokY80NskvJx5Eb0Xlek8zSGYPISc1bx3p8W8b03tvKuSEblrSNI8xiRGPAliKDvinLk80NukvN6XHr1WRHi8AwCHu44IpjzzMRW7r7YBPKOiHL1d1+Y8l0MpPdgEMD2Jh688AmXWPAreA70fw4g8I32VPPVDDbztyt+7Wh3aOy3KELxGtDA9jlO0u+aC/7wpdVM9F2mwu70IGLzEx7+8GDAau6X/Ir240i+8phGbPAOWIz2Z6z09DBw1vE2xAj25Sfe8xvgMPbNRubrG+Ay8ZjE/PZ/oD7waI708uB2+vEjGqLyycA69D6oIPHImzzw0Pio9wQ2zPGiOxTy/+zo9dkq/PP4zArxSXjI8T+8zPYBMrLzC1Jw7ndYXPP4UrbukZPI8E6+jPJ/oj7qSWE+7QTM6PXMH+jyZ6z29Go0gvc4u/DuPf206duDbvGiOxbsXSls8xkMbPXmaaDw0qA29OjboPB78nju/sCy8CUNTPKGQpLyqgBm9nFo8PLg8k7yjODm8PvD0PPegE7xT1Xk89SQ4PTYFlLtKbj08TGHgu1qzdjyrRwO8\"\n + \ }\n ],\n \"model\": \"text-embedding-3-small\",\n \"usage\": {\n \"prompt_tokens\": + 12,\n \"total_tokens\": 12\n }\n}\n" + headers: + CF-RAY: + - 92606d69df737e05-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 25 Mar 2025 18:21:21 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=YQe0r6xlg5bRl1wJ70dt0Aocti_r13sABgw2peP46Yw-1742926881-1.0.1.1-.p2IX5HrpoSy4WAMkQFz0iswmLdbuLJl2rLIWZkOOdUZ3jUTwTTGdAZqO8N084.xjQYo12Qj_tSEQnzCcc4a8DtoXIRULYMPRzIPeTezIkU; + path=/; expires=Tue, 25-Mar-25 18:51:21 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=T7Hv3cCn64SAlcAT1xFBTjlHSm.Ut3gTDw3SwYO5H9o-1742926881514-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-3-small + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '111' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-678fbc785b-244c7 + x-envoy-upstream-service-time: + - '82' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999986' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_eec876b36a4e41890b2123c7595d82bf + http_version: HTTP/1.1 + status_code: 200 +- request: + body: '{"input": ["What is Brandon''s favorite color? This is the expected criteria + for your final answer: Brandon''s favorite color. you MUST return the actual + complete content as the final answer, not a summary."], "model": "text-embedding-3-small", + "encoding_format": "base64"}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '272' + content-type: + - application/json + cookie: + - __cf_bm=YQe0r6xlg5bRl1wJ70dt0Aocti_r13sABgw2peP46Yw-1742926881-1.0.1.1-.p2IX5HrpoSy4WAMkQFz0iswmLdbuLJl2rLIWZkOOdUZ3jUTwTTGdAZqO8N084.xjQYo12Qj_tSEQnzCcc4a8DtoXIRULYMPRzIPeTezIkU; + _cfuvid=T7Hv3cCn64SAlcAT1xFBTjlHSm.Ut3gTDw3SwYO5H9o-1742926881514-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.68.2 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.68.2 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/embeddings + response: + content: "{\n \"object\": \"list\",\n \"data\": [\n {\n \"object\": + \"embedding\",\n \"index\": 0,\n \"embedding\": \"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\"\n + \ }\n ],\n \"model\": \"text-embedding-3-small\",\n \"usage\": {\n \"prompt_tokens\": + 39,\n \"total_tokens\": 39\n }\n}\n" + headers: + CF-RAY: + - 92606d71fadd7e05-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 25 Mar 2025 18:21:22 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-allow-origin: + - '*' + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-model: + - text-embedding-3-small + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '176' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + via: + - envoy-router-84d4976dd6-kn9b2 + x-envoy-upstream-service-time: + - '76' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '10000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '9999951' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_4a397f4ae14d9fcb88333d9ecb5be969 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: !!binary | + CocLCiQKIgoMc2VydmljZS5uYW1lEhIKEGNyZXdBSS10ZWxlbWV0cnkS3goKEgoQY3Jld2FpLnRl + bGVtZXRyeRK2CAoQro1thsfReS7yOp6MTxegrxIItR6JoTTghDkqDENyZXcgQ3JlYXRlZDABOZgk + XbC/HjAYQdgJurC/HjAYShsKDmNyZXdhaV92ZXJzaW9uEgkKBzAuMTA4LjBKGgoOcHl0aG9uX3Zl + cnNpb24SCAoGMy4xMi45Si4KCGNyZXdfa2V5EiIKIDU1MjczOGJmMDQwZTcxZGEyMjJmOWQzNjU1 + MjIzMjdjSjEKB2NyZXdfaWQSJgokMmUzMGJhOWQtZWNkOS00MDg5LTk5YTctMGIwYTE0ODk5ODdh + ShwKDGNyZXdfcHJvY2VzcxIMCgpzZXF1ZW50aWFsShEKC2NyZXdfbWVtb3J5EgIQAEoaChRjcmV3 + X251bWJlcl9vZl90YXNrcxICGAFKGwoVY3Jld19udW1iZXJfb2ZfYWdlbnRzEgIYAUqaAwoLY3Jl + d19hZ2VudHMSigMKhwNbeyJrZXkiOiAiYTk2YTQyMjM1Y2U0M2RiZDgwNzc0ZWIyODhhNzM3MzUi + LCAiaWQiOiAiZjU5NjZlYTktODk2Zi00MDRmLWIwOGUtZDk1MWI4OWNmZTM3IiwgInJvbGUiOiAi + SW5mb3JtYXRpb24gQWdlbnQgd2l0aCBleHRlbnNpdmUgcm9sZSBkZXNjcmlwdGlvbiB0aGF0IGlz + IGxvbmdlciB0aGFuIDgwIGNoYXJhY3RlcnMiLCAidmVyYm9zZT8iOiBmYWxzZSwgIm1heF9pdGVy + IjogMjUsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJsbG0i + OiAiZ3B0LTRvLW1pbmkiLCAiZGVsZWdhdGlvbl9lbmFibGVkPyI6IGZhbHNlLCAiYWxsb3dfY29k + ZV9leGVjdXRpb24/IjogZmFsc2UsICJtYXhfcmV0cnlfbGltaXQiOiAyLCAidG9vbHNfbmFtZXMi + OiBbXX1dSsgCCgpjcmV3X3Rhc2tzErkCCrYCW3sia2V5IjogIjg2ZmU1NTY3ZDFmNDFiMWY4NDQ1 + ZTRmOGQ0YmY0MGU2IiwgImlkIjogImM1ZTU3MTcwLWFkZWQtNDNkNS1iZTE3LTZhZDliM2ZjM2U3 + NCIsICJhc3luY19leGVjdXRpb24/IjogZmFsc2UsICJodW1hbl9pbnB1dD8iOiBmYWxzZSwgImFn + ZW50X3JvbGUiOiAiSW5mb3JtYXRpb24gQWdlbnQgd2l0aCBleHRlbnNpdmUgcm9sZSBkZXNjcmlw + dGlvbiB0aGF0IGlzIGxvbmdlciB0aGFuIDgwIGNoYXJhY3RlcnMiLCAiYWdlbnRfa2V5IjogImE5 + NmE0MjIzNWNlNDNkYmQ4MDc3NGViMjg4YTczNzM1IiwgInRvb2xzX25hbWVzIjogW119XXoCGAGF + AQABAAASjgIKELNeNWDbp5Ua0wTFrxxeOrASCIhmnaGTrxBNKgxUYXNrIENyZWF0ZWQwATkQlzso + wB4wGEE4Kz4owB4wGEouCghjcmV3X2tleRIiCiA1NTI3MzhiZjA0MGU3MWRhMjIyZjlkMzY1NTIy + MzI3Y0oxCgdjcmV3X2lkEiYKJDJlMzBiYTlkLWVjZDktNDA4OS05OWE3LTBiMGExNDg5OTg3YUou + Cgh0YXNrX2tleRIiCiA4NmZlNTU2N2QxZjQxYjFmODQ0NWU0ZjhkNGJmNDBlNkoxCgd0YXNrX2lk + EiYKJGM1ZTU3MTcwLWFkZWQtNDNkNS1iZTE3LTZhZDliM2ZjM2U3NHoCGAGFAQABAAA= + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '1418' + Content-Type: + - application/x-protobuf + User-Agent: + - OTel-OTLP-Exporter-Python/1.31.1 + method: POST + uri: https://telemetry.crewai.com:4319/v1/traces + response: + body: + string: "\n\0" + headers: + Content-Length: + - '2' + Content-Type: + - application/x-protobuf + Date: + - Tue, 25 Mar 2025 18:21:24 GMT + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Information Agent + with extensive role description that is longer than 80 characters. You have + access to specific knowledge sources.\nYour personal goal is: Provide information + based on knowledge sources\nTo give my best complete final answer to the task + respond using the exact following format:\n\nThought: I now can give a great + answer\nFinal Answer: Your final answer must be the great and the most complete + as possible, it must be outcome described.\n\nI MUST use these formats, my job + depends on it!"}, {"role": "user", "content": "\nCurrent Task: What is Brandon''s + favorite color?\n\nThis is the expected criteria for your final answer: Brandon''s + favorite color.\nyou MUST return the actual complete content as the final answer, + not a summary.Additional Information: Brandon''s favorite color is red and he + likes Mexican food.\n\nBegin! This is VERY important to you, use the tools available + and give your best Final Answer, your job depends on it!\n\nThought:"}], "model": + "gpt-4o-mini", "stop": ["\nObservation:"]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '1074' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.68.2 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.68.2 + x-stainless-raw-response: + - 'true' + x-stainless-read-timeout: + - '600.0' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-BF3Br9QWbmiaKiPLFd5URBfj1B7NQ\",\n \"object\": + \"chat.completion\",\n \"created\": 1742926883,\n \"model\": \"gpt-4o-mini-2024-07-18\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"I now can give a great answer \\nFinal + Answer: Brandon's favorite color is red.\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 194,\n \"completion_tokens\": + 19,\n \"total_tokens\": 213,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_27322b4e16\"\n}\n" + headers: + CF-RAY: + - 92606d78cdcb7def-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Tue, 25 Mar 2025 18:21:24 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=akSDYPP.eTuyDBep9apt00XQn2By0q4quUKYKaowxB4-1742926884-1.0.1.1-nmj4tC9iquLz9Y4C_Lm9AYbMb7_yjKru3.wztYGzcO7o4_kIFqmjYjAAdLL2ZOWQUXzhWiH_XRvDTY94ubficIUm7WB.5o4CQ41GRGDc6c0; + path=/; expires=Tue, 25-Mar-25 18:51:24 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=eJkVy2yBJBQb66LFc5ao3Y_Xwek6ZYZdKM7l5_pxS_E-1742926884663-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '1351' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999765' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_126a3481ff4c4d9e8e75ed2dacbe3719 + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/utilities/test_chromadb_utils.py b/tests/utilities/test_chromadb_utils.py new file mode 100644 index 000000000..9035562af --- /dev/null +++ b/tests/utilities/test_chromadb_utils.py @@ -0,0 +1,81 @@ +import unittest +from typing import Any, Dict, List, Union + +import pytest + +from crewai.utilities.chromadb import ( + MAX_COLLECTION_LENGTH, + MIN_COLLECTION_LENGTH, + is_ipv4_pattern, + sanitize_collection_name, +) + + +class TestChromadbUtils(unittest.TestCase): + def test_sanitize_collection_name_long_name(self): + """Test sanitizing a very long collection name.""" + long_name = "This is an extremely long role name that will definitely exceed the ChromaDB collection name limit of 63 characters and cause an error when used as a collection name" + sanitized = sanitize_collection_name(long_name) + self.assertLessEqual(len(sanitized), MAX_COLLECTION_LENGTH) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) + + def test_sanitize_collection_name_special_chars(self): + """Test sanitizing a name with special characters.""" + special_chars = "Agent@123!#$%^&*()" + sanitized = sanitize_collection_name(special_chars) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) + + def test_sanitize_collection_name_short_name(self): + """Test sanitizing a very short name.""" + short_name = "A" + sanitized = sanitize_collection_name(short_name) + self.assertGreaterEqual(len(sanitized), MIN_COLLECTION_LENGTH) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + + def test_sanitize_collection_name_bad_ends(self): + """Test sanitizing a name with non-alphanumeric start/end.""" + bad_ends = "_Agent_" + sanitized = sanitize_collection_name(bad_ends) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + + def test_sanitize_collection_name_none(self): + """Test sanitizing a None value.""" + sanitized = sanitize_collection_name(None) + self.assertEqual(sanitized, "default_collection") + + def test_sanitize_collection_name_ipv4_pattern(self): + """Test sanitizing an IPv4 address.""" + ipv4 = "192.168.1.1" + sanitized = sanitize_collection_name(ipv4) + self.assertTrue(sanitized.startswith("ip_")) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) + self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) + + def test_is_ipv4_pattern(self): + """Test IPv4 pattern detection.""" + self.assertTrue(is_ipv4_pattern("192.168.1.1")) + self.assertFalse(is_ipv4_pattern("not.an.ip.address")) + + def test_sanitize_collection_name_properties(self): + """Test that sanitized collection names always meet ChromaDB requirements.""" + test_cases = [ + "A" * 100, # Very long name + "_start_with_underscore", + "end_with_underscore_", + "contains@special#characters", + "192.168.1.1", # IPv4 address + "a" * 2, # Too short + ] + for test_case in test_cases: + sanitized = sanitize_collection_name(test_case) + self.assertGreaterEqual(len(sanitized), MIN_COLLECTION_LENGTH) + self.assertLessEqual(len(sanitized), MAX_COLLECTION_LENGTH) + self.assertTrue(sanitized[0].isalnum()) + self.assertTrue(sanitized[-1].isalnum()) diff --git a/tests/utilities/test_string_utils.py b/tests/utilities/test_string_utils.py index 2e2cf2e0c..441aae8c0 100644 --- a/tests/utilities/test_string_utils.py +++ b/tests/utilities/test_string_utils.py @@ -1,14 +1,8 @@ -import unittest from typing import Any, Dict, List, Union import pytest -from crewai.utilities import is_ipv4_pattern, sanitize_collection_name -from crewai.utilities.string_utils import ( - MAX_LENGTH, - MIN_LENGTH, - interpolate_only, -) +from crewai.utilities.string_utils import interpolate_only class TestInterpolateOnly: @@ -191,77 +185,3 @@ class TestInterpolateOnly: interpolate_only(template, inputs) assert "inputs dictionary cannot be empty" in str(excinfo.value).lower() - - -class TestStringUtils(unittest.TestCase): - def test_sanitize_collection_name_long_name(self): - """Test sanitizing a very long collection name.""" - long_name = "This is an extremely long role name that will definitely exceed the ChromaDB collection name limit of 63 characters and cause an error when used as a collection name" - sanitized = sanitize_collection_name(long_name) - self.assertLessEqual(len(sanitized), MAX_LENGTH) - self.assertTrue(sanitized[0].isalnum()) - self.assertTrue(sanitized[-1].isalnum()) - self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) - - def test_sanitize_collection_name_special_chars(self): - """Test sanitizing a name with special characters.""" - special_chars = "Agent@123!#$%^&*()" - sanitized = sanitize_collection_name(special_chars) - self.assertTrue(sanitized[0].isalnum()) - self.assertTrue(sanitized[-1].isalnum()) - self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) - - def test_sanitize_collection_name_short_name(self): - """Test sanitizing a very short name.""" - short_name = "A" - sanitized = sanitize_collection_name(short_name) - self.assertGreaterEqual(len(sanitized), MIN_LENGTH) - self.assertTrue(sanitized[0].isalnum()) - self.assertTrue(sanitized[-1].isalnum()) - - def test_sanitize_collection_name_bad_ends(self): - """Test sanitizing a name with non-alphanumeric start/end.""" - bad_ends = "_Agent_" - sanitized = sanitize_collection_name(bad_ends) - self.assertTrue(sanitized[0].isalnum()) - self.assertTrue(sanitized[-1].isalnum()) - - def test_sanitize_collection_name_none(self): - """Test sanitizing a None value.""" - sanitized = sanitize_collection_name(None) - self.assertEqual(sanitized, "default_collection") - - def test_sanitize_collection_name_ipv4_pattern(self): - """Test sanitizing an IPv4 address.""" - ipv4 = "192.168.1.1" - sanitized = sanitize_collection_name(ipv4) - self.assertTrue(sanitized.startswith("ip_")) - self.assertTrue(sanitized[0].isalnum()) - self.assertTrue(sanitized[-1].isalnum()) - self.assertTrue(all(c.isalnum() or c in ["_", "-"] for c in sanitized)) - - def test_is_ipv4_pattern(self): - """Test IPv4 pattern detection.""" - self.assertTrue(is_ipv4_pattern("192.168.1.1")) - self.assertFalse(is_ipv4_pattern("not.an.ip.address")) - - def test_sanitize_collection_name_properties(self): - """Test that sanitized collection names always meet ChromaDB requirements.""" - test_cases = [ - "A" * 100, # Very long name - "_start_with_underscore", - "end_with_underscore_", - "contains@special#characters", - "192.168.1.1", # IPv4 address - "a" * 2, # Too short - ] - for test_case in test_cases: - sanitized = sanitize_collection_name(test_case) - self.assertGreaterEqual(len(sanitized), MIN_LENGTH) - self.assertLessEqual(len(sanitized), MAX_LENGTH) - self.assertTrue(sanitized[0].isalnum()) - self.assertTrue(sanitized[-1].isalnum()) - - -if __name__ == "__main__": - unittest.main() From 6c003e0382b62026d5b00da5fb3c5942d7d5db5f Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 25 Mar 2025 20:31:22 +0000 Subject: [PATCH 18/41] Address PR comment: Move import to top level in knowledge_storage.py Co-Authored-By: Joe Moura --- src/crewai/knowledge/storage/knowledge_storage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/crewai/knowledge/storage/knowledge_storage.py b/src/crewai/knowledge/storage/knowledge_storage.py index 37b22ed24..e23b9e120 100644 --- a/src/crewai/knowledge/storage/knowledge_storage.py +++ b/src/crewai/knowledge/storage/knowledge_storage.py @@ -14,6 +14,7 @@ from chromadb.config import Settings from crewai.knowledge.storage.base_knowledge_storage import BaseKnowledgeStorage from crewai.utilities import EmbeddingConfigurator +from crewai.utilities.chromadb import sanitize_collection_name from crewai.utilities.constants import KNOWLEDGE_DIRECTORY from crewai.utilities.logger import Logger from crewai.utilities.paths import db_storage_path @@ -98,8 +99,6 @@ class KnowledgeStorage(BaseKnowledgeStorage): else "knowledge" ) if self.app: - from crewai.utilities.chromadb import sanitize_collection_name - self.collection = self.app.get_or_create_collection( name=sanitize_collection_name(collection_name), embedding_function=self.embedder, From c23e8fbb02589fde1822280e4e0d08379473a74a Mon Sep 17 00:00:00 2001 From: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> Date: Wed, 26 Mar 2025 08:16:09 -0700 Subject: [PATCH 19/41] Refactor type hints and clean up imports in crew.py (#2480) - Removed unused import of BaseTool from langchain_core.tools. - Updated type hints in crew.py to streamline code and improve readability. - Cleaned up whitespace for better code formatting. Co-authored-by: Brandon Hancock (bhancock_ai) <109994880+bhancockio@users.noreply.github.com> --- src/crewai/crew.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index c82ff309f..c54d50425 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -6,9 +6,8 @@ import warnings from concurrent.futures import Future from copy import copy as shallow_copy from hashlib import md5 -from typing import Any, Callable, Dict, List, Optional, Set, Tuple, TypeVar, Union, cast +from typing import Any, Callable, Dict, List, Optional, Set, Tuple, Union, cast -from langchain_core.tools import BaseTool as LangchainBaseTool from pydantic import ( UUID4, BaseModel, @@ -490,7 +489,7 @@ class Crew(BaseModel): task.key for task in self.tasks ] return md5("|".join(source).encode(), usedforsecurity=False).hexdigest() - + @property def fingerprint(self) -> Fingerprint: """ From e1b83942650d36f66d3b3a67ef4d9da13bbf82d9 Mon Sep 17 00:00:00 2001 From: "Brandon Hancock (bhancock_ai)" <109994880+bhancockio@users.noreply.github.com> Date: Wed, 26 Mar 2025 11:25:10 -0400 Subject: [PATCH 20/41] Fixed (#2481) Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> --- src/crewai/utilities/events/utils/console_formatter.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/crewai/utilities/events/utils/console_formatter.py b/src/crewai/utilities/events/utils/console_formatter.py index 3d3e03149..618238ec3 100644 --- a/src/crewai/utilities/events/utils/console_formatter.py +++ b/src/crewai/utilities/events/utils/console_formatter.py @@ -507,9 +507,10 @@ class ConsoleFormatter: # Remove the thinking status node when complete if "Thinking" in str(tool_branch.label): - agent_branch.children.remove(tool_branch) - self.print(crew_tree) - self.print() + if tool_branch in agent_branch.children: + agent_branch.children.remove(tool_branch) + self.print(crew_tree) + self.print() def handle_llm_call_failed( self, tool_branch: Optional[Tree], error: str, crew_tree: Optional[Tree] @@ -587,6 +588,7 @@ class ConsoleFormatter: for child in flow_tree.children: if "Running tests" in str(child.label): child.label = Text("✅ Tests completed successfully", style="green") + break self.print(flow_tree) self.print() From 50fe5080e6f10b8cdf56079ffe2c6542a4ce5779 Mon Sep 17 00:00:00 2001 From: Tony Kipkemboi Date: Wed, 26 Mar 2025 11:28:02 -0400 Subject: [PATCH 21/41] docs: update theme to mint and modify opik observability doc --- docs/docs.json | 2 +- docs/how-to/opik-observability.mdx | 87 ++++++++++++++++-------------- 2 files changed, 47 insertions(+), 42 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index cced352cf..dc3acdaa6 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -1,6 +1,6 @@ { "$schema": "https://mintlify.com/docs.json", - "theme": "palm", + "theme": "mint", "name": "CrewAI", "colors": { "primary": "#EB6658", diff --git a/docs/how-to/opik-observability.mdx b/docs/how-to/opik-observability.mdx index a1d128b8f..b87c1513d 100644 --- a/docs/how-to/opik-observability.mdx +++ b/docs/how-to/opik-observability.mdx @@ -8,6 +8,10 @@ icon: meteor With [Comet Opik](https://www.comet.com/docs/opik/), debug, evaluate, and monitor your LLM applications, RAG systems, and agentic workflows with comprehensive tracing, automated evaluations, and production-ready dashboards. + + Opik agent monitoring example with CrewAI + + Opik provides comprehensive support for every stage of your CrewAI application development: - **Log Traces and Spans**: Automatically track LLM calls and application logic to debug and analyze development and production systems. Manually or programmatically annotate, view, and compare responses across projects. @@ -27,7 +31,7 @@ For this guide we will use CrewAI’s quickstart example. ```shell - %pip install crewai crewai-tools opik --upgrade + pip install crewai crewai-tools opik --upgrade ``` @@ -35,8 +39,10 @@ For this guide we will use CrewAI’s quickstart example. import opik opik.configure(use_local=False) ``` + First, we set up our API keys for our LLM-provider as environment variables: + ```python import os import getpass @@ -47,53 +53,56 @@ For this guide we will use CrewAI’s quickstart example. The first step is to create our project. We will use an example from CrewAI’s documentation: + ```python from crewai import Agent, Crew, Task, Process -class YourCrewName: - def agent_one(self) -> Agent: - return Agent( - role="Data Analyst", - goal="Analyze data trends in the market", - backstory="An experienced data analyst with a background in economics", - verbose=True, - ) + class YourCrewName: + def agent_one(self) -> Agent: + return Agent( + role="Data Analyst", + goal="Analyze data trends in the market", + backstory="An experienced data analyst with a background in economics", + verbose=True, + ) - def agent_two(self) -> Agent: - return Agent( - role="Market Researcher", - goal="Gather information on market dynamics", - backstory="A diligent researcher with a keen eye for detail", - verbose=True, - ) + def agent_two(self) -> Agent: + return Agent( + role="Market Researcher", + goal="Gather information on market dynamics", + backstory="A diligent researcher with a keen eye for detail", + verbose=True, + ) - def task_one(self) -> Task: - return Task( - name="Collect Data Task", - description="Collect recent market data and identify trends.", - expected_output="A report summarizing key trends in the market.", - agent=self.agent_one(), - ) + def task_one(self) -> Task: + return Task( + name="Collect Data Task", + description="Collect recent market data and identify trends.", + expected_output="A report summarizing key trends in the market.", + agent=self.agent_one(), + ) - def task_two(self) -> Task: - return Task( - name="Market Research Task", - description="Research factors affecting market dynamics.", - expected_output="An analysis of factors influencing the market.", - agent=self.agent_two(), - ) + def task_two(self) -> Task: + return Task( + name="Market Research Task", + description="Research factors affecting market dynamics.", + expected_output="An analysis of factors influencing the market.", + agent=self.agent_two(), + ) - def crew(self) -> Crew: - return Crew( - agents=[self.agent_one(), self.agent_two()], - tasks=[self.task_one(), self.task_two()], - process=Process.sequential, - verbose=True, - ) + def crew(self) -> Crew: + return Crew( + agents=[self.agent_one(), self.agent_two()], + tasks=[self.task_one(), self.task_two()], + process=Process.sequential, + verbose=True, + ) ``` + Now we can import Opik’s tracker and run our crew: + ```python from opik.integrations.crewai import track_crewai @@ -109,10 +118,6 @@ class YourCrewName: - Agent interactions and task execution flow - Performance metrics like latency and token usage - Evaluation metrics (built-in or custom) - - - Opik agent monitoring example with CrewAI - From 6145331ee48ecff2e4c735dffea5c02f96f6e190 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 27 Mar 2025 00:12:38 +0530 Subject: [PATCH 22/41] Added test cases mentioned in the issue --- src/crewai/crew.py | 2 -- tests/crew_test.py | 51 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 78429468b..0f6db8c4a 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -1224,8 +1224,6 @@ class Crew(BaseModel): return copied_crew - - def _set_tasks_callbacks(self) -> None: """Sets callback for every task suing task_callback""" for task in self.tasks: diff --git a/tests/crew_test.py b/tests/crew_test.py index 39a3e9a08..bc137a214 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -11,7 +11,9 @@ import pydantic_core import pytest from crewai.agent import Agent +from crewai.agents import CacheHandler from crewai.agents.cache import CacheHandler +from crewai.agents.crew_agent_executor import CrewAgentExecutor from crewai.crew import Crew from crewai.crews.crew_output import CrewOutput from crewai.knowledge.source.string_knowledge_source import StringKnowledgeSource @@ -4025,3 +4027,52 @@ def test_crew_with_knowledge_sources_works_with_copy(): assert len(crew_copy.tasks) == len(crew.tasks) assert len(crew_copy.tasks) == len(crew.tasks) + + +def test_crew_kickoff_for_each_works_with_manager_agent_copy(): + researcher = Agent( + role="Researcher", + goal="Conduct thorough research and analysis on AI and AI agents", + backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", + allow_delegation=False + ) + + writer = Agent( + role="Senior Writer", + goal="Create compelling content about AI and AI agents", + backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", + allow_delegation=False + ) + + # Define task + task = Task( + description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", + expected_output="5 bullet points, each with a paragraph and accompanying notes.", + ) + + # Define manager agent + manager = Agent( + role="Project Manager", + goal="Efficiently manage the crew and ensure high-quality task completion", + backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", + allow_delegation=True + ) + + # Instantiate crew with a custom manager + crew = Crew( + agents=[researcher, writer], + tasks=[task], + manager_agent=manager, + process=Process.hierarchical, + verbose=True + ) + + crew_copy = crew.copy() + assert crew_copy.manager_agent is not None + assert crew_copy.manager_agent.id != crew.manager_agent.id + assert crew_copy.manager_agent.role == crew.manager_agent.role + assert crew_copy.manager_agent.goal == crew.manager_agent.goal + assert crew_copy.manager_agent.backstory == crew.manager_agent.backstory + assert isinstance(crew_copy.manager_agent.agent_executor, CrewAgentExecutor) + assert isinstance(crew_copy.manager_agent.cache_handler, CacheHandler) + From 49b8cc95ae6a76af37f39e9b85c1f85d4a3e222b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:11:06 +0000 Subject: [PATCH 23/41] fix: update LLMCallStartedEvent message type to support multimodal content (#2475) fix: sort imports in test file to fix linting fix: properly sort imports with ruff Co-Authored-By: Joe Moura --- src/crewai/utilities/events/llm_events.py | 2 +- tests/test_multimodal_validation.py | 46 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 tests/test_multimodal_validation.py diff --git a/src/crewai/utilities/events/llm_events.py b/src/crewai/utilities/events/llm_events.py index 988b6f945..10a648e86 100644 --- a/src/crewai/utilities/events/llm_events.py +++ b/src/crewai/utilities/events/llm_events.py @@ -15,7 +15,7 @@ class LLMCallStartedEvent(CrewEvent): """Event emitted when a LLM call starts""" type: str = "llm_call_started" - messages: Union[str, List[Dict[str, str]]] + messages: Union[str, List[Dict[str, Any]]] tools: Optional[List[dict]] = None callbacks: Optional[List[Any]] = None available_functions: Optional[Dict[str, Any]] = None diff --git a/tests/test_multimodal_validation.py b/tests/test_multimodal_validation.py new file mode 100644 index 000000000..3b0817bf2 --- /dev/null +++ b/tests/test_multimodal_validation.py @@ -0,0 +1,46 @@ +import os + +import pytest + +from crewai import LLM, Agent, Crew, Task + + +@pytest.mark.skip(reason="Only run manually with valid API keys") +def test_multimodal_agent_with_image_url(): + """ + Test that a multimodal agent can process images without validation errors. + This test reproduces the scenario from issue #2475. + """ + OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") + if not OPENAI_API_KEY: + pytest.skip("OPENAI_API_KEY environment variable not set") + + llm = LLM( + model="openai/gpt-4o", # model with vision capabilities + api_key=OPENAI_API_KEY, + temperature=0.7 + ) + + expert_analyst = Agent( + role="Visual Quality Inspector", + goal="Perform detailed quality analysis of product images", + backstory="Senior quality control expert with expertise in visual inspection", + llm=llm, + verbose=True, + allow_delegation=False, + multimodal=True + ) + + inspection_task = Task( + description=""" + Analyze the product image at https://www.us.maguireshoes.com/collections/spring-25/products/lucena-black-boot with focus on: + 1. Quality of materials + 2. Manufacturing defects + 3. Compliance with standards + Provide a detailed report highlighting any issues found. + """, + expected_output="A detailed report highlighting any issues found", + agent=expert_analyst + ) + + crew = Crew(agents=[expert_analyst], tasks=[inspection_task]) From e3dde17af0e082962a2cd8ac589cf92c438d4eb5 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 26 Mar 2025 12:14:13 +0000 Subject: [PATCH 24/41] docs: improve LLMCallStartedEvent docstring to clarify multimodal support Co-Authored-By: Joe Moura --- src/crewai/utilities/events/llm_events.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/crewai/utilities/events/llm_events.py b/src/crewai/utilities/events/llm_events.py index 10a648e86..b92072340 100644 --- a/src/crewai/utilities/events/llm_events.py +++ b/src/crewai/utilities/events/llm_events.py @@ -12,7 +12,12 @@ class LLMCallType(Enum): class LLMCallStartedEvent(CrewEvent): - """Event emitted when a LLM call starts""" + """Event emitted when a LLM call starts + + Attributes: + messages: Content can be either a string or a list of dictionaries that support + multimodal content (text, images, etc.) + """ type: str = "llm_call_started" messages: Union[str, List[Dict[str, Any]]] From 3deeba4cab96727c8ba2e2405a08c466f5b2b3fe Mon Sep 17 00:00:00 2001 From: lucasgomide Date: Wed, 26 Mar 2025 16:28:37 -0300 Subject: [PATCH 25/41] test: adding missing test to ensure multimodal content structures --- ...l_agent_describing_image_successfully.yaml | 378 ++++++++++++++++++ tests/crew_test.py | 38 ++ 2 files changed, 416 insertions(+) create mode 100644 tests/cassettes/test_multimodal_agent_describing_image_successfully.yaml diff --git a/tests/cassettes/test_multimodal_agent_describing_image_successfully.yaml b/tests/cassettes/test_multimodal_agent_describing_image_successfully.yaml new file mode 100644 index 000000000..c9371c243 --- /dev/null +++ b/tests/cassettes/test_multimodal_agent_describing_image_successfully.yaml @@ -0,0 +1,378 @@ +interactions: +- request: + body: !!binary | + CpIKCiQKIgoMc2VydmljZS5uYW1lEhIKEGNyZXdBSS10ZWxlbWV0cnkS6QkKEgoQY3Jld2FpLnRl + bGVtZXRyeRLBBwoQ08SlQ6w2FsCauTgZCqberRIITfOsgNi1qJkqDENyZXcgQ3JlYXRlZDABOdjG + 6D/PcDAYQahPEkDPcDAYShsKDmNyZXdhaV92ZXJzaW9uEgkKBzAuMTA4LjBKGgoOcHl0aG9uX3Zl + cnNpb24SCAoGMy4xMi45Si4KCGNyZXdfa2V5EiIKIDkwNzMxMTU4MzVlMWNhZjJhNmUxNTIyZDA1 + YTBiNTFkSjEKB2NyZXdfaWQSJgokMzdjOGM4NzgtN2NmZC00YjEyLWE4YzctYzIyZDZlOTIxODBk + ShwKDGNyZXdfcHJvY2VzcxIMCgpzZXF1ZW50aWFsShEKC2NyZXdfbWVtb3J5EgIQAEoaChRjcmV3 + X251bWJlcl9vZl90YXNrcxICGAFKGwoVY3Jld19udW1iZXJfb2ZfYWdlbnRzEgIYAUrgAgoLY3Jl + d19hZ2VudHMS0AIKzQJbeyJrZXkiOiAiNzYyM2ZjNGY3ZDk0Y2YzZmRiZmNjMjlmYjBiMDIyYmIi + LCAiaWQiOiAiYmVjMjljMTAtOTljYi00MzQwLWIwYTItMWU1NTVkNGRmZGM0IiwgInJvbGUiOiAi + VmlzdWFsIFF1YWxpdHkgSW5zcGVjdG9yIiwgInZlcmJvc2U/IjogdHJ1ZSwgIm1heF9pdGVyIjog + MjUsICJtYXhfcnBtIjogbnVsbCwgImZ1bmN0aW9uX2NhbGxpbmdfbGxtIjogIiIsICJsbG0iOiAi + b3BlbmFpL2dwdC00byIsICJkZWxlZ2F0aW9uX2VuYWJsZWQ/IjogZmFsc2UsICJhbGxvd19jb2Rl + X2V4ZWN1dGlvbj8iOiBmYWxzZSwgIm1heF9yZXRyeV9saW1pdCI6IDIsICJ0b29sc19uYW1lcyI6 + IFtdfV1KjQIKCmNyZXdfdGFza3MS/gEK+wFbeyJrZXkiOiAiMDExM2E5ZTg0N2M2NjI2ZDY0ZDZk + Yzk4M2IwNDA5MTgiLCAiaWQiOiAiZWQzYmY1YWUtZTBjMS00MjIxLWFhYTgtMThlNjVkYTMyZjc1 + IiwgImFzeW5jX2V4ZWN1dGlvbj8iOiBmYWxzZSwgImh1bWFuX2lucHV0PyI6IGZhbHNlLCAiYWdl + bnRfcm9sZSI6ICJWaXN1YWwgUXVhbGl0eSBJbnNwZWN0b3IiLCAiYWdlbnRfa2V5IjogIjc2MjNm + YzRmN2Q5NGNmM2ZkYmZjYzI5ZmIwYjAyMmJiIiwgInRvb2xzX25hbWVzIjogW119XXoCGAGFAQAB + AAASjgIKECo77ESam8oLrZMmgLLaoksSCLE6x14/Kb1vKgxUYXNrIENyZWF0ZWQwATlI/chAz3Aw + GEEAgMpAz3AwGEouCghjcmV3X2tleRIiCiA5MDczMTE1ODM1ZTFjYWYyYTZlMTUyMmQwNWEwYjUx + ZEoxCgdjcmV3X2lkEiYKJDM3YzhjODc4LTdjZmQtNGIxMi1hOGM3LWMyMmQ2ZTkyMTgwZEouCgh0 + YXNrX2tleRIiCiAwMTEzYTllODQ3YzY2MjZkNjRkNmRjOTgzYjA0MDkxOEoxCgd0YXNrX2lkEiYK + JGVkM2JmNWFlLWUwYzEtNDIyMS1hYWE4LTE4ZTY1ZGEzMmY3NXoCGAGFAQABAAA= + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '1301' + Content-Type: + - application/x-protobuf + User-Agent: + - OTel-OTLP-Exporter-Python/1.31.1 + method: POST + uri: https://telemetry.crewai.com:4319/v1/traces + response: + body: + string: "\n\0" + headers: + Content-Length: + - '2' + Content-Type: + - application/x-protobuf + Date: + - Wed, 26 Mar 2025 19:24:52 GMT + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Visual Quality Inspector. + Senior quality control expert with expertise in visual inspection\nYour personal + goal is: Perform detailed quality analysis of product images\nYou ONLY have + access to the following tools, and should NEVER make up tools that are not listed + here:\n\nTool Name: Add image to content\nTool Arguments: {''image_url'': {''description'': + ''The URL or path of the image to add'', ''type'': ''str''}, ''action'': {''description'': + ''Optional context or question about the image'', ''type'': ''Union[str, NoneType]''}}\nTool + Description: See image to understand its content, you can optionally ask a question + about the image\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 [Add image to content], 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```"}, {"role": "user", "content": "\nCurrent Task: \n Analyze + the product image at https://www.us.maguireshoes.com/cdn/shop/files/FW24-Edito-Lucena-Distressed-01_1920x.jpg?v=1736371244 + with focus on:\n 1. Quality of materials\n 2. Manufacturing defects\n 3. + Compliance with standards\n Provide a detailed report highlighting any + issues found.\n \n\nThis is the expected criteria for your final answer: + A detailed report highlighting any issues found\nyou MUST return the actual + complete content as the final answer, not a summary.\n\nBegin! This is VERY + important to you, use the tools available and give your best Final Answer, your + job depends on it!\n\nThought:"}], "model": "gpt-4o", "stop": ["\nObservation:"], + "temperature": 0.7}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '2033' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.68.2 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.68.2 + x-stainless-raw-response: + - 'true' + x-stainless-read-timeout: + - '600.0' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-BFQepLwSYYzdKLylSFsgcJeg6GTqS\",\n \"object\": + \"chat.completion\",\n \"created\": 1743017091,\n \"model\": \"gpt-4o-2024-08-06\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"Thought: I need to examine the product + image to assess the quality of materials, look for any manufacturing defects, + and check compliance with standards.\\n\\nAction: Add image to content\\nAction + Input: {\\\"image_url\\\": \\\"https://www.us.maguireshoes.com/cdn/shop/files/FW24-Edito-Lucena-Distressed-01_1920x.jpg?v=1736371244\\\", + \\\"action\\\": \\\"Analyze the quality of materials, manufacturing defects, + and compliance with standards.\\\"}\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 413,\n \"completion_tokens\": + 101,\n \"total_tokens\": 514,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_7e8d90e604\"\n}\n" + headers: + CF-RAY: + - 926907d79dcff1e7-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 26 Mar 2025 19:24:53 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=WK433.4kW8cr9rwvOlk4EZ2SfRYK9lAPwXCBYEvLcmU-1743017093-1.0.1.1-kVZyUew5rUbMk.2koGJF_rmX.fTseqN241n2M40n8KvBGoKgy6KM6xBmvFbIVWxUs2Y5ZAz8mWy9CrGjaNKSfCzxmv4.pq78z_DGHr37PgI; + path=/; expires=Wed, 26-Mar-25 19:54:53 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=T77PMcuNYeyzK0tQyDOe7EScjVBVzW_7DpD3YQBqmUc-1743017093675-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '1729' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-requests: + - '50000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '49999' + x-ratelimit-remaining-tokens: + - '149999534' + x-ratelimit-reset-requests: + - 1ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_2399c3355adf16734907c73611a7d330 + http_version: HTTP/1.1 + status_code: 200 +- request: + body: !!binary | + CtgBCiQKIgoMc2VydmljZS5uYW1lEhIKEGNyZXdBSS10ZWxlbWV0cnkSrwEKEgoQY3Jld2FpLnRl + bGVtZXRyeRKYAQoQp2ACB2xRGve4HGtU2RdWCBIIlQcsbhK22ykqClRvb2wgVXNhZ2UwATlACEXG + z3AwGEHAjGPGz3AwGEobCg5jcmV3YWlfdmVyc2lvbhIJCgcwLjEwOC4wSiMKCXRvb2xfbmFtZRIW + ChRBZGQgaW1hZ2UgdG8gY29udGVudEoOCghhdHRlbXB0cxICGAF6AhgBhQEAAQAA + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '219' + Content-Type: + - application/x-protobuf + User-Agent: + - OTel-OTLP-Exporter-Python/1.31.1 + method: POST + uri: https://telemetry.crewai.com:4319/v1/traces + response: + body: + string: "\n\0" + headers: + Content-Length: + - '2' + Content-Type: + - application/x-protobuf + Date: + - Wed, 26 Mar 2025 19:24:57 GMT + status: + code: 200 + message: OK +- request: + body: '{"messages": [{"role": "system", "content": "You are Visual Quality Inspector. + Senior quality control expert with expertise in visual inspection\nYour personal + goal is: Perform detailed quality analysis of product images\nYou ONLY have + access to the following tools, and should NEVER make up tools that are not listed + here:\n\nTool Name: Add image to content\nTool Arguments: {''image_url'': {''description'': + ''The URL or path of the image to add'', ''type'': ''str''}, ''action'': {''description'': + ''Optional context or question about the image'', ''type'': ''Union[str, NoneType]''}}\nTool + Description: See image to understand its content, you can optionally ask a question + about the image\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 [Add image to content], 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```"}, {"role": "user", "content": "\nCurrent Task: \n Analyze + the product image at https://www.us.maguireshoes.com/cdn/shop/files/FW24-Edito-Lucena-Distressed-01_1920x.jpg?v=1736371244 + with focus on:\n 1. Quality of materials\n 2. Manufacturing defects\n 3. + Compliance with standards\n Provide a detailed report highlighting any + issues found.\n \n\nThis is the expected criteria for your final answer: + A detailed report highlighting any issues found\nyou MUST return the actual + complete content as the final answer, not a summary.\n\nBegin! This is VERY + important to you, use the tools available and give your best Final Answer, your + job depends on it!\n\nThought:"}, {"role": "user", "content": [{"type": "text", + "text": "Analyze the quality of materials, manufacturing defects, and compliance + with standards."}, {"type": "image_url", "image_url": {"url": "https://www.us.maguireshoes.com/cdn/shop/files/FW24-Edito-Lucena-Distressed-01_1920x.jpg?v=1736371244"}}]}, + {"role": "assistant", "content": "Thought: I need to examine the product image + to assess the quality of materials, look for any manufacturing defects, and + check compliance with standards.\n\nAction: Add image to content\nAction Input: + {\"image_url\": \"https://www.us.maguireshoes.com/cdn/shop/files/FW24-Edito-Lucena-Distressed-01_1920x.jpg?v=1736371244\", + \"action\": \"Analyze the quality of materials, manufacturing defects, and compliance + with standards.\"}"}], "model": "gpt-4o", "stop": ["\nObservation:"], "temperature": + 0.7}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '2797' + content-type: + - application/json + cookie: + - __cf_bm=WK433.4kW8cr9rwvOlk4EZ2SfRYK9lAPwXCBYEvLcmU-1743017093-1.0.1.1-kVZyUew5rUbMk.2koGJF_rmX.fTseqN241n2M40n8KvBGoKgy6KM6xBmvFbIVWxUs2Y5ZAz8mWy9CrGjaNKSfCzxmv4.pq78z_DGHr37PgI; + _cfuvid=T77PMcuNYeyzK0tQyDOe7EScjVBVzW_7DpD3YQBqmUc-1743017093675-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - OpenAI/Python 1.68.2 + x-stainless-arch: + - x64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.68.2 + x-stainless-raw-response: + - 'true' + x-stainless-read-timeout: + - '600.0' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.9 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + content: "{\n \"id\": \"chatcmpl-BFQetNNvmPgPxhzaKiHYsPqm8aN0i\",\n \"object\": + \"chat.completion\",\n \"created\": 1743017095,\n \"model\": \"gpt-4o-2024-08-06\",\n + \ \"choices\": [\n {\n \"index\": 0,\n \"message\": {\n \"role\": + \"assistant\",\n \"content\": \"Observation: The image displays a black + leather boot with a pointed toe and a low heel. \\n\\nQuality of Materials:\\n1. + The leather appears to be of good quality, displaying a consistent texture and + finish, which suggests durability.\\n2. The material has a slight sheen, indicating + a possible finishing treatment that enhances the appearance and may offer some + protection.\\n\\nManufacturing Defects:\\n1. There are no visible stitching + errors; the seams appear straight and clean.\\n2. No apparent glue marks or + uneven edges, which indicates good craftsmanship.\\n3. There is a slight distressed + effect, but it appears intentional as part of the design rather than a defect.\\n\\nCompliance + with Standards:\\n1. The shoe design seems to comply with typical fashion standards, + showing a balance of aesthetics and functionality.\\n2. The heel height and + shape appear to provide stability, aligning with safety standards for footwear.\\n\\nFinal + Answer: The analysis of the product image reveals that the black leather boot + is made of high-quality materials with no visible manufacturing defects. The + craftsmanship is precise, with clean seams and a well-executed design. The distressed + effect appears intentional and part of the aesthetic. The boot seems to comply + with fashion and safety standards, offering both style and functionality. No + significant issues were found.\",\n \"refusal\": null,\n \"annotations\": + []\n },\n \"logprobs\": null,\n \"finish_reason\": \"stop\"\n + \ }\n ],\n \"usage\": {\n \"prompt_tokens\": 1300,\n \"completion_tokens\": + 250,\n \"total_tokens\": 1550,\n \"prompt_tokens_details\": {\n \"cached_tokens\": + 0,\n \"audio_tokens\": 0\n },\n \"completion_tokens_details\": {\n + \ \"reasoning_tokens\": 0,\n \"audio_tokens\": 0,\n \"accepted_prediction_tokens\": + 0,\n \"rejected_prediction_tokens\": 0\n }\n },\n \"service_tier\": + \"default\",\n \"system_fingerprint\": \"fp_3a5b33c01a\"\n}\n" + headers: + CF-RAY: + - 926907e45f33f1e7-GRU + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 26 Mar 2025 19:25:01 GMT + Server: + - cloudflare + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - crewai-iuxna1 + openai-processing-ms: + - '7242' + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-ratelimit-limit-input-images: + - '250000' + x-ratelimit-limit-requests: + - '50000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-input-images: + - '249999' + x-ratelimit-remaining-requests: + - '49999' + x-ratelimit-remaining-tokens: + - '149998641' + x-ratelimit-reset-input-images: + - 0s + x-ratelimit-reset-requests: + - 1ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_c5dd144c8ac1bb3bd96ffbba40707b2d + http_version: HTTP/1.1 + status_code: 200 +version: 1 diff --git a/tests/crew_test.py b/tests/crew_test.py index 39a3e9a08..e5f464131 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -3731,6 +3731,44 @@ def test_multimodal_agent_image_tool_handling(): assert result["content"][1]["type"] == "image_url" +@pytest.mark.vcr(filter_headers=["authorization"]) +def test_multimodal_agent_describing_image_successfully(): + """ + Test that a multimodal agent can process images without validation errors. + This test reproduces the scenario from issue #2475. + """ + llm = LLM(model="openai/gpt-4o", temperature=0.7) # model with vision capabilities + + expert_analyst = Agent( + role="Visual Quality Inspector", + goal="Perform detailed quality analysis of product images", + backstory="Senior quality control expert with expertise in visual inspection", + llm=llm, + verbose=True, + allow_delegation=False, + multimodal=True, + ) + + inspection_task = Task( + description=""" + Analyze the product image at https://www.us.maguireshoes.com/cdn/shop/files/FW24-Edito-Lucena-Distressed-01_1920x.jpg?v=1736371244 with focus on: + 1. Quality of materials + 2. Manufacturing defects + 3. Compliance with standards + Provide a detailed report highlighting any issues found. + """, + expected_output="A detailed report highlighting any issues found", + agent=expert_analyst, + ) + + crew = Crew(agents=[expert_analyst], tasks=[inspection_task]) + result = crew.kickoff() + + task_output = result.tasks_output[0] + assert isinstance(task_output, TaskOutput) + assert task_output.raw == result.raw + + @pytest.mark.vcr(filter_headers=["authorization"]) def test_multimodal_agent_live_image_analysis(): """ From 48983773f5eaaf2875505a649a4fa7ef0d64314c Mon Sep 17 00:00:00 2001 From: Eduardo Chiarotti Date: Wed, 26 Mar 2025 16:50:09 -0300 Subject: [PATCH 26/41] feat: add output to ToolUsageFinishedEvent (#2477) * feat: add output to ToolUsageFinishedEvent * feat: add type ignore * feat: add tests --- src/crewai/tools/tool_usage.py | 26 ++- .../utilities/events/tool_usage_events.py | 1 + tests/tools/test_tool_usage.py | 161 ++++++++++++++++++ 3 files changed, 181 insertions(+), 7 deletions(-) diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py index 9c924027d..edfedb4b8 100644 --- a/src/crewai/tools/tool_usage.py +++ b/src/crewai/tools/tool_usage.py @@ -117,7 +117,10 @@ class ToolUsage: self._printer.print(content=f"\n\n{error}\n", color="red") return error - if isinstance(tool, CrewStructuredTool) and tool.name == self._i18n.tools("add_image")["name"]: # type: ignore + if ( + isinstance(tool, CrewStructuredTool) + and tool.name == self._i18n.tools("add_image")["name"] # type: ignore + ): try: result = self._use(tool_string=tool_string, tool=tool, calling=calling) return result @@ -181,7 +184,9 @@ class ToolUsage: if calling.arguments: try: - acceptable_args = tool.args_schema.model_json_schema()["properties"].keys() # type: ignore + acceptable_args = tool.args_schema.model_json_schema()[ + "properties" + ].keys() # type: ignore arguments = { k: v for k, v in calling.arguments.items() @@ -202,7 +207,7 @@ class ToolUsage: error=e, tool=tool.name, tool_inputs=tool.description ) error = ToolUsageErrorException( - f'\n{error_message}.\nMoving on then. {self._i18n.slice("format").format(tool_names=self.tools_names)}' + f"\n{error_message}.\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}" ).message self.task.increment_tools_errors() if self.agent.verbose: @@ -244,6 +249,7 @@ class ToolUsage: tool_calling=calling, from_cache=from_cache, started_at=started_at, + result=result, ) if ( @@ -380,7 +386,7 @@ class ToolUsage: raise else: return ToolUsageErrorException( - f'{self._i18n.errors("tool_arguments_error")}' + f"{self._i18n.errors('tool_arguments_error')}" ) if not isinstance(arguments, dict): @@ -388,7 +394,7 @@ class ToolUsage: raise else: return ToolUsageErrorException( - f'{self._i18n.errors("tool_arguments_error")}' + f"{self._i18n.errors('tool_arguments_error')}" ) return ToolCalling( @@ -416,7 +422,7 @@ class ToolUsage: if self.agent.verbose: self._printer.print(content=f"\n\n{e}\n", color="red") return ToolUsageErrorException( # type: ignore # Incompatible return value type (got "ToolUsageErrorException", expected "ToolCalling | InstructorToolCalling") - f'{self._i18n.errors("tool_usage_error").format(error=e)}\nMoving on then. {self._i18n.slice("format").format(tool_names=self.tools_names)}' + f"{self._i18n.errors('tool_usage_error').format(error=e)}\nMoving on then. {self._i18n.slice('format').format(tool_names=self.tools_names)}" ) return self._tool_calling(tool_string) @@ -492,7 +498,12 @@ class ToolUsage: crewai_event_bus.emit(self, ToolUsageErrorEvent(**{**event_data, "error": e})) def on_tool_use_finished( - self, tool: Any, tool_calling: ToolCalling, from_cache: bool, started_at: float + self, + tool: Any, + tool_calling: ToolCalling, + from_cache: bool, + started_at: float, + result: Any, ) -> None: finished_at = time.time() event_data = self._prepare_event_data(tool, tool_calling) @@ -501,6 +512,7 @@ class ToolUsage: "started_at": datetime.datetime.fromtimestamp(started_at), "finished_at": datetime.datetime.fromtimestamp(finished_at), "from_cache": from_cache, + "output": result, } ) crewai_event_bus.emit(self, ToolUsageFinishedEvent(**event_data)) diff --git a/src/crewai/utilities/events/tool_usage_events.py b/src/crewai/utilities/events/tool_usage_events.py index aa375dcd7..e5027832c 100644 --- a/src/crewai/utilities/events/tool_usage_events.py +++ b/src/crewai/utilities/events/tool_usage_events.py @@ -30,6 +30,7 @@ class ToolUsageFinishedEvent(ToolUsageEvent): started_at: datetime finished_at: datetime from_cache: bool = False + output: Any type: str = "tool_usage_finished" diff --git a/tests/tools/test_tool_usage.py b/tests/tools/test_tool_usage.py index e09d4d537..9cf9ae1d4 100644 --- a/tests/tools/test_tool_usage.py +++ b/tests/tools/test_tool_usage.py @@ -1,5 +1,7 @@ +import datetime import json import random +import time from unittest.mock import MagicMock, patch import pytest @@ -11,6 +13,7 @@ from crewai.tools.tool_usage import ToolUsage from crewai.utilities.events import crewai_event_bus from crewai.utilities.events.tool_usage_events import ( ToolSelectionErrorEvent, + ToolUsageFinishedEvent, ToolValidateInputErrorEvent, ) @@ -624,3 +627,161 @@ def test_tool_validate_input_error_event(): assert event.agent_role == "test_role" assert event.tool_name == "test_tool" assert "must be a valid dictionary" in event.error + + +def test_tool_usage_finished_event_with_result(): + """Test that ToolUsageFinishedEvent is emitted with correct result attributes.""" + # Create mock agent with proper string values + mock_agent = MagicMock() + mock_agent.key = "test_agent_key" + mock_agent.role = "test_agent_role" + mock_agent._original_role = "test_agent_role" + mock_agent.i18n = MagicMock() + mock_agent.verbose = False + + # Create mock task + mock_task = MagicMock() + mock_task.delegations = 0 + + # Create mock tool + class TestTool(BaseTool): + name: str = "Test Tool" + description: str = "A test tool" + + def _run(self, input: dict) -> str: + return "test result" + + test_tool = TestTool() + + # Create mock tool calling + mock_tool_calling = MagicMock() + mock_tool_calling.arguments = {"arg1": "value1"} + + # Create ToolUsage instance + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[test_tool], + original_tools=[test_tool], + tools_description="Test Tool Description", + tools_names="Test Tool", + task=mock_task, + function_calling_llm=None, + agent=mock_agent, + action=MagicMock(), + ) + + # Track received events + received_events = [] + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def event_handler(source, event): + received_events.append(event) + + # Call on_tool_use_finished with test data + started_at = time.time() + result = "test output result" + tool_usage.on_tool_use_finished( + tool=test_tool, + tool_calling=mock_tool_calling, + from_cache=False, + started_at=started_at, + result=result, + ) + + # Verify event was emitted + assert len(received_events) == 1, "Expected one event to be emitted" + event = received_events[0] + assert isinstance(event, ToolUsageFinishedEvent) + + # Verify event attributes + assert event.agent_key == "test_agent_key" + assert event.agent_role == "test_agent_role" + assert event.tool_name == "Test Tool" + assert event.tool_args == {"arg1": "value1"} + assert event.tool_class == "TestTool" + assert event.run_attempts == 1 # Default value from ToolUsage + assert event.delegations == 0 + assert event.from_cache is False + assert event.output == "test output result" + assert isinstance(event.started_at, datetime.datetime) + assert isinstance(event.finished_at, datetime.datetime) + assert event.type == "tool_usage_finished" + + +def test_tool_usage_finished_event_with_cached_result(): + """Test that ToolUsageFinishedEvent is emitted with correct result attributes when using cached result.""" + # Create mock agent with proper string values + mock_agent = MagicMock() + mock_agent.key = "test_agent_key" + mock_agent.role = "test_agent_role" + mock_agent._original_role = "test_agent_role" + mock_agent.i18n = MagicMock() + mock_agent.verbose = False + + # Create mock task + mock_task = MagicMock() + mock_task.delegations = 0 + + # Create mock tool + class TestTool(BaseTool): + name: str = "Test Tool" + description: str = "A test tool" + + def _run(self, input: dict) -> str: + return "test result" + + test_tool = TestTool() + + # Create mock tool calling + mock_tool_calling = MagicMock() + mock_tool_calling.arguments = {"arg1": "value1"} + + # Create ToolUsage instance + tool_usage = ToolUsage( + tools_handler=MagicMock(), + tools=[test_tool], + original_tools=[test_tool], + tools_description="Test Tool Description", + tools_names="Test Tool", + task=mock_task, + function_calling_llm=None, + agent=mock_agent, + action=MagicMock(), + ) + + # Track received events + received_events = [] + + @crewai_event_bus.on(ToolUsageFinishedEvent) + def event_handler(source, event): + received_events.append(event) + + # Call on_tool_use_finished with test data and from_cache=True + started_at = time.time() + result = "cached test output result" + tool_usage.on_tool_use_finished( + tool=test_tool, + tool_calling=mock_tool_calling, + from_cache=True, + started_at=started_at, + result=result, + ) + + # Verify event was emitted + assert len(received_events) == 1, "Expected one event to be emitted" + event = received_events[0] + assert isinstance(event, ToolUsageFinishedEvent) + + # Verify event attributes + assert event.agent_key == "test_agent_key" + assert event.agent_role == "test_agent_role" + assert event.tool_name == "Test Tool" + assert event.tool_args == {"arg1": "value1"} + assert event.tool_class == "TestTool" + assert event.run_attempts == 1 # Default value from ToolUsage + assert event.delegations == 0 + assert event.from_cache is True + assert event.output == "cached test output result" + assert isinstance(event.started_at, datetime.datetime) + assert isinstance(event.finished_at, datetime.datetime) + assert event.type == "tool_usage_finished" From e1a73e0c44fd85b2ad34ca0a3eda57964c0dee4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Moura?= Date: Wed, 26 Mar 2025 14:54:23 -0700 Subject: [PATCH 27/41] Using fingerprints (#2456) * using fingerprints * passing fingerptins on tools * fix * update lock * Fix type checker errors --------- Co-authored-by: Brandon Hancock Co-authored-by: Brandon Hancock (bhancock_ai) <109994880+bhancockio@users.noreply.github.com> Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> --- src/crewai/agents/crew_agent_executor.py | 72 ++++++-- src/crewai/task.py | 6 +- src/crewai/telemetry/telemetry.py | 172 +++++++++++++++++- src/crewai/tools/tool_usage.py | 57 +++++- .../utilities/evaluators/task_evaluator.py | 2 +- src/crewai/utilities/events/agent_events.py | 27 +++ src/crewai/utilities/events/base_events.py | 4 + src/crewai/utilities/events/crew_events.py | 90 +++++++++ src/crewai/utilities/events/task_events.py | 42 ++++- .../utilities/events/tool_usage_events.py | 22 ++- 10 files changed, 458 insertions(+), 36 deletions(-) diff --git a/src/crewai/agents/crew_agent_executor.py b/src/crewai/agents/crew_agent_executor.py index bb17cd095..eb0cb22ad 100644 --- a/src/crewai/agents/crew_agent_executor.py +++ b/src/crewai/agents/crew_agent_executor.py @@ -20,7 +20,6 @@ from crewai.utilities import I18N, Printer from crewai.utilities.constants import MAX_LLM_RETRY, TRAINING_DATA_FILE from crewai.utilities.events import ( ToolUsageErrorEvent, - ToolUsageStartedEvent, crewai_event_bus, ) from crewai.utilities.events.tool_usage_events import ToolUsageStartedEvent @@ -153,8 +152,21 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): formatted_answer = self._process_llm_response(answer) if isinstance(formatted_answer, AgentAction): + # Extract agent fingerprint if available + fingerprint_context = {} + if ( + self.agent + and hasattr(self.agent, "security_config") + and hasattr(self.agent.security_config, "fingerprint") + ): + fingerprint_context = { + "agent_fingerprint": str( + self.agent.security_config.fingerprint + ) + } + tool_result = self._execute_tool_and_check_finality( - formatted_answer + formatted_answer, fingerprint_context=fingerprint_context ) formatted_answer = self._handle_agent_action( formatted_answer, tool_result @@ -360,19 +372,35 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): content=f"\033[95m## Final Answer:\033[00m \033[92m\n{formatted_answer.output}\033[00m\n\n" ) - def _execute_tool_and_check_finality(self, agent_action: AgentAction) -> ToolResult: + def _execute_tool_and_check_finality( + self, + agent_action: AgentAction, + fingerprint_context: Optional[Dict[str, str]] = None, + ) -> ToolResult: try: + fingerprint_context = fingerprint_context or {} + if self.agent: + # Create tool usage event with fingerprint information + event_data = { + "agent_key": self.agent.key, + "agent_role": self.agent.role, + "tool_name": agent_action.tool, + "tool_args": agent_action.tool_input, + "tool_class": agent_action.tool, + "agent": self.agent, # Pass the agent object for fingerprint extraction + } + + # Include fingerprint context + if fingerprint_context: + event_data.update(fingerprint_context) + + # Emit the tool usage started event with agent information crewai_event_bus.emit( self, - event=ToolUsageStartedEvent( - agent_key=self.agent.key, - agent_role=self.agent.role, - tool_name=agent_action.tool, - tool_args=agent_action.tool_input, - tool_class=agent_action.tool, - ), + event=ToolUsageStartedEvent(**event_data), ) + tool_usage = ToolUsage( tools_handler=self.tools_handler, tools=self.tools, @@ -383,6 +411,7 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): task=self.task, # type: ignore[arg-type] agent=self.agent, action=agent_action, + fingerprint_context=fingerprint_context, # Pass fingerprint context ) tool_calling = tool_usage.parse_tool_calling(agent_action.text) @@ -411,16 +440,23 @@ class CrewAgentExecutor(CrewAgentExecutorMixin): except Exception as e: # TODO: drop if self.agent: + error_event_data = { + "agent_key": self.agent.key, + "agent_role": self.agent.role, + "tool_name": agent_action.tool, + "tool_args": agent_action.tool_input, + "tool_class": agent_action.tool, + "error": str(e), + "agent": self.agent, # Pass the agent object for fingerprint extraction + } + + # Include fingerprint context + if fingerprint_context: + error_event_data.update(fingerprint_context) + crewai_event_bus.emit( self, - event=ToolUsageErrorEvent( # validation error - agent_key=self.agent.key, - agent_role=self.agent.role, - tool_name=agent_action.tool, - tool_args=agent_action.tool_input, - tool_class=agent_action.tool, - error=str(e), - ), + event=ToolUsageErrorEvent(**error_event_data), ) raise e diff --git a/src/crewai/task.py b/src/crewai/task.py index c74e0081e..9874b5100 100644 --- a/src/crewai/task.py +++ b/src/crewai/task.py @@ -388,7 +388,7 @@ class Task(BaseModel): tools = tools or self.tools or [] self.processed_by_agents.add(agent.role) - crewai_event_bus.emit(self, TaskStartedEvent(context=context)) + crewai_event_bus.emit(self, TaskStartedEvent(context=context, task=self)) result = agent.execute_task( task=self, context=context, @@ -464,11 +464,11 @@ class Task(BaseModel): ) ) self._save_file(content) - crewai_event_bus.emit(self, TaskCompletedEvent(output=task_output)) + crewai_event_bus.emit(self, TaskCompletedEvent(output=task_output, task=self)) return task_output except Exception as e: self.end_time = datetime.datetime.now() - crewai_event_bus.emit(self, TaskFailedEvent(error=str(e))) + crewai_event_bus.emit(self, TaskFailedEvent(error=str(e), task=self)) raise e # Re-raise the exception after emitting the event def prompt(self) -> str: diff --git a/src/crewai/telemetry/telemetry.py b/src/crewai/telemetry/telemetry.py index 559ca8d4f..edf4c886a 100644 --- a/src/crewai/telemetry/telemetry.py +++ b/src/crewai/telemetry/telemetry.py @@ -112,6 +112,23 @@ class Telemetry: self._add_attribute(span, "crew_memory", crew.memory) self._add_attribute(span, "crew_number_of_tasks", len(crew.tasks)) self._add_attribute(span, "crew_number_of_agents", len(crew.agents)) + + # Add fingerprint data + if hasattr(crew, "fingerprint") and crew.fingerprint: + self._add_attribute(span, "crew_fingerprint", crew.fingerprint.uuid_str) + self._add_attribute( + span, + "crew_fingerprint_created_at", + crew.fingerprint.created_at.isoformat(), + ) + # Add fingerprint metadata if it exists + if hasattr(crew.fingerprint, "metadata") and crew.fingerprint.metadata: + self._add_attribute( + span, + "crew_fingerprint_metadata", + json.dumps(crew.fingerprint.metadata), + ) + if crew.share_crew: self._add_attribute( span, @@ -129,17 +146,43 @@ class Telemetry: "max_rpm": agent.max_rpm, "i18n": agent.i18n.prompt_file, "function_calling_llm": ( - agent.function_calling_llm.model - if agent.function_calling_llm + getattr( + getattr(agent, "function_calling_llm", None), + "model", + "", + ) + if getattr(agent, "function_calling_llm", None) else "" ), "llm": agent.llm.model, "delegation_enabled?": agent.allow_delegation, - "allow_code_execution?": agent.allow_code_execution, - "max_retry_limit": agent.max_retry_limit, + "allow_code_execution?": getattr( + agent, "allow_code_execution", False + ), + "max_retry_limit": getattr(agent, "max_retry_limit", 3), "tools_names": [ tool.name.casefold() for tool in agent.tools or [] ], + # Add agent fingerprint data if sharing crew details + "fingerprint": ( + getattr( + getattr(agent, "fingerprint", None), + "uuid_str", + None, + ) + ), + "fingerprint_created_at": ( + created_at.isoformat() + if ( + created_at := getattr( + getattr(agent, "fingerprint", None), + "created_at", + None, + ) + ) + is not None + else None + ), } for agent in crew.agents ] @@ -169,6 +212,17 @@ class Telemetry: "tools_names": [ tool.name.casefold() for tool in task.tools or [] ], + # Add task fingerprint data if sharing crew details + "fingerprint": ( + task.fingerprint.uuid_str + if hasattr(task, "fingerprint") and task.fingerprint + else None + ), + "fingerprint_created_at": ( + task.fingerprint.created_at.isoformat() + if hasattr(task, "fingerprint") and task.fingerprint + else None + ), } for task in crew.tasks ] @@ -196,14 +250,20 @@ class Telemetry: "max_iter": agent.max_iter, "max_rpm": agent.max_rpm, "function_calling_llm": ( - agent.function_calling_llm.model - if agent.function_calling_llm + getattr( + getattr(agent, "function_calling_llm", None), + "model", + "", + ) + if getattr(agent, "function_calling_llm", None) else "" ), "llm": agent.llm.model, "delegation_enabled?": agent.allow_delegation, - "allow_code_execution?": agent.allow_code_execution, - "max_retry_limit": agent.max_retry_limit, + "allow_code_execution?": getattr( + agent, "allow_code_execution", False + ), + "max_retry_limit": getattr(agent, "max_retry_limit", 3), "tools_names": [ tool.name.casefold() for tool in agent.tools or [] ], @@ -252,6 +312,39 @@ class Telemetry: self._add_attribute(created_span, "task_key", task.key) self._add_attribute(created_span, "task_id", str(task.id)) + # Add fingerprint data + if hasattr(crew, "fingerprint") and crew.fingerprint: + self._add_attribute( + created_span, "crew_fingerprint", crew.fingerprint.uuid_str + ) + + if hasattr(task, "fingerprint") and task.fingerprint: + self._add_attribute( + created_span, "task_fingerprint", task.fingerprint.uuid_str + ) + self._add_attribute( + created_span, + "task_fingerprint_created_at", + task.fingerprint.created_at.isoformat(), + ) + # Add fingerprint metadata if it exists + if hasattr(task.fingerprint, "metadata") and task.fingerprint.metadata: + self._add_attribute( + created_span, + "task_fingerprint_metadata", + json.dumps(task.fingerprint.metadata), + ) + + # Add agent fingerprint if task has an assigned agent + if hasattr(task, "agent") and task.agent: + agent_fingerprint = getattr( + getattr(task.agent, "fingerprint", None), "uuid_str", None + ) + if agent_fingerprint: + self._add_attribute( + created_span, "agent_fingerprint", agent_fingerprint + ) + if crew.share_crew: self._add_attribute( created_span, "formatted_description", task.description @@ -270,6 +363,21 @@ class Telemetry: self._add_attribute(span, "task_key", task.key) self._add_attribute(span, "task_id", str(task.id)) + # Add fingerprint data to execution span + if hasattr(crew, "fingerprint") and crew.fingerprint: + self._add_attribute(span, "crew_fingerprint", crew.fingerprint.uuid_str) + + if hasattr(task, "fingerprint") and task.fingerprint: + self._add_attribute(span, "task_fingerprint", task.fingerprint.uuid_str) + + # Add agent fingerprint if task has an assigned agent + if hasattr(task, "agent") and task.agent: + agent_fingerprint = getattr( + getattr(task.agent, "fingerprint", None), "uuid_str", None + ) + if agent_fingerprint: + self._add_attribute(span, "agent_fingerprint", agent_fingerprint) + if crew.share_crew: self._add_attribute(span, "formatted_description", task.description) self._add_attribute( @@ -291,7 +399,12 @@ class Telemetry: Note: If share_crew is enabled, this will also record the task output """ + def operation(): + # Ensure fingerprint data is present on completion span + if hasattr(task, "fingerprint") and task.fingerprint: + self._add_attribute(span, "task_fingerprint", task.fingerprint.uuid_str) + if crew.share_crew: self._add_attribute( span, @@ -312,6 +425,7 @@ class Telemetry: tool_name (str): Name of the tool being repeatedly used attempts (int): Number of attempts made with this tool """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Tool Repeated Usage") @@ -329,14 +443,16 @@ class Telemetry: self._safe_telemetry_operation(operation) - def tool_usage(self, llm: Any, tool_name: str, attempts: int): + def tool_usage(self, llm: Any, tool_name: str, attempts: int, agent: Any = None): """Records the usage of a tool by an agent. Args: llm (Any): The language model being used tool_name (str): Name of the tool being used attempts (int): Number of attempts made with this tool + agent (Any, optional): The agent using the tool """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Tool Usage") @@ -349,17 +465,31 @@ class Telemetry: self._add_attribute(span, "attempts", attempts) if llm: self._add_attribute(span, "llm", llm.model) + + # Add agent fingerprint data if available + if agent and hasattr(agent, "fingerprint") and agent.fingerprint: + self._add_attribute( + span, "agent_fingerprint", agent.fingerprint.uuid_str + ) + if hasattr(agent, "role"): + self._add_attribute(span, "agent_role", agent.role) + span.set_status(Status(StatusCode.OK)) span.end() self._safe_telemetry_operation(operation) - def tool_usage_error(self, llm: Any): + def tool_usage_error( + self, llm: Any, agent: Any = None, tool_name: Optional[str] = None + ): """Records when a tool usage results in an error. Args: llm (Any): The language model being used when the error occurred + agent (Any, optional): The agent using the tool + tool_name (str, optional): Name of the tool that caused the error """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Tool Usage Error") @@ -370,6 +500,18 @@ class Telemetry: ) if llm: self._add_attribute(span, "llm", llm.model) + + if tool_name: + self._add_attribute(span, "tool_name", tool_name) + + # Add agent fingerprint data if available + if agent and hasattr(agent, "fingerprint") and agent.fingerprint: + self._add_attribute( + span, "agent_fingerprint", agent.fingerprint.uuid_str + ) + if hasattr(agent, "role"): + self._add_attribute(span, "agent_role", agent.role) + span.set_status(Status(StatusCode.OK)) span.end() @@ -386,6 +528,7 @@ class Telemetry: exec_time (int): Execution time in seconds model_name (str): Name of the model used """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Crew Individual Test Result") @@ -420,6 +563,7 @@ class Telemetry: inputs (dict[str, Any] | None): Input parameters for the test model_name (str): Name of the model used in testing """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Crew Test Execution") @@ -446,6 +590,7 @@ class Telemetry: def deploy_signup_error_span(self): """Records when an error occurs during the deployment signup process.""" + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Deploy Signup Error") @@ -460,6 +605,7 @@ class Telemetry: Args: uuid (Optional[str]): Unique identifier for the deployment """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Start Deployment") @@ -472,6 +618,7 @@ class Telemetry: def create_crew_deployment_span(self): """Records the creation of a new crew deployment.""" + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Create Crew Deployment") @@ -487,6 +634,7 @@ class Telemetry: uuid (Optional[str]): Unique identifier for the crew log_type (str, optional): Type of logs being retrieved. Defaults to "deployment". """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Get Crew Logs") @@ -504,6 +652,7 @@ class Telemetry: Args: uuid (Optional[str]): Unique identifier for the crew being removed """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Remove Crew") @@ -634,6 +783,7 @@ class Telemetry: Args: flow_name (str): Name of the flow being created """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Flow Creation") @@ -650,6 +800,7 @@ class Telemetry: flow_name (str): Name of the flow being plotted node_names (list[str]): List of node names in the flow """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Flow Plotting") @@ -667,6 +818,7 @@ class Telemetry: flow_name (str): Name of the flow being executed node_names (list[str]): List of nodes being executed in the flow """ + def operation(): tracer = trace.get_tracer("crewai.telemetry") span = tracer.start_span("Flow Execution") diff --git a/src/crewai/tools/tool_usage.py b/src/crewai/tools/tool_usage.py index edfedb4b8..66cb5f7e0 100644 --- a/src/crewai/tools/tool_usage.py +++ b/src/crewai/tools/tool_usage.py @@ -22,6 +22,7 @@ from crewai.utilities.events.tool_usage_events import ( ToolSelectionErrorEvent, ToolUsageErrorEvent, ToolUsageFinishedEvent, + ToolUsageStartedEvent, ToolValidateInputErrorEvent, ) @@ -69,6 +70,7 @@ class ToolUsage: function_calling_llm: Any, agent: Any, action: Any, + fingerprint_context: Optional[Dict[str, str]] = None, ) -> None: self._i18n: I18N = agent.i18n self._printer: Printer = Printer() @@ -85,6 +87,7 @@ class ToolUsage: self.task = task self.action = action self.function_calling_llm = function_calling_llm + self.fingerprint_context = fingerprint_context or {} # Set the maximum parsing attempts for bigger models if ( @@ -192,12 +195,18 @@ class ToolUsage: for k, v in calling.arguments.items() if k in acceptable_args } + # Add fingerprint metadata if available + arguments = self._add_fingerprint_metadata(arguments) result = tool.invoke(input=arguments) except Exception: arguments = calling.arguments + # Add fingerprint metadata if available + arguments = self._add_fingerprint_metadata(arguments) result = tool.invoke(input=arguments) else: - result = tool.invoke(input={}) + # Add fingerprint metadata even to empty arguments + arguments = self._add_fingerprint_metadata({}) + result = tool.invoke(input=arguments) except Exception as e: self.on_tool_error(tool=tool, tool_calling=calling, e=e) self._run_attempts += 1 @@ -486,8 +495,13 @@ class ToolUsage: "tool_name": self.action.tool, "tool_args": str(self.action.tool_input), "tool_class": self.__class__.__name__, + "agent": self.agent, # Adding agent for fingerprint extraction } + # Include fingerprint context if available + if self.fingerprint_context: + tool_selection_data.update(self.fingerprint_context) + crewai_event_bus.emit( self, ToolValidateInputErrorEvent(**tool_selection_data, error=final_error), @@ -518,7 +532,7 @@ class ToolUsage: crewai_event_bus.emit(self, ToolUsageFinishedEvent(**event_data)) def _prepare_event_data(self, tool: Any, tool_calling: ToolCalling) -> dict: - return { + event_data = { "agent_key": self.agent.key, "agent_role": (self.agent._original_role or self.agent.role), "run_attempts": self._run_attempts, @@ -526,4 +540,43 @@ class ToolUsage: "tool_name": tool.name, "tool_args": tool_calling.arguments, "tool_class": tool.__class__.__name__, + "agent": self.agent, # Adding agent for fingerprint extraction } + + # Include fingerprint context if available + if self.fingerprint_context: + event_data.update(self.fingerprint_context) + + return event_data + + def _add_fingerprint_metadata(self, arguments: dict) -> dict: + """Add fingerprint metadata to tool arguments if available. + + Args: + arguments: The original tool arguments + + Returns: + Updated arguments dictionary with fingerprint metadata + """ + # Create a shallow copy to avoid modifying the original + arguments = arguments.copy() + + # Add security metadata under a designated key + if not "security_context" in arguments: + arguments["security_context"] = {} + + security_context = arguments["security_context"] + + # Add agent fingerprint if available + if hasattr(self, "agent") and hasattr(self.agent, "security_config"): + security_context["agent_fingerprint"] = self.agent.security_config.fingerprint.to_dict() + + # Add task fingerprint if available + if hasattr(self, "task") and hasattr(self.task, "security_config"): + security_context["task_fingerprint"] = self.task.security_config.fingerprint.to_dict() + + # Add crew fingerprint if available + if hasattr(self, "crew") and hasattr(self.crew, "security_config"): + security_context["crew_fingerprint"] = self.crew.security_config.fingerprint.to_dict() + + return arguments diff --git a/src/crewai/utilities/evaluators/task_evaluator.py b/src/crewai/utilities/evaluators/task_evaluator.py index 2e9907bd7..6dde83c24 100644 --- a/src/crewai/utilities/evaluators/task_evaluator.py +++ b/src/crewai/utilities/evaluators/task_evaluator.py @@ -45,7 +45,7 @@ class TaskEvaluator: def evaluate(self, task, output) -> TaskEvaluation: crewai_event_bus.emit( - self, TaskEvaluationEvent(evaluation_type="task_evaluation") + self, TaskEvaluationEvent(evaluation_type="task_evaluation", task=task) ) evaluation_query = ( f"Assess the quality of the task completed based on the description, expected output, and actual results.\n\n" diff --git a/src/crewai/utilities/events/agent_events.py b/src/crewai/utilities/events/agent_events.py index ed0480957..3d325b41c 100644 --- a/src/crewai/utilities/events/agent_events.py +++ b/src/crewai/utilities/events/agent_events.py @@ -21,6 +21,15 @@ class AgentExecutionStartedEvent(CrewEvent): model_config = {"arbitrary_types_allowed": True} + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the agent + if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + self.source_fingerprint = self.agent.fingerprint.uuid_str + self.source_type = "agent" + if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + self.fingerprint_metadata = self.agent.fingerprint.metadata + class AgentExecutionCompletedEvent(CrewEvent): """Event emitted when an agent completes executing a task""" @@ -30,6 +39,15 @@ class AgentExecutionCompletedEvent(CrewEvent): output: str type: str = "agent_execution_completed" + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the agent + if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + self.source_fingerprint = self.agent.fingerprint.uuid_str + self.source_type = "agent" + if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + self.fingerprint_metadata = self.agent.fingerprint.metadata + class AgentExecutionErrorEvent(CrewEvent): """Event emitted when an agent encounters an error during execution""" @@ -38,3 +56,12 @@ class AgentExecutionErrorEvent(CrewEvent): task: Any error: str type: str = "agent_execution_error" + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the agent + if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + self.source_fingerprint = self.agent.fingerprint.uuid_str + self.source_type = "agent" + if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + self.fingerprint_metadata = self.agent.fingerprint.metadata diff --git a/src/crewai/utilities/events/base_events.py b/src/crewai/utilities/events/base_events.py index b29ae6fb6..52e600f5f 100644 --- a/src/crewai/utilities/events/base_events.py +++ b/src/crewai/utilities/events/base_events.py @@ -1,4 +1,5 @@ from datetime import datetime +from typing import Any, Dict, Optional from pydantic import BaseModel, Field @@ -8,3 +9,6 @@ class CrewEvent(BaseModel): timestamp: datetime = Field(default_factory=datetime.now) type: str + source_fingerprint: Optional[str] = None # UUID string of the source entity + source_type: Optional[str] = None # "agent", "task", "crew" + fingerprint_metadata: Optional[Dict[str, Any]] = None # Any relevant metadata diff --git a/src/crewai/utilities/events/crew_events.py b/src/crewai/utilities/events/crew_events.py index 13dfd8e34..4a10772a8 100644 --- a/src/crewai/utilities/events/crew_events.py +++ b/src/crewai/utilities/events/crew_events.py @@ -11,6 +11,16 @@ class CrewKickoffStartedEvent(CrewEvent): crew_name: Optional[str] inputs: Optional[Dict[str, Any]] type: str = "crew_kickoff_started" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewKickoffCompletedEvent(CrewEvent): @@ -19,6 +29,16 @@ class CrewKickoffCompletedEvent(CrewEvent): crew_name: Optional[str] output: Any type: str = "crew_kickoff_completed" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewKickoffFailedEvent(CrewEvent): @@ -27,6 +47,16 @@ class CrewKickoffFailedEvent(CrewEvent): error: str crew_name: Optional[str] type: str = "crew_kickoff_failed" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewTrainStartedEvent(CrewEvent): @@ -37,6 +67,16 @@ class CrewTrainStartedEvent(CrewEvent): filename: str inputs: Optional[Dict[str, Any]] type: str = "crew_train_started" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewTrainCompletedEvent(CrewEvent): @@ -46,6 +86,16 @@ class CrewTrainCompletedEvent(CrewEvent): n_iterations: int filename: str type: str = "crew_train_completed" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewTrainFailedEvent(CrewEvent): @@ -54,6 +104,16 @@ class CrewTrainFailedEvent(CrewEvent): error: str crew_name: Optional[str] type: str = "crew_train_failed" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewTestStartedEvent(CrewEvent): @@ -64,6 +124,16 @@ class CrewTestStartedEvent(CrewEvent): eval_llm: Optional[Union[str, Any]] inputs: Optional[Dict[str, Any]] type: str = "crew_test_started" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewTestCompletedEvent(CrewEvent): @@ -71,6 +141,16 @@ class CrewTestCompletedEvent(CrewEvent): crew_name: Optional[str] type: str = "crew_test_completed" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata class CrewTestFailedEvent(CrewEvent): @@ -79,3 +159,13 @@ class CrewTestFailedEvent(CrewEvent): error: str crew_name: Optional[str] type: str = "crew_test_failed" + crew: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the crew + if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: + self.fingerprint_metadata = self.crew.fingerprint.metadata diff --git a/src/crewai/utilities/events/task_events.py b/src/crewai/utilities/events/task_events.py index d81b7ce2d..7c7bb8964 100644 --- a/src/crewai/utilities/events/task_events.py +++ b/src/crewai/utilities/events/task_events.py @@ -1,4 +1,4 @@ -from typing import Optional +from typing import Any, Optional from crewai.tasks.task_output import TaskOutput from crewai.utilities.events.base_events import CrewEvent @@ -9,6 +9,16 @@ class TaskStartedEvent(CrewEvent): type: str = "task_started" context: Optional[str] + task: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the task + if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + self.source_fingerprint = self.task.fingerprint.uuid_str + self.source_type = "task" + if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + self.fingerprint_metadata = self.task.fingerprint.metadata class TaskCompletedEvent(CrewEvent): @@ -16,6 +26,16 @@ class TaskCompletedEvent(CrewEvent): output: TaskOutput type: str = "task_completed" + task: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the task + if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + self.source_fingerprint = self.task.fingerprint.uuid_str + self.source_type = "task" + if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + self.fingerprint_metadata = self.task.fingerprint.metadata class TaskFailedEvent(CrewEvent): @@ -23,6 +43,16 @@ class TaskFailedEvent(CrewEvent): error: str type: str = "task_failed" + task: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the task + if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + self.source_fingerprint = self.task.fingerprint.uuid_str + self.source_type = "task" + if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + self.fingerprint_metadata = self.task.fingerprint.metadata class TaskEvaluationEvent(CrewEvent): @@ -30,3 +60,13 @@ class TaskEvaluationEvent(CrewEvent): type: str = "task_evaluation" evaluation_type: str + task: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the task + if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + self.source_fingerprint = self.task.fingerprint.uuid_str + self.source_type = "task" + if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + self.fingerprint_metadata = self.task.fingerprint.metadata diff --git a/src/crewai/utilities/events/tool_usage_events.py b/src/crewai/utilities/events/tool_usage_events.py index e5027832c..d4202f563 100644 --- a/src/crewai/utilities/events/tool_usage_events.py +++ b/src/crewai/utilities/events/tool_usage_events.py @@ -1,5 +1,5 @@ from datetime import datetime -from typing import Any, Callable, Dict +from typing import Any, Callable, Dict, Optional from .base_events import CrewEvent @@ -14,9 +14,19 @@ class ToolUsageEvent(CrewEvent): tool_class: str run_attempts: int | None = None delegations: int | None = None + agent: Optional[Any] = None model_config = {"arbitrary_types_allowed": True} + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the agent + if self.agent and hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + self.source_fingerprint = self.agent.fingerprint.uuid_str + self.source_type = "agent" + if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + self.fingerprint_metadata = self.agent.fingerprint.metadata + class ToolUsageStartedEvent(ToolUsageEvent): """Event emitted when a tool execution is started""" @@ -63,3 +73,13 @@ class ToolExecutionErrorEvent(CrewEvent): tool_name: str tool_args: Dict[str, Any] tool_class: Callable + agent: Optional[Any] = None + + def __init__(self, **data): + super().__init__(**data) + # Set fingerprint data from the agent + if self.agent and hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + self.source_fingerprint = self.agent.fingerprint.uuid_str + self.source_type = "agent" + if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + self.fingerprint_metadata = self.agent.fingerprint.metadata From af7983be43d34685d18a974c9df673880a32f587 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 27 Mar 2025 08:12:47 +0530 Subject: [PATCH 28/41] Fixed Intent --- tests/crew_test.py | 48 +++++++++++++++++++++++----------------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/crew_test.py b/tests/crew_test.py index 694be09f4..43cb9f6ea 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -4069,41 +4069,41 @@ def test_crew_with_knowledge_sources_works_with_copy(): def test_crew_kickoff_for_each_works_with_manager_agent_copy(): researcher = Agent( - role="Researcher", - goal="Conduct thorough research and analysis on AI and AI agents", - backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", - allow_delegation=False - ) + role="Researcher", + goal="Conduct thorough research and analysis on AI and AI agents", + backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", + allow_delegation=False + ) writer = Agent( - role="Senior Writer", - goal="Create compelling content about AI and AI agents", - backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", - allow_delegation=False - ) + role="Senior Writer", + goal="Create compelling content about AI and AI agents", + backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", + allow_delegation=False + ) # Define task task = Task( - description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", - expected_output="5 bullet points, each with a paragraph and accompanying notes.", - ) + description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", + expected_output="5 bullet points, each with a paragraph and accompanying notes.", + ) # Define manager agent manager = Agent( - role="Project Manager", - goal="Efficiently manage the crew and ensure high-quality task completion", - backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", - allow_delegation=True - ) + role="Project Manager", + goal="Efficiently manage the crew and ensure high-quality task completion", + backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", + allow_delegation=True + ) # Instantiate crew with a custom manager crew = Crew( - agents=[researcher, writer], - tasks=[task], - manager_agent=manager, - process=Process.hierarchical, - verbose=True - ) + agents=[researcher, writer], + tasks=[task], + manager_agent=manager, + process=Process.hierarchical, + verbose=True + ) crew_copy = crew.copy() assert crew_copy.manager_agent is not None From 02f790ffcbc4e0d48e67d711b67415e71e841810 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Thu, 27 Mar 2025 08:14:07 +0530 Subject: [PATCH 29/41] Fixed Intent --- tests/crew_test.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/crew_test.py b/tests/crew_test.py index 43cb9f6ea..402e82d9b 100644 --- a/tests/crew_test.py +++ b/tests/crew_test.py @@ -4073,20 +4073,20 @@ def test_crew_kickoff_for_each_works_with_manager_agent_copy(): goal="Conduct thorough research and analysis on AI and AI agents", backstory="You're an expert researcher, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently researching for a new client.", allow_delegation=False - ) + ) writer = Agent( role="Senior Writer", goal="Create compelling content about AI and AI agents", backstory="You're a senior writer, specialized in technology, software engineering, AI, and startups. You work as a freelancer and are currently writing content for a new client.", allow_delegation=False - ) + ) # Define task task = Task( description="Generate a list of 5 interesting ideas for an article, then write one captivating paragraph for each idea that showcases the potential of a full article on this topic. Return the list of ideas with their paragraphs and your notes.", expected_output="5 bullet points, each with a paragraph and accompanying notes.", - ) + ) # Define manager agent manager = Agent( @@ -4094,7 +4094,7 @@ def test_crew_kickoff_for_each_works_with_manager_agent_copy(): goal="Efficiently manage the crew and ensure high-quality task completion", backstory="You're an experienced project manager, skilled in overseeing complex projects and guiding teams to success. Your role is to coordinate the efforts of the crew members, ensuring that each task is completed on time and to the highest standard.", allow_delegation=True - ) + ) # Instantiate crew with a custom manager crew = Crew( @@ -4103,7 +4103,7 @@ def test_crew_kickoff_for_each_works_with_manager_agent_copy(): manager_agent=manager, process=Process.hierarchical, verbose=True - ) + ) crew_copy = crew.copy() assert crew_copy.manager_agent is not None From 06950921e9a270d953cc9d3143af625a728d82f7 Mon Sep 17 00:00:00 2001 From: exiao Date: Thu, 27 Mar 2025 13:07:16 -0400 Subject: [PATCH 30/41] Update phoenix-observability.mdx --- docs/how-to/phoenix-observability.mdx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/how-to/phoenix-observability.mdx b/docs/how-to/phoenix-observability.mdx index e962f981d..c1815d0d6 100644 --- a/docs/how-to/phoenix-observability.mdx +++ b/docs/how-to/phoenix-observability.mdx @@ -130,7 +130,14 @@ Log into your Phoenix Cloud account and navigate to the project you specified in ![Example trace in Phoenix showing agent interactions](https://storage.googleapis.com/arize-assets/fixtures/crewai_traces.png) -## References -- [Phoenix Documentation](https://docs.arize.com/phoenix/) -- [CrewAI Documentation](https://docs.crewai.com/) +### Version Compatibility Information +- Python 3.8+ +- CrewAI >= 0.86.0 +- Arize Phoenix >= 7.0.1 +- OpenTelemetry SDK >= 1.31.0 + +- [Phoenix Documentation](https://docs.arize.com/phoenix/) - Overview of the Phoenix platform. +- [CrewAI Documentation](https://docs.crewai.com/) - Overview of the CrewAI framework. +- [OpenTelemetry Docs](https://opentelemetry.io/docs/) - OpenTelemetry guide +- [OpenInference GitHub](https://github.com/openinference/openinference) - Source code for OpenInference SDK. From b6c32b014c1846f636c3557eda55672de1333424 Mon Sep 17 00:00:00 2001 From: exiao Date: Thu, 27 Mar 2025 13:22:33 -0400 Subject: [PATCH 31/41] Update phoenix-observability.mdx --- docs/how-to/phoenix-observability.mdx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/how-to/phoenix-observability.mdx b/docs/how-to/phoenix-observability.mdx index c1815d0d6..0f101d1b8 100644 --- a/docs/how-to/phoenix-observability.mdx +++ b/docs/how-to/phoenix-observability.mdx @@ -50,7 +50,7 @@ os.environ["SERPER_API_KEY"] = SERPER_API_KEY ### Step 3: Initialize OpenTelemetry with Phoenix -Initialize the OpenInference OpenTelemetry instrumentation SDK to start capturing traces and send them to Phoenix. +Initialize the OpenInference OpenTelemetry instrumentation SDK to start capturing traces and send them to Phoenix. ```python from phoenix.otel import register @@ -137,6 +137,8 @@ Log into your Phoenix Cloud account and navigate to the project you specified in - Arize Phoenix >= 7.0.1 - OpenTelemetry SDK >= 1.31.0 + +### References - [Phoenix Documentation](https://docs.arize.com/phoenix/) - Overview of the Phoenix platform. - [CrewAI Documentation](https://docs.crewai.com/) - Overview of the CrewAI framework. - [OpenTelemetry Docs](https://opentelemetry.io/docs/) - OpenTelemetry guide From f845fac4da7ec0255f966bf5f5999e713f0535e5 Mon Sep 17 00:00:00 2001 From: Vini Brasil Date: Thu, 27 Mar 2025 15:42:11 -0300 Subject: [PATCH 32/41] Refactor event base classes (#2491) - Renamed `CrewEvent` to `BaseEvent` across the codebase for consistency - Created a `CrewBaseEvent` that automatically identifies fingerprints for DRY - Added a new `to_json()` method for serializing events --- docs/concepts/event-listener.mdx | 4 +- src/crewai/utilities/events/agent_events.py | 29 ++-- src/crewai/utilities/events/base_events.py | 18 ++- src/crewai/utilities/events/crew_events.py | 153 +++++------------- .../utilities/events/crewai_event_bus.py | 8 +- src/crewai/utilities/events/flow_events.py | 4 +- src/crewai/utilities/events/llm_events.py | 12 +- src/crewai/utilities/events/task_events.py | 38 +++-- .../utilities/events/tool_usage_events.py | 20 ++- .../serialization.py} | 26 +-- .../utilities/events/test_crewai_event_bus.py | 6 +- .../test_serialization.py} | 56 +++---- 12 files changed, 155 insertions(+), 219 deletions(-) rename src/crewai/{flow/state_utils.py => utilities/serialization.py} (76%) rename tests/{flow/test_state_utils.py => utilities/test_serialization.py} (74%) diff --git a/docs/concepts/event-listener.mdx b/docs/concepts/event-listener.mdx index 28825c8f4..d641687f0 100644 --- a/docs/concepts/event-listener.mdx +++ b/docs/concepts/event-listener.mdx @@ -13,7 +13,7 @@ CrewAI provides a powerful event system that allows you to listen for and react CrewAI uses an event bus architecture to emit events throughout the execution lifecycle. The event system is built on the following components: 1. **CrewAIEventsBus**: A singleton event bus that manages event registration and emission -2. **CrewEvent**: Base class for all events in the system +2. **BaseEvent**: Base class for all events in the system 3. **BaseEventListener**: Abstract base class for creating custom event listeners When specific actions occur in CrewAI (like a Crew starting execution, an Agent completing a task, or a tool being used), the system emits corresponding events. You can register handlers for these events to execute custom code when they occur. @@ -234,7 +234,7 @@ Each event handler receives two parameters: 1. **source**: The object that emitted the event 2. **event**: The event instance, containing event-specific data -The structure of the event object depends on the event type, but all events inherit from `CrewEvent` and include: +The structure of the event object depends on the event type, but all events inherit from `BaseEvent` and include: - **timestamp**: The time when the event was emitted - **type**: A string identifier for the event type diff --git a/src/crewai/utilities/events/agent_events.py b/src/crewai/utilities/events/agent_events.py index 3d325b41c..0bb6b4f38 100644 --- a/src/crewai/utilities/events/agent_events.py +++ b/src/crewai/utilities/events/agent_events.py @@ -4,13 +4,13 @@ from crewai.agents.agent_builder.base_agent import BaseAgent from crewai.tools.base_tool import BaseTool from crewai.tools.structured_tool import CrewStructuredTool -from .base_events import CrewEvent +from .base_events import BaseEvent if TYPE_CHECKING: from crewai.agents.agent_builder.base_agent import BaseAgent -class AgentExecutionStartedEvent(CrewEvent): +class AgentExecutionStartedEvent(BaseEvent): """Event emitted when an agent starts executing a task""" agent: BaseAgent @@ -24,14 +24,17 @@ class AgentExecutionStartedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata -class AgentExecutionCompletedEvent(CrewEvent): +class AgentExecutionCompletedEvent(BaseEvent): """Event emitted when an agent completes executing a task""" agent: BaseAgent @@ -42,14 +45,17 @@ class AgentExecutionCompletedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata -class AgentExecutionErrorEvent(CrewEvent): +class AgentExecutionErrorEvent(BaseEvent): """Event emitted when an agent encounters an error during execution""" agent: BaseAgent @@ -60,8 +66,11 @@ class AgentExecutionErrorEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata diff --git a/src/crewai/utilities/events/base_events.py b/src/crewai/utilities/events/base_events.py index 52e600f5f..46648500b 100644 --- a/src/crewai/utilities/events/base_events.py +++ b/src/crewai/utilities/events/base_events.py @@ -3,12 +3,26 @@ from typing import Any, Dict, Optional from pydantic import BaseModel, Field +from crewai.utilities.serialization import to_serializable -class CrewEvent(BaseModel): - """Base class for all crew events""" + +class BaseEvent(BaseModel): + """Base class for all events""" timestamp: datetime = Field(default_factory=datetime.now) type: str source_fingerprint: Optional[str] = None # UUID string of the source entity source_type: Optional[str] = None # "agent", "task", "crew" fingerprint_metadata: Optional[Dict[str, Any]] = None # Any relevant metadata + + def to_json(self, exclude: set[str] | None = None): + """ + Converts the event to a JSON-serializable dictionary. + + Args: + exclude (set[str], optional): Set of keys to exclude from the result. Defaults to None. + + Returns: + dict: A JSON-serializable dictionary. + """ + return to_serializable(self, exclude=exclude) diff --git a/src/crewai/utilities/events/crew_events.py b/src/crewai/utilities/events/crew_events.py index 4a10772a8..d73cd95d3 100644 --- a/src/crewai/utilities/events/crew_events.py +++ b/src/crewai/utilities/events/crew_events.py @@ -1,171 +1,102 @@ -from typing import Any, Dict, Optional, Union +from typing import TYPE_CHECKING, Any, Dict, Optional, Union -from pydantic import InstanceOf +from crewai.utilities.events.base_events import BaseEvent -from crewai.utilities.events.base_events import CrewEvent +if TYPE_CHECKING: + from crewai.crew import Crew +else: + Crew = Any -class CrewKickoffStartedEvent(CrewEvent): +class CrewBaseEvent(BaseEvent): + """Base class for crew events with fingerprint handling""" + + crew_name: Optional[str] + crew: Optional[Crew] = None + + def __init__(self, **data): + super().__init__(**data) + self.set_crew_fingerprint() + + def set_crew_fingerprint(self) -> None: + if self.crew and hasattr(self.crew, "fingerprint") and self.crew.fingerprint: + self.source_fingerprint = self.crew.fingerprint.uuid_str + self.source_type = "crew" + if ( + hasattr(self.crew.fingerprint, "metadata") + and self.crew.fingerprint.metadata + ): + self.fingerprint_metadata = self.crew.fingerprint.metadata + + def to_json(self, exclude: set[str] | None = None): + if exclude is None: + exclude = set() + exclude.add("crew") + return super().to_json(exclude=exclude) + + +class CrewKickoffStartedEvent(CrewBaseEvent): """Event emitted when a crew starts execution""" - crew_name: Optional[str] inputs: Optional[Dict[str, Any]] type: str = "crew_kickoff_started" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewKickoffCompletedEvent(CrewEvent): +class CrewKickoffCompletedEvent(CrewBaseEvent): """Event emitted when a crew completes execution""" - crew_name: Optional[str] output: Any type: str = "crew_kickoff_completed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewKickoffFailedEvent(CrewEvent): +class CrewKickoffFailedEvent(CrewBaseEvent): """Event emitted when a crew fails to complete execution""" error: str - crew_name: Optional[str] type: str = "crew_kickoff_failed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTrainStartedEvent(CrewEvent): +class CrewTrainStartedEvent(CrewBaseEvent): """Event emitted when a crew starts training""" - crew_name: Optional[str] n_iterations: int filename: str inputs: Optional[Dict[str, Any]] type: str = "crew_train_started" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTrainCompletedEvent(CrewEvent): +class CrewTrainCompletedEvent(CrewBaseEvent): """Event emitted when a crew completes training""" - crew_name: Optional[str] n_iterations: int filename: str type: str = "crew_train_completed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTrainFailedEvent(CrewEvent): +class CrewTrainFailedEvent(CrewBaseEvent): """Event emitted when a crew fails to complete training""" error: str - crew_name: Optional[str] type: str = "crew_train_failed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTestStartedEvent(CrewEvent): +class CrewTestStartedEvent(CrewBaseEvent): """Event emitted when a crew starts testing""" - crew_name: Optional[str] n_iterations: int eval_llm: Optional[Union[str, Any]] inputs: Optional[Dict[str, Any]] type: str = "crew_test_started" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTestCompletedEvent(CrewEvent): +class CrewTestCompletedEvent(CrewBaseEvent): """Event emitted when a crew completes testing""" - crew_name: Optional[str] type: str = "crew_test_completed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata -class CrewTestFailedEvent(CrewEvent): +class CrewTestFailedEvent(CrewBaseEvent): """Event emitted when a crew fails to complete testing""" error: str - crew_name: Optional[str] type: str = "crew_test_failed" - crew: Optional[Any] = None - - def __init__(self, **data): - super().__init__(**data) - # Set fingerprint data from the crew - if self.crew and hasattr(self.crew, 'fingerprint') and self.crew.fingerprint: - self.source_fingerprint = self.crew.fingerprint.uuid_str - self.source_type = "crew" - if hasattr(self.crew.fingerprint, 'metadata') and self.crew.fingerprint.metadata: - self.fingerprint_metadata = self.crew.fingerprint.metadata diff --git a/src/crewai/utilities/events/crewai_event_bus.py b/src/crewai/utilities/events/crewai_event_bus.py index 5df5ee689..9cde461ca 100644 --- a/src/crewai/utilities/events/crewai_event_bus.py +++ b/src/crewai/utilities/events/crewai_event_bus.py @@ -4,10 +4,10 @@ from typing import Any, Callable, Dict, List, Type, TypeVar, cast from blinker import Signal -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent from crewai.utilities.events.event_types import EventTypes -EventT = TypeVar("EventT", bound=CrewEvent) +EventT = TypeVar("EventT", bound=BaseEvent) class CrewAIEventsBus: @@ -30,7 +30,7 @@ class CrewAIEventsBus: def _initialize(self) -> None: """Initialize the event bus internal state""" self._signal = Signal("crewai_event_bus") - self._handlers: Dict[Type[CrewEvent], List[Callable]] = {} + self._handlers: Dict[Type[BaseEvent], List[Callable]] = {} def on( self, event_type: Type[EventT] @@ -59,7 +59,7 @@ class CrewAIEventsBus: return decorator - def emit(self, source: Any, event: CrewEvent) -> None: + def emit(self, source: Any, event: BaseEvent) -> None: """ Emit an event to all registered handlers diff --git a/src/crewai/utilities/events/flow_events.py b/src/crewai/utilities/events/flow_events.py index 8800b301b..7f48215e9 100644 --- a/src/crewai/utilities/events/flow_events.py +++ b/src/crewai/utilities/events/flow_events.py @@ -2,10 +2,10 @@ from typing import Any, Dict, Optional, Union from pydantic import BaseModel, ConfigDict -from .base_events import CrewEvent +from .base_events import BaseEvent -class FlowEvent(CrewEvent): +class FlowEvent(BaseEvent): """Base class for all flow events""" type: str diff --git a/src/crewai/utilities/events/llm_events.py b/src/crewai/utilities/events/llm_events.py index b92072340..07a17a48b 100644 --- a/src/crewai/utilities/events/llm_events.py +++ b/src/crewai/utilities/events/llm_events.py @@ -1,7 +1,7 @@ from enum import Enum from typing import Any, Dict, List, Optional, Union -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent class LLMCallType(Enum): @@ -11,9 +11,9 @@ class LLMCallType(Enum): LLM_CALL = "llm_call" -class LLMCallStartedEvent(CrewEvent): +class LLMCallStartedEvent(BaseEvent): """Event emitted when a LLM call starts - + Attributes: messages: Content can be either a string or a list of dictionaries that support multimodal content (text, images, etc.) @@ -26,7 +26,7 @@ class LLMCallStartedEvent(CrewEvent): available_functions: Optional[Dict[str, Any]] = None -class LLMCallCompletedEvent(CrewEvent): +class LLMCallCompletedEvent(BaseEvent): """Event emitted when a LLM call completes""" type: str = "llm_call_completed" @@ -34,14 +34,14 @@ class LLMCallCompletedEvent(CrewEvent): call_type: LLMCallType -class LLMCallFailedEvent(CrewEvent): +class LLMCallFailedEvent(BaseEvent): """Event emitted when a LLM call fails""" error: str type: str = "llm_call_failed" -class LLMStreamChunkEvent(CrewEvent): +class LLMStreamChunkEvent(BaseEvent): """Event emitted when a streaming chunk is received""" type: str = "llm_stream_chunk" diff --git a/src/crewai/utilities/events/task_events.py b/src/crewai/utilities/events/task_events.py index 7c7bb8964..1bf5baf8c 100644 --- a/src/crewai/utilities/events/task_events.py +++ b/src/crewai/utilities/events/task_events.py @@ -1,10 +1,10 @@ from typing import Any, Optional from crewai.tasks.task_output import TaskOutput -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent -class TaskStartedEvent(CrewEvent): +class TaskStartedEvent(BaseEvent): """Event emitted when a task starts""" type: str = "task_started" @@ -14,14 +14,17 @@ class TaskStartedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata -class TaskCompletedEvent(CrewEvent): +class TaskCompletedEvent(BaseEvent): """Event emitted when a task completes""" output: TaskOutput @@ -31,14 +34,17 @@ class TaskCompletedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata -class TaskFailedEvent(CrewEvent): +class TaskFailedEvent(BaseEvent): """Event emitted when a task fails""" error: str @@ -48,14 +54,17 @@ class TaskFailedEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata -class TaskEvaluationEvent(CrewEvent): +class TaskEvaluationEvent(BaseEvent): """Event emitted when a task evaluation is completed""" type: str = "task_evaluation" @@ -65,8 +74,11 @@ class TaskEvaluationEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the task - if hasattr(self.task, 'fingerprint') and self.task.fingerprint: + if hasattr(self.task, "fingerprint") and self.task.fingerprint: self.source_fingerprint = self.task.fingerprint.uuid_str self.source_type = "task" - if hasattr(self.task.fingerprint, 'metadata') and self.task.fingerprint.metadata: + if ( + hasattr(self.task.fingerprint, "metadata") + and self.task.fingerprint.metadata + ): self.fingerprint_metadata = self.task.fingerprint.metadata diff --git a/src/crewai/utilities/events/tool_usage_events.py b/src/crewai/utilities/events/tool_usage_events.py index d4202f563..8ab22f667 100644 --- a/src/crewai/utilities/events/tool_usage_events.py +++ b/src/crewai/utilities/events/tool_usage_events.py @@ -1,10 +1,10 @@ from datetime import datetime from typing import Any, Callable, Dict, Optional -from .base_events import CrewEvent +from .base_events import BaseEvent -class ToolUsageEvent(CrewEvent): +class ToolUsageEvent(BaseEvent): """Base event for tool usage tracking""" agent_key: str @@ -21,10 +21,13 @@ class ToolUsageEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if self.agent and hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if self.agent and hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata @@ -65,7 +68,7 @@ class ToolSelectionErrorEvent(ToolUsageEvent): type: str = "tool_selection_error" -class ToolExecutionErrorEvent(CrewEvent): +class ToolExecutionErrorEvent(BaseEvent): """Event emitted when a tool execution encounters an error""" error: Any @@ -78,8 +81,11 @@ class ToolExecutionErrorEvent(CrewEvent): def __init__(self, **data): super().__init__(**data) # Set fingerprint data from the agent - if self.agent and hasattr(self.agent, 'fingerprint') and self.agent.fingerprint: + if self.agent and hasattr(self.agent, "fingerprint") and self.agent.fingerprint: self.source_fingerprint = self.agent.fingerprint.uuid_str self.source_type = "agent" - if hasattr(self.agent.fingerprint, 'metadata') and self.agent.fingerprint.metadata: + if ( + hasattr(self.agent.fingerprint, "metadata") + and self.agent.fingerprint.metadata + ): self.fingerprint_metadata = self.agent.fingerprint.metadata diff --git a/src/crewai/flow/state_utils.py b/src/crewai/utilities/serialization.py similarity index 76% rename from src/crewai/flow/state_utils.py rename to src/crewai/utilities/serialization.py index 533bc5e00..c3c0c3d47 100644 --- a/src/crewai/flow/state_utils.py +++ b/src/crewai/utilities/serialization.py @@ -5,35 +5,17 @@ from typing import Any, Dict, List, Union from pydantic import BaseModel -from crewai.flow import Flow - SerializablePrimitive = Union[str, int, float, bool, None] Serializable = Union[ SerializablePrimitive, List["Serializable"], Dict[str, "Serializable"] ] -def export_state(flow: Flow) -> dict[str, Serializable]: - """Exports the Flow's internal state as JSON-compatible data structures. - - Performs a one-way transformation of a Flow's state into basic Python types - that can be safely serialized to JSON. To prevent infinite recursion with - circular references, the conversion is limited to a depth of 5 levels. - - Args: - flow: The Flow object whose state needs to be exported - - Returns: - dict[str, Any]: The transformed state using JSON-compatible Python - types. - """ - result = to_serializable(flow._state) - assert isinstance(result, dict) - return result - - def to_serializable( - obj: Any, exclude: set[str] | None = None, max_depth: int = 5, _current_depth: int = 0 + obj: Any, + exclude: set[str] | None = None, + max_depth: int = 5, + _current_depth: int = 0, ) -> Serializable: """Converts a Python object into a JSON-compatible representation. diff --git a/tests/utilities/events/test_crewai_event_bus.py b/tests/utilities/events/test_crewai_event_bus.py index 0dd8c8b34..315fbe138 100644 --- a/tests/utilities/events/test_crewai_event_bus.py +++ b/tests/utilities/events/test_crewai_event_bus.py @@ -1,10 +1,10 @@ from unittest.mock import Mock -from crewai.utilities.events.base_events import CrewEvent +from crewai.utilities.events.base_events import BaseEvent from crewai.utilities.events.crewai_event_bus import crewai_event_bus -class TestEvent(CrewEvent): +class TestEvent(BaseEvent): pass @@ -24,7 +24,7 @@ def test_specific_event_handler(): def test_wildcard_event_handler(): mock_handler = Mock() - @crewai_event_bus.on(CrewEvent) + @crewai_event_bus.on(BaseEvent) def handler(source, event): mock_handler(source, event) diff --git a/tests/flow/test_state_utils.py b/tests/utilities/test_serialization.py similarity index 74% rename from tests/flow/test_state_utils.py rename to tests/utilities/test_serialization.py index 48564f297..b1e042639 100644 --- a/tests/flow/test_state_utils.py +++ b/tests/utilities/test_serialization.py @@ -5,8 +5,7 @@ from unittest.mock import Mock import pytest from pydantic import BaseModel -from crewai.flow import Flow -from crewai.flow.state_utils import export_state, to_serializable, to_string +from crewai.utilities.serialization import to_serializable, to_string class Address(BaseModel): @@ -23,16 +22,6 @@ class Person(BaseModel): skills: List[str] -@pytest.fixture -def mock_flow(): - def create_flow(state): - flow = Mock(spec=Flow) - flow._state = state - return flow - - return create_flow - - @pytest.mark.parametrize( "test_input,expected", [ @@ -47,9 +36,8 @@ def mock_flow(): ({"nested": [1, [2, 3], {4, 5}]}, {"nested": [1, [2, 3], [4, 5]]}), ], ) -def test_basic_serialization(mock_flow, test_input, expected): - flow = mock_flow(test_input) - result = export_state(flow) +def test_basic_serialization(test_input, expected): + result = to_serializable(test_input) assert result == expected @@ -60,9 +48,8 @@ def test_basic_serialization(mock_flow, test_input, expected): (datetime(2024, 1, 1, 12, 30), "2024-01-01T12:30:00"), ], ) -def test_temporal_serialization(mock_flow, input_date, expected): - flow = mock_flow({"date": input_date}) - result = export_state(flow) +def test_temporal_serialization(input_date, expected): + result = to_serializable({"date": input_date}) assert result["date"] == expected @@ -75,9 +62,8 @@ def test_temporal_serialization(mock_flow, input_date, expected): ("normal", "value", str), ], ) -def test_dictionary_key_serialization(mock_flow, key, value, expected_key_type): - flow = mock_flow({key: value}) - result = export_state(flow) +def test_dictionary_key_serialization(key, value, expected_key_type): + result = to_serializable({key: value}) assert len(result) == 1 result_key = next(iter(result.keys())) assert isinstance(result_key, expected_key_type) @@ -91,14 +77,13 @@ def test_dictionary_key_serialization(mock_flow, key, value, expected_key_type): (str.upper, "upper"), ], ) -def test_callable_serialization(mock_flow, callable_obj, expected_in_result): - flow = mock_flow({"func": callable_obj}) - result = export_state(flow) +def test_callable_serialization(callable_obj, expected_in_result): + result = to_serializable({"func": callable_obj}) assert isinstance(result["func"], str) assert expected_in_result in result["func"].lower() -def test_pydantic_model_serialization(mock_flow): +def test_pydantic_model_serialization(): address = Address(street="123 Main St", city="Tech City", country="Pythonia") person = Person( @@ -109,23 +94,21 @@ def test_pydantic_model_serialization(mock_flow): skills=["Python", "Testing"], ) - flow = mock_flow( - { - "single_model": address, - "nested_model": person, - "model_list": [address, address], - "model_dict": {"home": address}, - } - ) + data = { + "single_model": address, + "nested_model": person, + "model_list": [address, address], + "model_dict": {"home": address}, + } - result = export_state(flow) + result = to_serializable(data) assert ( to_string(result) == '{"single_model": {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}, "nested_model": {"name": "John Doe", "age": 30, "address": {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}, "birthday": "1994-01-01", "skills": ["Python", "Testing"]}, "model_list": [{"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}, {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}], "model_dict": {"home": {"street": "123 Main St", "city": "Tech City", "country": "Pythonia"}}}' ) -def test_depth_limit(mock_flow): +def test_depth_limit(): """Test max depth handling with a deeply nested structure""" def create_nested(depth): @@ -134,8 +117,7 @@ def test_depth_limit(mock_flow): return {"next": create_nested(depth - 1)} deep_structure = create_nested(10) - flow = mock_flow(deep_structure) - result = export_state(flow) + result = to_serializable(deep_structure) assert result == { "next": { From 6e209d5d77fac3dfe2bb9496d2c4ffe7719dda7c Mon Sep 17 00:00:00 2001 From: lucasgomide Date: Thu, 27 Mar 2025 16:27:44 -0300 Subject: [PATCH 33/41] chore(deps): pin crewai-tools to compatible version ~=0.38.0 fixes [issue](https://github.com/crewAIInc/crewAI/issues/2390) --- pyproject.toml | 2 +- uv.lock | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0d7b9068e..799efacb8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ Documentation = "https://docs.crewai.com" Repository = "https://github.com/crewAIInc/crewAI" [project.optional-dependencies] -tools = ["crewai-tools>=0.37.0"] +tools = ["crewai-tools~=0.38.0"] embeddings = [ "tiktoken~=0.7.0" ] diff --git a/uv.lock b/uv.lock index 2bbe20efd..fea201520 100644 --- a/uv.lock +++ b/uv.lock @@ -1,4 +1,5 @@ version = 1 +revision = 1 requires-python = ">=3.10, <3.13" resolution-markers = [ "python_full_version < '3.11' and sys_platform == 'darwin'", @@ -694,7 +695,7 @@ requires-dist = [ { name = "blinker", specifier = ">=1.9.0" }, { name = "chromadb", specifier = ">=0.5.23" }, { name = "click", specifier = ">=8.1.7" }, - { name = "crewai-tools", marker = "extra == 'tools'", specifier = ">=0.37.0" }, + { name = "crewai-tools", marker = "extra == 'tools'", specifier = "~=0.38.0" }, { name = "docling", marker = "extra == 'docling'", specifier = ">=2.12.0" }, { name = "fastembed", marker = "extra == 'fastembed'", specifier = ">=0.4.1" }, { name = "instructor", specifier = ">=1.3.3" }, @@ -721,6 +722,7 @@ requires-dist = [ { name = "tomli-w", specifier = ">=1.1.0" }, { name = "uv", specifier = ">=0.4.25" }, ] +provides-extras = ["tools", "embeddings", "agentops", "fastembed", "pdfplumber", "pandas", "openpyxl", "mem0", "docling", "aisuite"] [package.metadata.requires-dev] dev = [ @@ -2973,7 +2975,6 @@ name = "nvidia-nccl-cu12" version = "2.20.5" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/bb/d09dda47c881f9ff504afd6f9ca4f502ded6d8fc2f572cacc5e39da91c28/nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_aarch64.whl", hash = "sha256:1fc150d5c3250b170b29410ba682384b14581db722b2531b0d8d33c595f33d01", size = 176238458 }, { url = "https://files.pythonhosted.org/packages/4b/2a/0a131f572aa09f741c30ccd45a8e56316e8be8dfc7bc19bf0ab7cfef7b19/nvidia_nccl_cu12-2.20.5-py3-none-manylinux2014_x86_64.whl", hash = "sha256:057f6bf9685f75215d0c53bf3ac4a10b3e6578351de307abad9e18a99182af56", size = 176249402 }, ] @@ -2983,7 +2984,6 @@ version = "12.6.85" source = { registry = "https://pypi.org/simple" } wheels = [ { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971 }, - { url = "https://files.pythonhosted.org/packages/31/db/dc71113d441f208cdfe7ae10d4983884e13f464a6252450693365e166dcf/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cf4eaa7d4b6b543ffd69d6abfb11efdeb2db48270d94dfd3a452c24150829e41", size = 19270338 }, ] [[package]] From 08a6a820719af7a0881f58a3876e146af62519a7 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Fri, 28 Mar 2025 22:08:15 +0530 Subject: [PATCH 34/41] Minor Changes --- docs/concepts/memory.mdx | 85 +++++++++++++++---- src/crewai/crew.py | 17 ++-- src/crewai/memory/user/user_memory.py | 8 ++ tests/memory/user_memory_test.py | 68 +++++++++++++++ tests/storage/test_mem0_storage.py | 114 ++++++++++++++++++++++++++ 5 files changed, 270 insertions(+), 22 deletions(-) create mode 100644 tests/memory/user_memory_test.py create mode 100644 tests/storage/test_mem0_storage.py diff --git a/docs/concepts/memory.mdx b/docs/concepts/memory.mdx index bb4e885cd..14153f2b0 100644 --- a/docs/concepts/memory.mdx +++ b/docs/concepts/memory.mdx @@ -164,7 +164,10 @@ crew = Crew( [Mem0](https://mem0.ai/) is a self-improving memory layer for LLM applications, enabling personalized AI experiences. -To include user-specific memory you can get your API key [here](https://app.mem0.ai/dashboard/api-keys) and refer the [docs](https://docs.mem0.ai/platform/quickstart#4-1-create-memories) for adding user preferences. + +### Using Mem0 API platform + +To include user-specific memory you can get your API key [here](https://app.mem0.ai/dashboard/api-keys) and refer the [docs](https://docs.mem0.ai/platform/quickstart#4-1-create-memories) for adding user preferences. In this case `user_memory` is set to `MemoryClient` from mem0. ```python Code @@ -175,18 +178,7 @@ from mem0 import MemoryClient # Set environment variables for Mem0 os.environ["MEM0_API_KEY"] = "m0-xx" -# Step 1: Record preferences based on past conversation or user input -client = MemoryClient() -messages = [ - {"role": "user", "content": "Hi there! I'm planning a vacation and could use some advice."}, - {"role": "assistant", "content": "Hello! I'd be happy to help with your vacation planning. What kind of destination do you prefer?"}, - {"role": "user", "content": "I am more of a beach person than a mountain person."}, - {"role": "assistant", "content": "That's interesting. Do you like hotels or Airbnb?"}, - {"role": "user", "content": "I like Airbnb more."}, -] -client.add(messages, user_id="john") - -# Step 2: Create a Crew with User Memory +# Step 1: Create a Crew with User Memory crew = Crew( agents=[...], @@ -197,11 +189,12 @@ crew = Crew( memory_config={ "provider": "mem0", "config": {"user_id": "john"}, + "user_memory : {}" #Set user_memory explicitly to a dictionary, we are working on this issue. }, ) ``` -## Memory Configuration Options +#### Additional Memory Configuration Options If you want to access a specific organization and project, you can set the `org_id` and `project_id` parameters in the memory configuration. ```python Code @@ -215,10 +208,74 @@ crew = Crew( memory_config={ "provider": "mem0", "config": {"user_id": "john", "org_id": "my_org_id", "project_id": "my_project_id"}, + "user_memory : {}" #Set user_memory explicitly to a dictionary, we are working on this issue. }, ) ``` +### Using Local Mem0 memory +If you want to use local mem0 memory, with a custom configuration, you can set a parameter `local_mem0_config` in the config itself. +If both os environment key is set and local_mem0_config is given, the API platform takes higher priority over the local configuration. +Check [this](https://docs.mem0.ai/open-source/python-quickstart#run-mem0-locally) mem0 local configuration docs for more understanding. +In this case `user_memory` is set to `Memory` from mem0. + + +```python Code +from crewai import Crew + + +#local mem0 config +config = { + "vector_store": { + "provider": "qdrant", + "config": { + "host": "localhost", + "port": 6333 + } + }, + "llm": { + "provider": "openai", + "config": { + "api_key": "your-api-key", + "model": "gpt-4" + } + }, + "embedder": { + "provider": "openai", + "config": { + "api_key": "your-api-key", + "model": "text-embedding-3-small" + } + }, + "graph_store": { + "provider": "neo4j", + "config": { + "url": "neo4j+s://your-instance", + "username": "neo4j", + "password": "password" + } + }, + "history_db_path": "/path/to/history.db", + "version": "v1.1", + "custom_fact_extraction_prompt": "Optional custom prompt for fact extraction for memory", + "custom_update_memory_prompt": "Optional custom prompt for update memory" +} + +crew = Crew( + agents=[...], + tasks=[...], + verbose=True, + memory=True, + memory_config={ + "provider": "mem0", + "config": {"user_id": "john", 'local_mem0_config': config}, + "user_memory : {}" #Set user_memory explicitly to a dictionary, we are working on this issue. + }, +) +``` + + + ## Additional Embedding Providers ### Using OpenAI embeddings (already default) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 0f6db8c4a..60f7c5677 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -290,24 +290,25 @@ class Crew(BaseModel): else EntityMemory(crew=self, embedder_config=self.embedder) ) if ( - self.memory_config and "user_memory" in self.memory_config + self.memory_config and "user_memory" in self.memory_config and self.memory_config.get('provider') == 'mem0' ): # Check for user_memory in config user_memory_config = self.memory_config["user_memory"] if isinstance( - user_memory_config, UserMemory - ): # Check if it is already an instance - self._user_memory = user_memory_config - elif isinstance( user_memory_config, dict ): # Check if it's a configuration dict self._user_memory = UserMemory( - crew=self, **user_memory_config - ) # Initialize with config + crew=self + ) else: raise TypeError( - "user_memory must be a UserMemory instance or a configuration dictionary" + "user_memory must be a configuration dictionary" ) else: + self._logger.log( + "warning", + "User memory initialization failed. For setup instructions, please refer to the memory documentation: https://docs.crewai.com/concepts/memory#integrating-mem0-for-enhanced-user-memory", + color="yellow" + ) self._user_memory = None # No user memory if not in config return self diff --git a/src/crewai/memory/user/user_memory.py b/src/crewai/memory/user/user_memory.py index 24e5fe035..1c710bb69 100644 --- a/src/crewai/memory/user/user_memory.py +++ b/src/crewai/memory/user/user_memory.py @@ -43,3 +43,11 @@ class UserMemory(Memory): score_threshold=score_threshold, ) return results + + def reset(self) -> None: + try: + self.storage.reset() + except Exception as e: + raise Exception( + f"An error occurred while resetting the user memory: {e}" + ) diff --git a/tests/memory/user_memory_test.py b/tests/memory/user_memory_test.py new file mode 100644 index 000000000..53514f638 --- /dev/null +++ b/tests/memory/user_memory_test.py @@ -0,0 +1,68 @@ + +from unittest.mock import MagicMock, patch + +import pytest +from mem0.memory.main import Memory + +from crewai.memory.user.user_memory import UserMemory +from crewai.memory.user.user_memory_item import UserMemoryItem + + +class MockCrew: + def __init__(self, memory_config): + self.memory_config = memory_config + +@pytest.fixture +def user_memory(): + """Fixture to create a UserMemory instance""" + crew = MockCrew( + memory_config={ + "provider": "mem0", + "config": {"user_id": "john"}, + "user_memory" : {} + } + ) + + user_memory = MagicMock(spec=UserMemory) + + with patch.object(Memory,'__new__',return_value=user_memory): + user_memory_instance = UserMemory(crew=crew) + + return user_memory_instance + +def test_save_and_search(user_memory): + memory = UserMemoryItem( + data="""test value test value test value test value test value test value + test value test value test value test value test value test value + test value test value test value test value test value test value""", + user="test_user", + metadata={"task": "test_task"}, + ) + + with patch.object(UserMemory, "save") as mock_save: + user_memory.save( + value=memory.data, + metadata=memory.metadata, + user=memory.user + ) + + mock_save.assert_called_once_with( + value=memory.data, + metadata=memory.metadata, + user=memory.user + ) + + expected_result = [ + { + "context": memory.data, + "metadata": {"agent": "test_agent"}, + "score": 0.95, + } + ] + expected_result = ["mocked_result"] + + # Use patch.object to mock UserMemory's search method + with patch.object(UserMemory, 'search', return_value=expected_result) as mock_search: + find = UserMemory.search("test value", score_threshold=0.01)[0] + mock_search.assert_called_once_with("test value", score_threshold=0.01) + assert find == expected_result[0] \ No newline at end of file diff --git a/tests/storage/test_mem0_storage.py b/tests/storage/test_mem0_storage.py new file mode 100644 index 000000000..e3d68092a --- /dev/null +++ b/tests/storage/test_mem0_storage.py @@ -0,0 +1,114 @@ +import os +from unittest.mock import MagicMock, patch + +import pytest +from mem0.client.main import MemoryClient +from mem0.memory.main import Memory + +from crewai.agent import Agent +from crewai.crew import Crew +from crewai.memory.storage.mem0_storage import Mem0Storage +from crewai.task import Task + + +# Define the class (if not already defined) +class MockCrew: + def __init__(self, memory_config): + self.memory_config = memory_config + + +@pytest.fixture +def mock_mem0_memory(): + """Fixture to create a mock Memory instance""" + mock_memory = MagicMock(spec=Memory) + return mock_memory + + +@pytest.fixture +def mem0_storage_with_mocked_config(mock_mem0_memory): + """Fixture to create a Mem0Storage instance with mocked dependencies""" + + # Patch the Memory class to return our mock + with patch('mem0.memory.main.Memory.from_config', return_value=mock_mem0_memory): + config = { + "vector_store": { + "provider": "mock_vector_store", + "config": { + "host": "localhost", + "port": 6333 + } + }, + "llm": { + "provider": "mock_llm", + "config": { + "api_key": "mock-api-key", + "model": "mock-model" + } + }, + "embedder": { + "provider": "mock_embedder", + "config": { + "api_key": "mock-api-key", + "model": "mock-model" + } + }, + "graph_store": { + "provider": "mock_graph_store", + "config": { + "url": "mock-url", + "username": "mock-user", + "password": "mock-password" + } + }, + "history_db_path": "/mock/path", + "version": "test-version", + "custom_fact_extraction_prompt": "mock prompt 1", + "custom_update_memory_prompt": "mock prompt 2" + } + + # Instantiate the class with memory_config + crew = MockCrew( + memory_config={ + "provider": "mem0", + "config": {"user_id": "test_user", "local_mem0_config": config}, + } + ) + + mem0_storage = Mem0Storage(type="short_term", crew=crew) + return mem0_storage + + +def test_mem0_storage_initialization(mem0_storage_with_mocked_config, mock_mem0_memory): + """Test that Mem0Storage initializes correctly with the mocked config""" + assert mem0_storage_with_mocked_config.memory_type == "short_term" + assert mem0_storage_with_mocked_config.memory is mock_mem0_memory + + +@pytest.fixture +def mock_mem0_memory_client(): + """Fixture to create a mock MemoryClient instance""" + mock_memory = MagicMock(spec=MemoryClient) + return mock_memory + + +@pytest.fixture +def mem0_storage_with_memory_client(mock_mem0_memory_client): + """Fixture to create a Mem0Storage instance with mocked dependencies""" + + # We need to patch the MemoryClient before it's instantiated + with patch.object(MemoryClient, '__new__', return_value=mock_mem0_memory_client): + crew = MockCrew( + memory_config={ + "provider": "mem0", + "config": {"user_id": "test_user", "api_key": "ABCDEFGH", "org_id": "my_org_id", "project_id": "my_project_id"}, + } + ) + + mem0_storage = Mem0Storage(type="short_term", crew=crew) + return mem0_storage + + +def test_mem0_storage_with_memory_client_initialization(mem0_storage_with_memory_client, mock_mem0_memory_client): + """Test Mem0Storage initialization with MemoryClient""" + assert mem0_storage_with_memory_client.memory_type == "short_term" + assert mem0_storage_with_memory_client.memory is mock_mem0_memory_client From 77fa1b18c72334dff65fa2ee39b60d1780bc08f4 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Fri, 28 Mar 2025 22:30:32 +0530 Subject: [PATCH 35/41] added early return --- src/crewai/memory/contextual/contextual_memory.py | 4 ++++ src/crewai/memory/storage/mem0_storage.py | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/crewai/memory/contextual/contextual_memory.py b/src/crewai/memory/contextual/contextual_memory.py index cdb9cf836..a9f657d8a 100644 --- a/src/crewai/memory/contextual/contextual_memory.py +++ b/src/crewai/memory/contextual/contextual_memory.py @@ -94,6 +94,10 @@ class ContextualMemory: Returns: str: Formatted user memories as bullet points, or an empty string if none found. """ + + if self.um is None: + return "" + user_memories = self.um.search(query) if not user_memories: return "" diff --git a/src/crewai/memory/storage/mem0_storage.py b/src/crewai/memory/storage/mem0_storage.py index 0319c6a8a..6c9ffc682 100644 --- a/src/crewai/memory/storage/mem0_storage.py +++ b/src/crewai/memory/storage/mem0_storage.py @@ -31,6 +31,7 @@ class Mem0Storage(Storage): mem0_api_key = config.get("api_key") or os.getenv("MEM0_API_KEY") mem0_org_id = config.get("org_id") mem0_project_id = config.get("project_id") + mem0_local_config = config.get("local_mem0_config") # Initialize MemoryClient or Memory based on the presence of the mem0_api_key if mem0_api_key: @@ -41,7 +42,10 @@ class Mem0Storage(Storage): else: self.memory = MemoryClient(api_key=mem0_api_key) else: - self.memory = Memory() # Fallback to Memory if no Mem0 API key is provided + if mem0_local_config and len(mem0_local_config): + self.memory = Memory.from_config(config) + else: + self.memory = Memory() def _sanitize_role(self, role: str) -> str: """ @@ -114,3 +118,7 @@ class Mem0Storage(Storage): agents = [self._sanitize_role(agent.role) for agent in agents] agents = "_".join(agents) return agents + + def reset(self): + if self.memory: + self.memory.reset() From e290064eccfa8b0e71068e31590c9d9e599f6832 Mon Sep 17 00:00:00 2001 From: Vidit-Ostwal Date: Fri, 28 Mar 2025 22:39:17 +0530 Subject: [PATCH 36/41] Fixes minor typo in memory docs --- docs/concepts/memory.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/concepts/memory.mdx b/docs/concepts/memory.mdx index 14153f2b0..f3f1812c2 100644 --- a/docs/concepts/memory.mdx +++ b/docs/concepts/memory.mdx @@ -189,7 +189,7 @@ crew = Crew( memory_config={ "provider": "mem0", "config": {"user_id": "john"}, - "user_memory : {}" #Set user_memory explicitly to a dictionary, we are working on this issue. + "user_memory" : {} #Set user_memory explicitly to a dictionary, we are working on this issue. }, ) ``` @@ -208,7 +208,7 @@ crew = Crew( memory_config={ "provider": "mem0", "config": {"user_id": "john", "org_id": "my_org_id", "project_id": "my_project_id"}, - "user_memory : {}" #Set user_memory explicitly to a dictionary, we are working on this issue. + "user_memory" : {} #Set user_memory explicitly to a dictionary, we are working on this issue. }, ) ``` @@ -269,7 +269,7 @@ crew = Crew( memory_config={ "provider": "mem0", "config": {"user_id": "john", 'local_mem0_config': config}, - "user_memory : {}" #Set user_memory explicitly to a dictionary, we are working on this issue. + "user_memory" : {} #Set user_memory explicitly to a dictionary, we are working on this issue. }, ) ``` From 3c2435030606da5c1c199bfa19d158ab669ffb2d Mon Sep 17 00:00:00 2001 From: Lucas Gomide Date: Mon, 31 Mar 2025 12:27:36 -0300 Subject: [PATCH 37/41] fix: remove logs we don't need to see from UserMemory initializion (#2497) --- src/crewai/crew.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/src/crewai/crew.py b/src/crewai/crew.py index 60f7c5677..b5f3e3ff5 100644 --- a/src/crewai/crew.py +++ b/src/crewai/crew.py @@ -290,25 +290,18 @@ class Crew(BaseModel): else EntityMemory(crew=self, embedder_config=self.embedder) ) if ( - self.memory_config and "user_memory" in self.memory_config and self.memory_config.get('provider') == 'mem0' + self.memory_config + and "user_memory" in self.memory_config + and self.memory_config.get("provider") == "mem0" ): # Check for user_memory in config user_memory_config = self.memory_config["user_memory"] if isinstance( user_memory_config, dict ): # Check if it's a configuration dict - self._user_memory = UserMemory( - crew=self - ) + self._user_memory = UserMemory(crew=self) else: - raise TypeError( - "user_memory must be a configuration dictionary" - ) + raise TypeError("user_memory must be a configuration dictionary") else: - self._logger.log( - "warning", - "User memory initialization failed. For setup instructions, please refer to the memory documentation: https://docs.crewai.com/concepts/memory#integrating-mem0-for-enhanced-user-memory", - color="yellow" - ) self._user_memory = None # No user memory if not in config return self @@ -1159,7 +1152,7 @@ class Crew(BaseModel): def copy(self): """ Creates a deep copy of the Crew instance. - + Returns: Crew: A new instance with copied components """ @@ -1181,7 +1174,6 @@ class Crew(BaseModel): "knowledge", "manager_agent", "manager_llm", - } cloned_agents = [agent.copy() for agent in self.agents] From 63ef3918dd4cac2e949d03300657b9c019c7122d Mon Sep 17 00:00:00 2001 From: Lucas Gomide Date: Tue, 1 Apr 2025 12:45:45 -0300 Subject: [PATCH 38/41] feat: cleanup Pydantic warning (#2507) A several warnings were addressed following by https://docs.pydantic.dev/2.10/migration --- src/crewai/tools/base_tool.py | 15 +++++++-------- src/crewai/utilities/converter.py | 5 +++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/crewai/tools/base_tool.py b/src/crewai/tools/base_tool.py index b3c0f997c..dc69b02a2 100644 --- a/src/crewai/tools/base_tool.py +++ b/src/crewai/tools/base_tool.py @@ -7,29 +7,27 @@ from pydantic import ( BaseModel, ConfigDict, Field, - PydanticDeprecatedSince20, create_model, - validator, + field_validator, ) from pydantic import BaseModel as PydanticBaseModel from crewai.tools.structured_tool import CrewStructuredTool -# Ignore all "PydanticDeprecatedSince20" warnings globally -warnings.filterwarnings("ignore", category=PydanticDeprecatedSince20) - class BaseTool(BaseModel, ABC): class _ArgsSchemaPlaceholder(PydanticBaseModel): pass - model_config = ConfigDict() + model_config = ConfigDict(arbitrary_types_allowed=True) name: str """The unique name of the tool that clearly communicates its purpose.""" description: str """Used to tell the model how/when/why to use the tool.""" - args_schema: Type[PydanticBaseModel] = Field(default_factory=_ArgsSchemaPlaceholder) + args_schema: Type[PydanticBaseModel] = Field( + default_factory=_ArgsSchemaPlaceholder, validate_default=True + ) """The schema for the arguments that the tool accepts.""" description_updated: bool = False """Flag to check if the description has been updated.""" @@ -38,7 +36,8 @@ class BaseTool(BaseModel, ABC): result_as_answer: bool = False """Flag to check if the tool should be the final agent answer.""" - @validator("args_schema", always=True, pre=True) + @field_validator("args_schema", mode="before") + @classmethod def _default_args_schema( cls, v: Type[PydanticBaseModel] ) -> Type[PydanticBaseModel]: diff --git a/src/crewai/utilities/converter.py b/src/crewai/utilities/converter.py index 991185f4a..b16677ace 100644 --- a/src/crewai/utilities/converter.py +++ b/src/crewai/utilities/converter.py @@ -287,8 +287,9 @@ def generate_model_description(model: Type[BaseModel]) -> str: else: return str(field_type) - fields = model.__annotations__ + fields = model.model_fields field_descriptions = [ - f'"{name}": {describe_field(type_)}' for name, type_ in fields.items() + f'"{name}": {describe_field(field.annotation)}' + for name, field in fields.items() ] return "{\n " + ",\n ".join(field_descriptions) + "\n}" From b0f9637662dda1b182ef5a7ebfddab57560ca66e Mon Sep 17 00:00:00 2001 From: theadityarao <63926883+theadityarao@users.noreply.github.com> Date: Tue, 1 Apr 2025 23:01:22 +0530 Subject: [PATCH 39/41] fix documentation for "Using Crews and Flows Together" (#2490) * Update README.md * Update README.md * Update README.md * Update README.md --------- Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> --- README.md | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b44ff6f4f..a1bf63645 100644 --- a/README.md +++ b/README.md @@ -401,11 +401,16 @@ You can test different real life examples of AI crews in the [CrewAI-examples re ### Using Crews and Flows Together -CrewAI's power truly shines when combining Crews with Flows to create sophisticated automation pipelines. Here's how you can orchestrate multiple Crews within a Flow: +CrewAI's power truly shines when combining Crews with Flows to create sophisticated automation pipelines. +CrewAI flows support logical operators like `or_` and `and_` to combine multiple conditions. This can be used with `@start`, `@listen`, or `@router` decorators to create complex triggering conditions. +- `or_`: Triggers when any of the specified conditions are met. +- `and_`Triggers when all of the specified conditions are met. + +Here's how you can orchestrate multiple Crews within a Flow: ```python -from crewai.flow.flow import Flow, listen, start, router -from crewai import Crew, Agent, Task +from crewai.flow.flow import Flow, listen, start, router, or_ +from crewai import Crew, Agent, Task, Process from pydantic import BaseModel # Define structured state for precise control @@ -479,7 +484,7 @@ class AdvancedAnalysisFlow(Flow[MarketState]): ) return strategy_crew.kickoff() - @listen("medium_confidence", "low_confidence") + @listen(or_("medium_confidence", "low_confidence")) def request_additional_analysis(self): self.state.recommendations.append("Gather more data") return "Additional analysis required" From bce4bb5c4e734eb21df4314a3beb73e158fd84e1 Mon Sep 17 00:00:00 2001 From: exiao Date: Tue, 1 Apr 2025 14:51:01 -0400 Subject: [PATCH 40/41] Update docs.json --- docs/docs.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs.json b/docs/docs.json index dc3acdaa6..d2d7d629e 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -103,14 +103,15 @@ { "group": "Agent Monitoring & Observability", "pages": [ - "how-to/weave-integration", "how-to/agentops-observability", "how-to/langfuse-observability", "how-to/langtrace-observability", "how-to/mlflow-observability", "how-to/openlit-observability", "how-to/opik-observability", - "how-to/portkey-observability" + "how-to/phoenix-observability", + "how-to/portkey-observability", + "how-to/weave-integration" ] }, { From 9b51e1174c49b0496a307ebd724a3d45d0202fd0 Mon Sep 17 00:00:00 2001 From: Orce MARINKOVSKI Date: Wed, 2 Apr 2025 06:54:35 +0200 Subject: [PATCH 41/41] fix expected output (#2498) fix expected output. missing expected_output on task throws errors Co-authored-by: Lorenze Jay <63378463+lorenzejay@users.noreply.github.com> --- docs/how-to/kickoff-async.mdx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/how-to/kickoff-async.mdx b/docs/how-to/kickoff-async.mdx index 81300b19b..e3b92dd28 100644 --- a/docs/how-to/kickoff-async.mdx +++ b/docs/how-to/kickoff-async.mdx @@ -92,12 +92,14 @@ coding_agent = Agent( # Create tasks that require code execution task_1 = Task( description="Analyze the first dataset and calculate the average age of participants. Ages: {ages}", - agent=coding_agent + agent=coding_agent, + expected_output="The average age of the participants." ) task_2 = Task( description="Analyze the second dataset and calculate the average age of participants. Ages: {ages}", - agent=coding_agent + agent=coding_agent, + expected_output="The average age of the participants." ) # Create two crews and add tasks