From 3f35a0a44e6b444bab890b905f2ea11e23da78d6 Mon Sep 17 00:00:00 2001 From: LEE KYU WON Date: Fri, 19 Jul 2024 07:48:22 +0900 Subject: [PATCH 01/28] Update wait-user-input.ipynb (#1065) --- examples/human_in_the_loop/wait-user-input.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/human_in_the_loop/wait-user-input.ipynb b/examples/human_in_the_loop/wait-user-input.ipynb index aad39d623..a08b0c114 100644 --- a/examples/human_in_the_loop/wait-user-input.ipynb +++ b/examples/human_in_the_loop/wait-user-input.ipynb @@ -155,7 +155,7 @@ "\n", "builder = StateGraph(State)\n", "builder.add_node(\"step_1\", step_1)\n", - "builder.add_node(\"human_feedback\", step_2)\n", + "builder.add_node(\"human_feedback\", human_feedback)\n", "builder.add_node(\"step_3\", step_3)\n", "builder.add_edge(START, \"step_1\")\n", "builder.add_edge(\"step_1\", \"human_feedback\")\n", From 56238036d760a49ee0608e35b7f666e2f6659e1e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 09:29:12 -0700 Subject: [PATCH 02/28] WIP: Split out loop logic from Pregel class --- libs/langgraph/langgraph/checkpoint/base.py | 3 +- libs/langgraph/langgraph/pregel/__init__.py | 737 ++------------------ libs/langgraph/langgraph/pregel/algo.py | 413 +++++++++++ libs/langgraph/langgraph/pregel/debug.py | 2 +- libs/langgraph/langgraph/pregel/loop.py | 282 ++++++++ libs/langgraph/langgraph/pregel/types.py | 9 + libs/langgraph/tests/test_prebuilt.py | 2 +- libs/langgraph/tests/test_pregel.py | 2 - libs/langgraph/tests/test_pregel_async.py | 2 - 9 files changed, 785 insertions(+), 667 deletions(-) create mode 100644 libs/langgraph/langgraph/pregel/algo.py create mode 100644 libs/langgraph/langgraph/pregel/loop.py diff --git a/libs/langgraph/langgraph/checkpoint/base.py b/libs/langgraph/langgraph/checkpoint/base.py index 6b147f3e3..2f73b52a6 100644 --- a/libs/langgraph/langgraph/checkpoint/base.py +++ b/libs/langgraph/langgraph/checkpoint/base.py @@ -25,6 +25,7 @@ from langgraph.serde.base import SerializerProtocol from langgraph.serde.jsonplus import JsonPlusSerializer V = TypeVar("V", int, float, str) +PendingWrite = Tuple[str, str, Any] # Marked as total=False to allow for future expansion. @@ -118,7 +119,7 @@ class CheckpointTuple(NamedTuple): checkpoint: Checkpoint metadata: CheckpointMetadata parent_config: Optional[RunnableConfig] = None - pending_writes: Optional[List[Tuple[str, str, Any]]] = None + pending_writes: Optional[List[PendingWrite]] = None CheckpointThreadId = ConfigurableFieldSpec( diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index d0d1c461d..5b1d5b9f1 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -2,9 +2,8 @@ from __future__ import annotations import asyncio import concurrent.futures -import json import time -from collections import defaultdict, deque +from collections import deque from functools import partial from inspect import signature from typing import ( @@ -14,7 +13,6 @@ from typing import ( Callable, Dict, Iterator, - Literal, Mapping, Optional, Sequence, @@ -26,7 +24,6 @@ from typing import ( ) from uuid import UUID, uuid5 -from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager from langchain_core.globals import get_debug from langchain_core.load.dump import dumpd from langchain_core.pydantic_v1 import BaseModel, Field, root_validator @@ -41,7 +38,6 @@ from langchain_core.runnables.config import ( ensure_config, get_async_callback_manager_for_config, get_callback_manager_for_config, - merge_configs, patch_config, ) from langchain_core.runnables.utils import ( @@ -54,7 +50,6 @@ from typing_extensions import Self from langgraph.channels.base import ( BaseChannel, - EmptyChannelError, ) from langgraph.channels.context import Context from langgraph.channels.manager import ( @@ -64,7 +59,6 @@ from langgraph.channels.manager import ( ) from langgraph.checkpoint.base import ( BaseCheckpointSaver, - Checkpoint, CheckpointMetadata, copy_checkpoint, empty_checkpoint, @@ -73,18 +67,21 @@ from langgraph.constants import ( CONFIG_KEY_READ, CONFIG_KEY_SEND, INTERRUPT, - TAG_HIDDEN, - TASKS, - Send, ) from langgraph.errors import GraphRecursionError, InvalidUpdateError from langgraph.managed.base import ( AsyncManagedValuesManager, - ManagedValueMapping, ManagedValuesManager, ManagedValueSpec, is_managed_value, ) +from langgraph.pregel.algo import ( + apply_writes, + increment, + local_read, + prepare_next_tasks, + should_interrupt, +) from langgraph.pregel.debug import ( map_debug_checkpoint, map_debug_task_results, @@ -93,23 +90,22 @@ from langgraph.pregel.debug import ( print_step_tasks, print_step_writes, ) -from langgraph.pregel.executor import AsyncBackgroundExecutor, BackgroundExecutor +from langgraph.pregel.executor import AsyncBackgroundExecutor from langgraph.pregel.io import ( map_input, map_output_updates, map_output_values, - read_channel, read_channels, single, ) -from langgraph.pregel.log import logger +from langgraph.pregel.loop import PregelLoop from langgraph.pregel.read import PregelNode from langgraph.pregel.retry import RetryPolicy, arun_with_retry, run_with_retry from langgraph.pregel.types import ( All, PregelExecutableTask, - PregelTaskDescription, StateSnapshot, + StreamMode, ) from langgraph.pregel.validate import validate_graph, validate_keys from langgraph.pregel.write import ChannelWrite, ChannelWriteEntry @@ -196,16 +192,6 @@ class Channel: ) -StreamMode = Literal["values", "updates", "debug"] -"""How the stream method should emit outputs. - -- 'values': Emit all values of the state for each step. -- 'updates': Emit only the node name(s) and updates - that were returned by the node(s) **after** each step. -- 'debug': Emit debug events for each step. -""" - - class Pregel( RunnableSerializable[Union[dict[str, Any], Any], Union[dict[str, Any], Any]] ): @@ -378,7 +364,7 @@ class Pregel( ) as channels, ManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = _prepare_next_tasks( + _, next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -410,7 +396,7 @@ class Pregel( ) as channels, AsyncManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = _prepare_next_tasks( + _, next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -452,7 +438,7 @@ class Pregel( ) as channels, ManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = _prepare_next_tasks( + _, next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -498,7 +484,7 @@ class Pregel( ) as channels, AsyncManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = _prepare_next_tasks( + _, next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -583,13 +569,13 @@ class Pregel( # deque.extend is thread-safe CONFIG_KEY_SEND: task.writes.extend, CONFIG_KEY_READ: partial( - _local_read, checkpoint, channels, task.writes, config + local_read, checkpoint, channels, task.writes, config ), }, ), ) # apply to checkpoint and save - _apply_writes( + apply_writes( checkpoint, channels, task.writes, self.checkpointer.get_next_version ) step = saved.metadata.get("step", -2) + 1 if saved else -1 @@ -675,13 +661,13 @@ class Pregel( # deque.extend is thread-safe CONFIG_KEY_SEND: task.writes.extend, CONFIG_KEY_READ: partial( - _local_read, checkpoint, channels, task.writes, config + local_read, checkpoint, channels, task.writes, config ), }, ), ) # apply to checkpoint and save - _apply_writes( + apply_writes( checkpoint, channels, task.writes, self.checkpointer.get_next_version ) step = saved.metadata.get("step", -2) + 1 if saved else -1 @@ -711,7 +697,6 @@ class Pregel( config: Optional[RunnableConfig] = None, *, stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None, - input_keys: Optional[Union[str, Sequence[str]]] = None, output_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, @@ -729,10 +714,6 @@ class Pregel( output_keys = self.stream_channels_asis else: validate_keys(output_keys, self.channels) - if input_keys is None: - input_keys = self.input_channels - else: - validate_keys(input_keys, self.channels) interrupt_before = interrupt_before or self.interrupt_before_nodes interrupt_after = interrupt_after or self.interrupt_after_nodes stream_mode = stream_mode if stream_mode is not None else self.stream_mode @@ -744,7 +725,6 @@ class Pregel( return ( debug, stream_mode, - input_keys, output_keys, interrupt_before, interrupt_after, @@ -757,7 +737,6 @@ class Pregel( *, stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None, output_keys: Optional[Union[str, Sequence[str]]] = None, - input_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, @@ -848,214 +827,68 @@ class Pregel( ( debug, stream_modes, - input_keys, output_keys, interrupt_before, interrupt_after, ) = self._defaults( config, stream_mode=stream_mode, - input_keys=input_keys, output_keys=output_keys, interrupt_before=interrupt_before, interrupt_after=interrupt_after, debug=debug, ) - # copy nodes to ignore mutations during execution - processes = {**self.nodes} - # get checkpoint from saver, or create an empty one - saved = self.checkpointer.get_tuple(config) if self.checkpointer else None - checkpoint = saved.checkpoint if saved else empty_checkpoint() - # merge configurable fields with previous checkpoint config - checkpoint_config = config - if saved: - checkpoint_config = { - **config, - **saved.config, - "configurable": { - **config.get("configurable", {}), - **saved.config["configurable"], - }, - } - - start = saved.metadata.get("step", -2) + 1 if saved else -1 # create channels from checkpoint - with BackgroundExecutor(config) as submit, ChannelsManager( - self.channels, checkpoint, config - ) as channels, ManagedValuesManager( - self.managed_values_dict, config, self - ) as managed: - - def put_writes(task_id: str, writes: Sequence[tuple[str, Any]]) -> None: - if self.checkpointer is not None: - submit( - self.checkpointer.put_writes, - { - **checkpoint_config, - "configurable": { - **checkpoint_config["configurable"], - "thread_ts": checkpoint["id"], - }, - }, - writes, - task_id, - ) - - def put_checkpoint(metadata: CheckpointMetadata) -> Iterator[Any]: - nonlocal checkpoint, checkpoint_config, channels - - if self.checkpointer is None: - return - if debug: - print_step_checkpoint( - metadata["step"], channels, self.stream_channels_list - ) - - # create new checkpoint - checkpoint = create_checkpoint( - checkpoint, channels, metadata["step"] - ) - # save it, without blocking - submit( - self.checkpointer.put, - checkpoint_config, - copy_checkpoint(checkpoint), - metadata, - ) - # update checkpoint config - checkpoint_config = { - **checkpoint_config, - "configurable": { - **checkpoint_config["configurable"], - "thread_ts": checkpoint["id"], - }, - } - # yield debug checkpoint event - if "debug" in stream_modes: - yield from _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_checkpoint( - metadata["step"], - checkpoint_config, - channels, - self.stream_channels_asis, - metadata, - ), - ) - - # map inputs to channel updates - if input_writes := deque(map_input(input_keys, input)): - # discard any unfinished tasks from previous checkpoint - checkpoint, _ = _prepare_next_tasks( - checkpoint, - processes, - channels, - managed, - config, - -1, - for_execution=True, - get_next_version=( - self.checkpointer.get_next_version - if self.checkpointer - else _increment - ), - ) - # apply input writes - _apply_writes( - checkpoint, - channels, - input_writes, - ( - self.checkpointer.get_next_version - if self.checkpointer - else _increment - ), - ) - # save input checkpoint - yield from put_checkpoint( - { - "source": "input", - "step": start, - "writes": input, - } - ) - # increment start to 0 - start += 1 - else: - # no input is taken as signal to proceed past previous interrupt - checkpoint = copy_checkpoint(checkpoint) - for k in channels: - if k in checkpoint["channel_versions"]: - version = checkpoint["channel_versions"][k] - checkpoint["versions_seen"][INTERRUPT][k] = version - + with PregelLoop( + input, config=config, checkpointer=self.checkpointer, graph=self + ) as loop: # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates # channel updates from step N are only visible in step N+1 # channels are guaranteed to be immutable for the duration of the step, # with channel updates applied only at the transition between steps - stop = start + config["recursion_limit"] + 1 - for step in range(start, stop): - next_checkpoint, next_tasks = _prepare_next_tasks( - checkpoint, - processes, - channels, - managed, - config, - step, - for_execution=True, - manager=run_manager, - get_next_version=( - self.checkpointer.get_next_version - if self.checkpointer - else _increment - ), - ) - - # assign pending writes to tasks - if saved and saved.pending_writes: - for task in next_tasks: - task.writes.extend( - (c, v) - for tid, c, v in saved.pending_writes - if tid == task.id - ) - - # if no more tasks, we're done - if not next_tasks: - if step == start: - raise ValueError("No tasks to run in graph.") - else: - break - - # before execution, check if we should interrupt - if _should_interrupt( - checkpoint, - interrupt_before, - self.stream_channels_list, - next_tasks, - ): - break - else: - checkpoint = next_checkpoint + while loop.tick( + interrupt_before=interrupt_before, interrupt_after=interrupt_after + ): + # print debug output + if self.debug: + print_step_checkpoint( + loop.checkpoint_metadata, + loop.channels, + self.stream_channels_list, + ) + # emit debug output + if self.checkpointer and "debug" in stream_modes: + yield from _with_mode( + "debug", + isinstance(stream_mode, list), + map_debug_checkpoint( + loop.checkpoint_metadata["step"], + loop.config, + loop.channels, + self.stream_channels_asis, + loop.checkpoint_metadata, + ), + ) if debug: - print_step_tasks(step, next_tasks) + print_step_tasks(loop.checkpoint_metadata["step"], loop.tasks) if "debug" in stream_modes: yield from _with_mode( "debug", isinstance(stream_mode, list), - map_debug_tasks(step, next_tasks), + map_debug_tasks( + loop.checkpoint_metadata["step"], loop.tasks + ), ) # execute tasks, and wait for one to fail or all to finish. # each task is independent from all other concurrent tasks # yield updates/debug output as each task finishes futures = { - submit(run_with_retry, task, self.retry_policy): task - for task in next_tasks + loop.submit(run_with_retry, task, self.retry_policy): task + for task in loop.tasks if not task.writes } end_time = ( @@ -1084,10 +917,8 @@ class Pregel( # exception will be handled in panic_or_proceed futures.clear() else: - # save task writes to checkpointer, unless this - # is the single or last task in this step - if futures: - put_writes(task.id, task.writes) + # save task writes to checkpointer + loop.put_writes(task.id, task.writes) # yield updates output for the finished task if "updates" in stream_modes: yield from _with_mode( @@ -1100,7 +931,9 @@ class Pregel( "debug", isinstance(stream_mode, list), map_debug_task_results( - step, [task], self.stream_channels_list + loop.checkpoint_metadata["step"], + [task], + self.stream_channels_list, ), ) else: @@ -1108,66 +941,32 @@ class Pregel( del fut, task # panic on failure or timeout - _panic_or_proceed(done, inflight, step) + _panic_or_proceed(done, inflight, loop.checkpoint_metadata["step"]) # don't keep futures around in memory longer than needed del done, inflight, futures # combine pending writes from all tasks pending_writes = deque[tuple[str, Any]]() - for task in next_tasks: + for task in loop.tasks: pending_writes.extend(task.writes) if debug: print_step_writes( - step, pending_writes, self.stream_channels_list + loop.checkpoint_metadata["step"], + pending_writes, + self.stream_channels_list, ) - # apply writes to channels - _apply_writes( - checkpoint, - channels, - pending_writes, - ( - self.checkpointer.get_next_version - if self.checkpointer - else _increment - ), - ) - # yield values output if "values" in stream_modes: yield from _with_mode( "values", isinstance(stream_mode, list), - map_output_values(output_keys, pending_writes, channels), - ) - - # save end of step checkpoint - yield from put_checkpoint( - { - "source": "loop", - "step": step, - "writes": ( - single(map_output_updates(output_keys, next_tasks)) - if self.stream_mode == "updates" - else single( - map_output_values( - output_keys, pending_writes, channels - ), - ) + map_output_values( + output_keys, pending_writes, loop.channels ), - } - ) - - # after execution, check if we should interrupt - if _should_interrupt( - checkpoint, - interrupt_after, - self.stream_channels_list, - next_tasks, - ): - break - else: + ) + if loop.status == "out_of_steps": raise GraphRecursionError( f"Recursion limit of {config['recursion_limit']} reached" "without hitting a stop condition. You can increase the " @@ -1175,7 +974,7 @@ class Pregel( ) # set final channel values as run output - run_manager.on_chain_end(read_channels(channels, output_keys)) + run_manager.on_chain_end(read_channels(loop.channels, output_keys)) except BaseException as e: run_manager.on_chain_error(e) raise @@ -1187,7 +986,6 @@ class Pregel( *, stream_mode: Optional[Union[StreamMode, list[StreamMode]]] = None, output_keys: Optional[Union[str, Sequence[str]]] = None, - input_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, @@ -1204,7 +1002,6 @@ class Pregel( Output is a dict with the node name as key and the updated values as value. debug: Emit debug events for each step. output_keys: The keys to stream, defaults to all non-context channels. - input_keys: The keys to use from the input, defaults to all input channels. interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. debug: Whether to print debug information during execution, defaults to False. @@ -1288,14 +1085,12 @@ class Pregel( ( debug, stream_modes, - input_keys, output_keys, interrupt_before, interrupt_after, ) = self._defaults( config, stream_mode=stream_mode, - input_keys=input_keys, output_keys=output_keys, interrupt_before=interrupt_before, interrupt_after=interrupt_after, @@ -1391,9 +1186,9 @@ class Pregel( ) # map inputs to channel updates - if input_writes := deque(map_input(input_keys, input)): + if input_writes := deque(map_input(self.input_channels, input)): # discard any unfinished tasks from previous checkpoint - checkpoint, _ = _prepare_next_tasks( + checkpoint, _ = prepare_next_tasks( checkpoint, processes, channels, @@ -1404,18 +1199,18 @@ class Pregel( get_next_version=( self.checkpointer.get_next_version if self.checkpointer - else _increment + else increment ), ) # apply input writes - _apply_writes( + apply_writes( checkpoint, channels, input_writes, ( self.checkpointer.get_next_version if self.checkpointer - else _increment + else increment ), ) # save input checkpoint @@ -1440,7 +1235,7 @@ class Pregel( # channel updates being applied only at the transition between steps stop = start + config["recursion_limit"] + 1 for step in range(start, stop): - next_checkpoint, next_tasks = _prepare_next_tasks( + next_checkpoint, next_tasks = prepare_next_tasks( checkpoint, processes, channels, @@ -1452,7 +1247,7 @@ class Pregel( get_next_version=( self.checkpointer.get_next_version if self.checkpointer - else _increment + else increment ), ) @@ -1473,7 +1268,7 @@ class Pregel( break # before execution, check if we should interrupt - if _should_interrupt( + if should_interrupt( checkpoint, interrupt_before, self.stream_channels_list, @@ -1571,14 +1366,14 @@ class Pregel( ) # apply writes to channels - _apply_writes( + apply_writes( checkpoint, channels, pending_writes, ( self.checkpointer.get_next_version if self.checkpointer - else _increment + else increment ), ) @@ -1610,7 +1405,7 @@ class Pregel( yield chunk # after execution, check if we should interrupt - if _should_interrupt( + if should_interrupt( checkpoint, interrupt_after, self.stream_channels_list, @@ -1637,7 +1432,6 @@ class Pregel( *, stream_mode: StreamMode = "values", output_keys: Optional[Union[str, Sequence[str]]] = None, - input_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, @@ -1650,7 +1444,6 @@ class Pregel( config: Optional. The configuration for the graph run. stream_mode: Optional[str]. The stream mode for the graph run. Default is "values". output_keys: Optional. The output keys to retrieve from the graph run. - input_keys: Optional. The input keys to provide for the graph run. interrupt_before: Optional. The nodes to interrupt the graph run before. interrupt_after: Optional. The nodes to interrupt the graph run after. debug: Optional. Enable debug mode for the graph run. @@ -1670,7 +1463,6 @@ class Pregel( config, stream_mode=stream_mode, output_keys=output_keys, - input_keys=input_keys, interrupt_before=interrupt_before, interrupt_after=interrupt_after, debug=debug, @@ -1692,7 +1484,6 @@ class Pregel( *, stream_mode: StreamMode = "values", output_keys: Optional[Union[str, Sequence[str]]] = None, - input_keys: Optional[Union[str, Sequence[str]]] = None, interrupt_before: Optional[Union[All, Sequence[str]]] = None, interrupt_after: Optional[Union[All, Sequence[str]]] = None, debug: Optional[bool] = None, @@ -1705,7 +1496,6 @@ class Pregel( config: Optional. The configuration for the computation. stream_mode: Optional. The stream mode for the computation. Default is "values". output_keys: Optional. The output keys to include in the result. Default is None. - input_keys: Optional. The input keys to include in the result. Default is None. interrupt_before: Optional. The nodes to interrupt before. Default is None. interrupt_after: Optional. The nodes to interrupt after. Default is None. debug: Optional. Whether to enable debug mode. Default is None. @@ -1726,7 +1516,6 @@ class Pregel( config, stream_mode=stream_mode, output_keys=output_keys, - input_keys=input_keys, interrupt_before=interrupt_before, interrupt_after=interrupt_after, debug=debug, @@ -1766,378 +1555,6 @@ def _panic_or_proceed( raise timeout_exc_cls(f"Timed out at step {step}") -def _should_interrupt( - checkpoint: Checkpoint, - interrupt_nodes: Union[All, Sequence[str]], - snapshot_channels: Sequence[str], - tasks: list[PregelExecutableTask], -) -> bool: - version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) - null_version = version_type() - # defaultdicts are mutated on access :( so we need to copy - seen = checkpoint["versions_seen"].copy()[INTERRUPT] - return ( - # interrupt if any channel has been updated since last interrupt - any( - version > seen.get(chan, null_version) - for chan, version in checkpoint["channel_versions"].items() - ) - # and any triggered node is in interrupt_nodes list - and any( - task.name - for task in tasks - if ( - (not task.config or TAG_HIDDEN not in task.config.get("tags")) - if interrupt_nodes == "*" - else task.name in interrupt_nodes - ) - ) - ) - - -def _local_read( - checkpoint: Checkpoint, - channels: Mapping[str, BaseChannel], - writes: Sequence[tuple[str, Any]], - config: RunnableConfig, - select: Union[list[str], str], - fresh: bool = False, -) -> Union[dict[str, Any], Any]: - if fresh: - checkpoint = create_checkpoint(checkpoint, channels, -1) - context_channels = {k: v for k, v in channels.items() if isinstance(v, Context)} - with ChannelsManager( - {k: v for k, v in channels.items() if k not in context_channels}, - checkpoint, - config, - ) as channels: - all_channels = {**channels, **context_channels} - _apply_writes(copy_checkpoint(checkpoint), all_channels, writes, None) - return read_channels(all_channels, select) - else: - return read_channels(channels, select) - - -def _local_write( - commit: Callable[[Sequence[tuple[str, Any]]], None], - processes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - writes: Sequence[tuple[str, Any]], -) -> None: - for chan, value in writes: - if chan == TASKS: - if not isinstance(value, Send): - raise InvalidUpdateError( - f"Invalid packet type, expected Packet, got {value}" - ) - if value.node not in processes: - raise InvalidUpdateError(f"Invalid node name {value.node} in packet") - elif chan not in channels: - logger.warning(f"Skipping write for channel '{chan}' which has no readers") - commit(writes) - - -def _increment(current: Optional[int], channel: BaseChannel) -> int: - return current + 1 if current is not None else 1 - - -def _apply_writes( - checkpoint: Checkpoint, - channels: Mapping[str, BaseChannel], - pending_writes: Sequence[tuple[str, Any]], - get_next_version: Optional[Callable[[int, BaseChannel], int]], -) -> None: - if checkpoint["pending_sends"]: - checkpoint["pending_sends"].clear() - - pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list) - # Group writes by channel - for chan, val in pending_writes: - if chan == TASKS: - checkpoint["pending_sends"].append(val) - else: - pending_writes_by_channel[chan].append(val) - - # Find the highest version of all channels - if checkpoint["channel_versions"]: - max_version = max(checkpoint["channel_versions"].values()) - else: - max_version = None - - updated_channels: set[str] = set() - # Apply writes to channels - for chan, vals in pending_writes_by_channel.items(): - if chan in channels: - try: - updated = channels[chan].update(vals) - except InvalidUpdateError as e: - raise InvalidUpdateError( - f"Invalid update for channel {chan} with values {vals}" - ) from e - if updated and get_next_version is not None: - checkpoint["channel_versions"][chan] = get_next_version( - max_version, channels[chan] - ) - updated_channels.add(chan) - # Channels that weren't updated in this step are notified of a new step - for chan in channels: - if chan not in updated_channels: - if channels[chan].update([]) and get_next_version is not None: - checkpoint["channel_versions"][chan] = get_next_version( - max_version, channels[chan] - ) - - -@overload -def _prepare_next_tasks( - checkpoint: Checkpoint, - processes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - managed: ManagedValueMapping, - config: RunnableConfig, - step: int, - for_execution: Literal[False], - get_next_version: Literal[None] = None, - manager: Literal[None] = None, -) -> tuple[Checkpoint, list[PregelTaskDescription]]: - ... - - -@overload -def _prepare_next_tasks( - checkpoint: Checkpoint, - processes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - managed: ManagedValueMapping, - config: RunnableConfig, - step: int, - for_execution: Literal[True], - get_next_version: Callable[[int, BaseChannel], int], - manager: Union[None, ParentRunManager, AsyncParentRunManager], -) -> tuple[Checkpoint, list[PregelExecutableTask]]: - ... - - -def _prepare_next_tasks( - checkpoint: Checkpoint, - processes: Mapping[str, PregelNode], - channels: Mapping[str, BaseChannel], - managed: ManagedValueMapping, - config: RunnableConfig, - step: int, - *, - for_execution: bool, - get_next_version: Union[None, Callable[[int, BaseChannel], int]] = None, - manager: Union[None, ParentRunManager, AsyncParentRunManager] = None, -) -> tuple[Checkpoint, Union[list[PregelTaskDescription], list[PregelExecutableTask]]]: - checkpoint = copy_checkpoint(checkpoint) - tasks: Union[list[PregelTaskDescription], list[PregelExecutableTask]] = [] - # Consume pending packets - for packet in checkpoint["pending_sends"]: - if not isinstance(packet, Send): - logger.warn(f"Ignoring invalid packet type {type(packet)} in pending sends") - continue - if for_execution: - proc = processes[packet.node] - if node := proc.get_node(): - triggers = [TASKS] - metadata = { - "langgraph_step": step, - "langgraph_node": packet.node, - "langgraph_triggers": triggers, - "langgraph_task_idx": len(tasks), - } - task_id = str(uuid5(UUID(checkpoint["id"]), json.dumps(metadata))) - writes = deque() - tasks.append( - PregelExecutableTask( - packet.node, - packet.arg, - node, - writes, - patch_config( - merge_configs( - config, - processes[packet.node].config, - {"metadata": metadata}, - ), - run_name=packet.node, - callbacks=( - manager.get_child(f"graph:step:{step}") - if manager - else None - ), - configurable={ - # deque.extend is thread-safe - CONFIG_KEY_SEND: partial( - _local_write, writes.extend, processes, channels - ), - CONFIG_KEY_READ: partial( - _local_read, checkpoint, channels, writes, config - ), - }, - ), - triggers, - proc.retry_policy, - task_id, - ) - ) - else: - tasks.append(PregelTaskDescription(packet.node, packet.arg)) - if for_execution: - checkpoint["pending_sends"].clear() - # Collect channels to consume - channels_to_consume = set() - # Check if any processes should be run in next step - # If so, prepare the values to be passed to them - version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) - null_version = version_type() - if null_version is None: - return checkpoint, tasks - for name, proc in processes.items(): - seen = checkpoint["versions_seen"][name] - # If any of the channels read by this process were updated - if triggers := sorted( - chan - for chan in proc.triggers - if not isinstance( - read_channel(channels, chan, return_exception=True), EmptyChannelError - ) - and checkpoint["channel_versions"].get(chan, null_version) - > seen.get(chan, null_version) - ): - channels_to_consume.update(triggers) - try: - val = next(_proc_input(step, name, proc, managed, channels)) - except StopIteration: - continue - - # update seen versions - if for_execution: - seen.update( - { - chan: checkpoint["channel_versions"][chan] - for chan in proc.triggers - if chan in checkpoint["channel_versions"] - } - ) - - if for_execution: - if node := proc.get_node(): - metadata = { - "langgraph_step": step, - "langgraph_node": name, - "langgraph_triggers": triggers, - "langgraph_task_idx": len(tasks), - } - task_id = str(uuid5(UUID(checkpoint["id"]), json.dumps(metadata))) - writes = deque() - tasks.append( - PregelExecutableTask( - name, - val, - node, - writes, - patch_config( - merge_configs( - config, - proc.config, - {"metadata": metadata}, - ), - run_name=name, - callbacks=( - manager.get_child(f"graph:step:{step}") - if manager - else None - ), - configurable={ - # deque.extend is thread-safe - CONFIG_KEY_SEND: partial( - _local_write, writes.extend, processes, channels - ), - CONFIG_KEY_READ: partial( - _local_read, - checkpoint, - channels, - writes, - config, - ), - }, - ), - triggers, - proc.retry_policy, - task_id, - ) - ) - else: - tasks.append(PregelTaskDescription(name, val)) - # Find the highest version of all channels - if checkpoint["channel_versions"]: - max_version = max(checkpoint["channel_versions"].values()) - else: - max_version = None - # Consume all channels that were read - if for_execution: - for chan in channels_to_consume: - if channels[chan].consume(): - checkpoint["channel_versions"][chan] = get_next_version( - max_version, channels[chan] - ) - return checkpoint, tasks - - -def _proc_input( - step: int, - name: str, - proc: PregelNode, - managed: ManagedValueMapping, - channels: Mapping[str, BaseChannel], -) -> Iterator[Any]: - # If all trigger channels subscribed by this process are not empty - # then invoke the process with the values of all non-empty channels - if isinstance(proc.channels, dict): - try: - val: dict = { - k: read_channel( - channels, - chan, - catch=chan not in proc.triggers, - ) - for k, chan in proc.channels.items() - if isinstance(chan, str) - } - - managed_values = {} - for key, chan in proc.channels.items(): - if is_managed_value(chan): - managed_values[key] = managed[key]( - step, PregelTaskDescription(name, val) - ) - - val.update(managed_values) - except EmptyChannelError: - return - elif isinstance(proc.channels, list): - for chan in proc.channels: - try: - val = read_channel(channels, chan, catch=False) - break - except EmptyChannelError: - pass - else: - return - else: - raise RuntimeError( - "Invalid channels type, expected list or dict, got {proc.channels}" - ) - - # If the process has a mapper, apply it to the value - if proc.mapper is not None: - val = proc.mapper(val) - - yield val - - def _with_mode(mode: StreamMode, on: bool, iter: Iterator[Any]) -> Iterator[Any]: if on: for chunk in iter: diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py new file mode 100644 index 000000000..256e62fd8 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -0,0 +1,413 @@ +import json +from collections import defaultdict, deque +from functools import partial +from typing import ( + Any, + Callable, + Iterator, + Literal, + Mapping, + Optional, + Sequence, + Union, + overload, +) +from uuid import UUID, uuid5 + +from langchain_core.callbacks.manager import AsyncParentRunManager, ParentRunManager +from langchain_core.runnables.config import ( + RunnableConfig, + merge_configs, + patch_config, +) + +from langgraph.channels.base import BaseChannel +from langgraph.channels.context import Context +from langgraph.channels.manager import ChannelsManager, create_checkpoint +from langgraph.checkpoint.base import Checkpoint, copy_checkpoint +from langgraph.constants import ( + CONFIG_KEY_READ, + CONFIG_KEY_SEND, + INTERRUPT, + TAG_HIDDEN, + TASKS, + Send, +) +from langgraph.errors import EmptyChannelError, InvalidUpdateError +from langgraph.managed.base import ManagedValueMapping, is_managed_value +from langgraph.pregel.io import read_channel, read_channels +from langgraph.pregel.log import logger +from langgraph.pregel.read import PregelNode +from langgraph.pregel.types import All, PregelExecutableTask, PregelTaskDescription + + +def should_interrupt( + checkpoint: Checkpoint, + interrupt_nodes: Union[All, Sequence[str]], + snapshot_channels: Sequence[str], + tasks: list[PregelExecutableTask], +) -> bool: + version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) + null_version = version_type() + # defaultdicts are mutated on access :( so we need to copy + seen = checkpoint["versions_seen"].copy()[INTERRUPT] + return ( + # interrupt if any channel has been updated since last interrupt + any( + version > seen.get(chan, null_version) + for chan, version in checkpoint["channel_versions"].items() + ) + # and any triggered node is in interrupt_nodes list + and any( + task.name + for task in tasks + if ( + (not task.config or TAG_HIDDEN not in task.config.get("tags")) + if interrupt_nodes == "*" + else task.name in interrupt_nodes + ) + ) + ) + + +def local_read( + checkpoint: Checkpoint, + channels: Mapping[str, BaseChannel], + writes: Sequence[tuple[str, Any]], + config: RunnableConfig, + select: Union[list[str], str], + fresh: bool = False, +) -> Union[dict[str, Any], Any]: + if fresh: + checkpoint = create_checkpoint(checkpoint, channels, -1) + context_channels = {k: v for k, v in channels.items() if isinstance(v, Context)} + with ChannelsManager( + {k: v for k, v in channels.items() if k not in context_channels}, + checkpoint, + config, + ) as channels: + all_channels = {**channels, **context_channels} + apply_writes(copy_checkpoint(checkpoint), all_channels, writes, None) + return read_channels(all_channels, select) + else: + return read_channels(channels, select) + + +def local_write( + commit: Callable[[Sequence[tuple[str, Any]]], None], + processes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + writes: Sequence[tuple[str, Any]], +) -> None: + for chan, value in writes: + if chan == TASKS: + if not isinstance(value, Send): + raise InvalidUpdateError( + f"Invalid packet type, expected Packet, got {value}" + ) + if value.node not in processes: + raise InvalidUpdateError(f"Invalid node name {value.node} in packet") + elif chan not in channels: + logger.warning(f"Skipping write for channel '{chan}' which has no readers") + commit(writes) + + +def increment(current: Optional[int], channel: BaseChannel) -> int: + return current + 1 if current is not None else 1 + + +def apply_writes( + checkpoint: Checkpoint, + channels: Mapping[str, BaseChannel], + pending_writes: Sequence[tuple[str, Any]], + get_next_version: Optional[Callable[[int, BaseChannel], int]], +) -> None: + if checkpoint["pending_sends"]: + checkpoint["pending_sends"].clear() + + pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list) + # Group writes by channel + for chan, val in pending_writes: + if chan == TASKS: + checkpoint["pending_sends"].append(val) + else: + pending_writes_by_channel[chan].append(val) + + # Find the highest version of all channels + if checkpoint["channel_versions"]: + max_version = max(checkpoint["channel_versions"].values()) + else: + max_version = None + + updated_channels: set[str] = set() + # Apply writes to channels + for chan, vals in pending_writes_by_channel.items(): + if chan in channels: + try: + updated = channels[chan].update(vals) + except InvalidUpdateError as e: + raise InvalidUpdateError( + f"Invalid update for channel {chan} with values {vals}" + ) from e + if updated and get_next_version is not None: + checkpoint["channel_versions"][chan] = get_next_version( + max_version, channels[chan] + ) + updated_channels.add(chan) + # Channels that weren't updated in this step are notified of a new step + for chan in channels: + if chan not in updated_channels: + if channels[chan].update([]) and get_next_version is not None: + checkpoint["channel_versions"][chan] = get_next_version( + max_version, channels[chan] + ) + + +@overload +def prepare_next_tasks( + checkpoint: Checkpoint, + processes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + managed: ManagedValueMapping, + config: RunnableConfig, + step: int, + for_execution: Literal[False], + get_next_version: Literal[None] = None, + manager: Literal[None] = None, +) -> tuple[Checkpoint, list[PregelTaskDescription]]: + ... + + +@overload +def prepare_next_tasks( + checkpoint: Checkpoint, + processes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + managed: ManagedValueMapping, + config: RunnableConfig, + step: int, + for_execution: Literal[True], + get_next_version: Callable[[int, BaseChannel], int], + manager: Union[None, ParentRunManager, AsyncParentRunManager], +) -> tuple[Checkpoint, list[PregelExecutableTask]]: + ... + + +def prepare_next_tasks( + checkpoint: Checkpoint, + processes: Mapping[str, PregelNode], + channels: Mapping[str, BaseChannel], + managed: ManagedValueMapping, + config: RunnableConfig, + step: int, + *, + for_execution: bool, + get_next_version: Union[None, Callable[[int, BaseChannel], int]] = None, + manager: Union[None, ParentRunManager, AsyncParentRunManager] = None, +) -> tuple[Checkpoint, Union[list[PregelTaskDescription], list[PregelExecutableTask]]]: + checkpoint = copy_checkpoint(checkpoint) + tasks: Union[list[PregelTaskDescription], list[PregelExecutableTask]] = [] + # Consume pending packets + for packet in checkpoint["pending_sends"]: + if not isinstance(packet, Send): + logger.warn(f"Ignoring invalid packet type {type(packet)} in pending sends") + continue + if for_execution: + proc = processes[packet.node] + if node := proc.get_node(): + triggers = [TASKS] + metadata = { + "langgraph_step": step, + "langgraph_node": packet.node, + "langgraph_triggers": triggers, + "langgraph_task_idx": len(tasks), + } + task_id = str(uuid5(UUID(checkpoint["id"]), json.dumps(metadata))) + writes = deque() + tasks.append( + PregelExecutableTask( + packet.node, + packet.arg, + node, + writes, + patch_config( + merge_configs( + config, + processes[packet.node].config, + {"metadata": metadata}, + ), + run_name=packet.node, + callbacks=( + manager.get_child(f"graph:step:{step}") + if manager + else None + ), + configurable={ + # deque.extend is thread-safe + CONFIG_KEY_SEND: partial( + local_write, writes.extend, processes, channels + ), + CONFIG_KEY_READ: partial( + local_read, checkpoint, channels, writes, config + ), + }, + ), + triggers, + proc.retry_policy, + task_id, + ) + ) + else: + tasks.append(PregelTaskDescription(packet.node, packet.arg)) + if for_execution: + checkpoint["pending_sends"].clear() + # Collect channels to consume + channels_to_consume = set() + # Check if any processes should be run in next step + # If so, prepare the values to be passed to them + version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) + null_version = version_type() + if null_version is None: + return checkpoint, tasks + for name, proc in processes.items(): + seen = checkpoint["versions_seen"][name] + # If any of the channels read by this process were updated + if triggers := sorted( + chan + for chan in proc.triggers + if not isinstance( + read_channel(channels, chan, return_exception=True), EmptyChannelError + ) + and checkpoint["channel_versions"].get(chan, null_version) + > seen.get(chan, null_version) + ): + channels_to_consume.update(triggers) + try: + val = next(_proc_input(step, name, proc, managed, channels)) + except StopIteration: + continue + + # update seen versions + if for_execution: + seen.update( + { + chan: checkpoint["channel_versions"][chan] + for chan in proc.triggers + if chan in checkpoint["channel_versions"] + } + ) + + if for_execution: + if node := proc.get_node(): + metadata = { + "langgraph_step": step, + "langgraph_node": name, + "langgraph_triggers": triggers, + "langgraph_task_idx": len(tasks), + } + task_id = str(uuid5(UUID(checkpoint["id"]), json.dumps(metadata))) + writes = deque() + tasks.append( + PregelExecutableTask( + name, + val, + node, + writes, + patch_config( + merge_configs( + config, + proc.config, + {"metadata": metadata}, + ), + run_name=name, + callbacks=( + manager.get_child(f"graph:step:{step}") + if manager + else None + ), + configurable={ + # deque.extend is thread-safe + CONFIG_KEY_SEND: partial( + local_write, writes.extend, processes, channels + ), + CONFIG_KEY_READ: partial( + local_read, + checkpoint, + channels, + writes, + config, + ), + }, + ), + triggers, + proc.retry_policy, + task_id, + ) + ) + else: + tasks.append(PregelTaskDescription(name, val)) + # Find the highest version of all channels + if checkpoint["channel_versions"]: + max_version = max(checkpoint["channel_versions"].values()) + else: + max_version = None + # Consume all channels that were read + if for_execution: + for chan in channels_to_consume: + if channels[chan].consume(): + checkpoint["channel_versions"][chan] = get_next_version( + max_version, channels[chan] + ) + return checkpoint, tasks + + +def _proc_input( + step: int, + name: str, + proc: PregelNode, + managed: ManagedValueMapping, + channels: Mapping[str, BaseChannel], +) -> Iterator[Any]: + # If all trigger channels subscribed by this process are not empty + # then invoke the process with the values of all non-empty channels + if isinstance(proc.channels, dict): + try: + val: dict = { + k: read_channel( + channels, + chan, + catch=chan not in proc.triggers, + ) + for k, chan in proc.channels.items() + if isinstance(chan, str) + } + + managed_values = {} + for key, chan in proc.channels.items(): + if is_managed_value(chan): + managed_values[key] = managed[key]( + step, PregelTaskDescription(name, val) + ) + + val.update(managed_values) + except EmptyChannelError: + return + elif isinstance(proc.channels, list): + for chan in proc.channels: + try: + val = read_channel(channels, chan, catch=False) + break + except EmptyChannelError: + pass + else: + return + else: + raise RuntimeError( + "Invalid channels type, expected list or dict, got {proc.channels}" + ) + + # If the process has a mapper, apply it to the value + if proc.mapper is not None: + val = proc.mapper(val) + + yield val diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 389c59e81..5a317bb26 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -110,7 +110,7 @@ def map_debug_task_results( def map_debug_checkpoint( - step: int, + step: int, # TODO remove arg, it's in metadata config: RunnableConfig, channels: Mapping[str, BaseChannel], stream_channels: Union[str, Sequence[str]], diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py new file mode 100644 index 000000000..b601b6446 --- /dev/null +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -0,0 +1,282 @@ +from collections import deque +from contextlib import ExitStack +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + ContextManager, + List, + Literal, + Mapping, + Optional, + Self, + Sequence, + Type, +) + +from langchain_core.runnables import RunnableConfig + +from langgraph.channels.base import BaseChannel +from langgraph.channels.manager import ChannelsManager, create_checkpoint +from langgraph.checkpoint.base import ( + BaseCheckpointSaver, + Checkpoint, + CheckpointMetadata, + CheckpointTuple, + PendingWrite, + copy_checkpoint, + empty_checkpoint, +) +from langgraph.constants import INTERRUPT +from langgraph.managed.base import ManagedValueMapping, ManagedValuesManager +from langgraph.pregel.algo import ( + apply_writes, + increment, + prepare_next_tasks, + should_interrupt, +) +from langgraph.pregel.executor import BackgroundExecutor, Submit +from langgraph.pregel.io import map_input +from langgraph.pregel.types import PregelExecutableTask + +if TYPE_CHECKING: + from langgraph.pregel import Pregel + + +INPUT_DONE = object() + + +class PregelLoop(ContextManager): + config: RunnableConfig + checkpoint: Checkpoint + checkpoint_metadata: CheckpointMetadata + checkpoint_pending_writes: Optional[List[PendingWrite]] + + submit: Submit + channels: Mapping[str, BaseChannel] + managed: ManagedValueMapping + + status: Literal[ + "pending", "done", "interrupt_before", "interrupt_after", "out_of_steps" + ] + tasks: Sequence[PregelExecutableTask] + + def __init__( + self, + input: Optional[Any], + *, + config: RunnableConfig, + checkpointer: Optional[BaseCheckpointSaver], + graph: "Pregel", + ) -> None: + self.stack = ExitStack() + self.input = input + self.config = config + self.checkpointer = checkpointer + self.get_next_version = ( + checkpointer.get_next_version if checkpointer else increment + ) + self.graph = graph + # TODO if managed values no longer needs graph we can replace with + # managed_specs, channel_specs + + def __enter__(self) -> Self: + saved = ( + self.checkpointer.get_tuple(self.config) if self.checkpointer else None + ) or CheckpointTuple(self.config, empty_checkpoint(), {"step": -2}, None, []) + self.config = { + **self.config, + **saved.config, + "configurable": { + **self.config.get("configurable", {}), + **saved.config.get("configurable", {}), + }, + } + self.checkpoint = saved.checkpoint + self.checkpoint_metadata = saved.metadata + self.checkpoint_pending_writes = saved.pending_writes + + self.submit = self.stack.enter_context(BackgroundExecutor(self.config)) + self.channels = self.stack.enter_context( + ChannelsManager(self.graph.channels, self.checkpoint, self.config) + ) + self.managed = self.stack.enter_context( + ManagedValuesManager( + self.graph.managed_values_dict, self.config, self.graph + ) + ) + self.status = "pending" + + return self + + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Optional[bool]: + del self.graph + return self.stack.__exit__(exc_type, exc_value, traceback) + + def tick( + self, + *, + interrupt_after: Optional[Sequence[str]] = None, + interrupt_before: Optional[Sequence[str]] = None, + ) -> bool: + print("ticking", self.status, self.checkpoint_metadata["step"]) + if self.status != "pending": + raise RuntimeError(f"Cannot tick when status is {self.status}") + if self.input is not INPUT_DONE: + self.first() + elif len({tid for tid, _, _ in self.checkpoint_pending_writes}) == len( + self.tasks + ): + # all tasks have finished + apply_writes( + self.checkpoint, + self.channels, + ((k, v) for _, k, v in self.checkpoint_pending_writes), + self.get_next_version, + ) + # clear pending writes + self.checkpoint_pending_writes.clear() + # save checkpoint + self.put_checkpoint({"source": "loop", "writes": None}) # TODO + # after execution, check if we should interrupt + if should_interrupt( + self.checkpoint, + interrupt_after, + self.graph.stream_channels_list, + self.tasks, + ): + self.status = "interrupt_after" + return False + else: + return False + + # check if iteration limit is reached + if self.checkpoint_metadata["step"] >= self.config["recursion_limit"]: + self.status = "out_of_steps" + return False + + # prepare next tasks + self.checkpoint, self.tasks = prepare_next_tasks( + self.checkpoint, + self.graph.nodes, + self.channels, + self.managed, + self.config, + self.checkpoint_metadata["step"], + for_execution=True, + get_next_version=self.get_next_version, + ) + + # if no more tasks, we're done + if not self.tasks: + self.status = "done" + return False + + # TODO how to make this work for both + # - online case: we should schedule remaining tasks + # - offline case: we should just bail, as other tasks were scheduled before + # assign pending writes to tasks + # if self.checkpoint_pending_writes: + # # if there are pending writes from a previous loop, apply them + # for tid, k, v in self.checkpoint_pending_writes: + # if task := next((t for t in self.tasks if t.id == tid), None): + # task.writes.append((k, v)) + # if + + # before execution, check if we should interrupt + if should_interrupt( + self.checkpoint, + interrupt_before, + self.graph.stream_channels_list, + self.tasks, + ): + self.status = "interrupt_before" + return False + + return True + + def first(self) -> None: + # map inputs to channel updates + if input_writes := deque(map_input(self.graph.input_channels, self.input)): + # discard any unfinished tasks from previous checkpoint + self.checkpoint, _ = prepare_next_tasks( + self.checkpoint, + self.graph.nodes, + self.channels, + self.managed, + self.config, + -1, + for_execution=True, + get_next_version=self.get_next_version, + ) + # apply input writes + apply_writes( + self.checkpoint, + self.channels, + input_writes, + self.get_next_version, + ) + # save input checkpoint + self.put_checkpoint({"source": "input", "writes": self.input}) + else: + # no input is taken as signal to proceed past previous interrupt + self.checkpoint = copy_checkpoint(self.checkpoint) + for k in self.channels: + if k in self.checkpoint["channel_versions"]: + version = self.checkpoint["channel_versions"][k] + self.checkpoint["versions_seen"][INTERRUPT][k] = version + # done with input + self.input = INPUT_DONE + + def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: + self.checkpoint_pending_writes.extend((task_id, k, v) for k, v in writes) + if self.checkpointer is not None: + self.submit( + self.checkpointer.put_writes, + { + **self.config, + "configurable": { + **self.config["configurable"], + "thread_ts": self.checkpoint["id"], + }, + }, + writes, + task_id, + ) + + def put_checkpoint( + self, + metadata: CheckpointMetadata, + ) -> None: + # increment step + self.checkpoint_metadata = { + **metadata, + "step": self.checkpoint_metadata["step"] + 1, + } + # bail if no checkpointer + if self.checkpointer is None: + return + # create new checkpoint + self.checkpoint = create_checkpoint( + self.checkpoint, self.channels, self.checkpoint_metadata["step"] + ) + # save it, without blocking + self.submit( + self.checkpointer.put, + self.config, + copy_checkpoint(self.checkpoint), + self.checkpoint_metadata, + ) + # update checkpoint config + self.config = { + **self.config, + "configurable": { + **self.config["configurable"], + "thread_ts": self.checkpoint["id"], + }, + } diff --git a/libs/langgraph/langgraph/pregel/types.py b/libs/langgraph/langgraph/pregel/types.py index 9a1afb596..19d3c3301 100644 --- a/libs/langgraph/langgraph/pregel/types.py +++ b/libs/langgraph/langgraph/pregel/types.py @@ -88,3 +88,12 @@ class StateSnapshot(NamedTuple): All = Literal["*"] + +StreamMode = Literal["values", "updates", "debug"] +"""How the stream method should emit outputs. + +- 'values': Emit all values of the state for each step. +- 'updates': Emit only the node name(s) and updates + that were returned by the node(s) **after** each step. +- 'debug': Emit debug events for each step. +""" diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index 264f30db6..6053a4e5b 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -60,7 +60,7 @@ def test_no_modifier(): model = FakeToolCallingModel() agent = create_react_agent(model, []) inputs = [HumanMessage("hi?")] - response = agent.invoke({"messages": inputs}) + response = agent.invoke({"messages": inputs}, debug=True) expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]} assert response == expected_response diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index df6ba34ca..805794bdf 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -520,8 +520,6 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert app.invoke(2) == 4 - assert app.invoke(2, input_keys="inbox") == 3 - with pytest.raises(GraphRecursionError): app.invoke(2, {"recursion_limit": 1}) diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 4c166cdc5..1cf5c35d4 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -623,8 +623,6 @@ async def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert await app.ainvoke(2) == 4 - assert await app.ainvoke(2, input_keys="inbox") == 3 - with pytest.raises(GraphRecursionError): await app.ainvoke(2, {"recursion_limit": 1}) From 60f7a7d593d88bc55d0694dda654745a180a81d0 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 13:23:17 -0700 Subject: [PATCH 03/28] Lint --- libs/langgraph/langgraph/pregel/loop.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index b601b6446..f217e201a 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -9,12 +9,12 @@ from typing import ( Literal, Mapping, Optional, - Self, Sequence, Type, ) from langchain_core.runnables import RunnableConfig +from typing_extensions import Self from langgraph.channels.base import BaseChannel from langgraph.channels.manager import ChannelsManager, create_checkpoint From b3ee7288396d58ea7d18c74ef793c2c7b8b2d136 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 15:48:28 -0700 Subject: [PATCH 04/28] Pregel.stream passing all tests --- libs/langgraph/langgraph/pregel/__init__.py | 88 +++++------- libs/langgraph/langgraph/pregel/algo.py | 1 - libs/langgraph/langgraph/pregel/debug.py | 4 + libs/langgraph/langgraph/pregel/loop.py | 149 ++++++++++++-------- libs/langgraph/tests/test_prebuilt.py | 118 +++++++++++++++- libs/langgraph/tests/test_pregel.py | 19 ++- libs/langgraph/tests/test_pregel_async.py | 12 ++ 7 files changed, 272 insertions(+), 119 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 5b1d5b9f1..b61d05982 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -849,38 +849,34 @@ class Pregel( # channels are guaranteed to be immutable for the duration of the step, # with channel updates applied only at the transition between steps while loop.tick( - interrupt_before=interrupt_before, interrupt_after=interrupt_after + output_keys=output_keys, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, ): - # print debug output + # debug flag if self.debug: print_step_checkpoint( loop.checkpoint_metadata, loop.channels, self.stream_channels_list, ) - # emit debug output - if self.checkpointer and "debug" in stream_modes: - yield from _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_checkpoint( - loop.checkpoint_metadata["step"], - loop.config, - loop.channels, - self.stream_channels_asis, - loop.checkpoint_metadata, - ), - ) - + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload + # debug flag if debug: - print_step_tasks(loop.checkpoint_metadata["step"], loop.tasks) + print_step_tasks(loop.step, loop.tasks) + # TODO move to tick() ? if "debug" in stream_modes: yield from _with_mode( "debug", isinstance(stream_mode, list), - map_debug_tasks( - loop.checkpoint_metadata["step"], loop.tasks - ), + map_debug_tasks(loop.step, loop.tasks), ) # execute tasks, and wait for one to fail or all to finish. @@ -931,7 +927,7 @@ class Pregel( "debug", isinstance(stream_mode, list), map_debug_task_results( - loop.checkpoint_metadata["step"], + loop.step, [task], self.stream_channels_list, ), @@ -941,38 +937,31 @@ class Pregel( del fut, task # panic on failure or timeout - _panic_or_proceed(done, inflight, loop.checkpoint_metadata["step"]) + _panic_or_proceed(done, inflight, loop.step) # don't keep futures around in memory longer than needed del done, inflight, futures - - # combine pending writes from all tasks - pending_writes = deque[tuple[str, Any]]() - for task in loop.tasks: - pending_writes.extend(task.writes) - + # debug flag if debug: print_step_writes( - loop.checkpoint_metadata["step"], - pending_writes, + loop.step, + [w for t in loop.tasks for w in t.writes], self.stream_channels_list, ) - - # yield values output - if "values" in stream_modes: - yield from _with_mode( - "values", - isinstance(stream_mode, list), - map_output_values( - output_keys, pending_writes, loop.channels - ), - ) + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload + # handle exit if loop.status == "out_of_steps": raise GraphRecursionError( - f"Recursion limit of {config['recursion_limit']} reached" + f"Recursion limit of {config['recursion_limit']} reached " "without hitting a stop condition. You can increase the " "limit by setting the `recursion_limit` config key." ) - # set final channel values as run output run_manager.on_chain_end(read_channels(loop.channels, output_keys)) except BaseException as e: @@ -1142,6 +1131,7 @@ class Pregel( ) def put_checkpoint(metadata: CheckpointMetadata) -> Iterator[Any]: + print(metadata) nonlocal checkpoint, checkpoint_config, channels if self.checkpointer is None: @@ -1268,12 +1258,7 @@ class Pregel( break # before execution, check if we should interrupt - if should_interrupt( - checkpoint, - interrupt_before, - self.stream_channels_list, - next_tasks, - ): + if should_interrupt(checkpoint, interrupt_before, next_tasks): break else: checkpoint = next_checkpoint @@ -1405,12 +1390,7 @@ class Pregel( yield chunk # after execution, check if we should interrupt - if should_interrupt( - checkpoint, - interrupt_after, - self.stream_channels_list, - next_tasks, - ): + if should_interrupt(checkpoint, interrupt_after, next_tasks): break else: raise GraphRecursionError( diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 256e62fd8..46950b9f8 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -44,7 +44,6 @@ from langgraph.pregel.types import All, PregelExecutableTask, PregelTaskDescript def should_interrupt( checkpoint: Checkpoint, interrupt_nodes: Union[All, Sequence[str]], - snapshot_channels: Sequence[str], tasks: list[PregelExecutableTask], ) -> bool: version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 5a317bb26..33b7f440e 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -70,6 +70,8 @@ def map_debug_tasks( if config is not None and TAG_HIDDEN in config.get("tags", []): continue + print("map_debug_tasks", json.dumps((name, step, config["metadata"]))) + yield { "type": "task", "timestamp": ts, @@ -95,6 +97,8 @@ def map_debug_task_results( if config is not None and TAG_HIDDEN in config.get("tags", []): continue + print("map_debug_tasks_r", json.dumps((name, step, config["metadata"]))) + yield { "type": "task_result", "timestamp": ts, diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index f217e201a..d618ffa24 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -4,13 +4,17 @@ from types import TracebackType from typing import ( TYPE_CHECKING, Any, + Callable, ContextManager, List, Literal, Mapping, Optional, Sequence, + Tuple, Type, + TypeVar, + Union, ) from langchain_core.runnables import RunnableConfig @@ -35,31 +39,39 @@ from langgraph.pregel.algo import ( prepare_next_tasks, should_interrupt, ) +from langgraph.pregel.debug import map_debug_checkpoint from langgraph.pregel.executor import BackgroundExecutor, Submit -from langgraph.pregel.io import map_input +from langgraph.pregel.io import map_input, map_output_updates, map_output_values, single from langgraph.pregel.types import PregelExecutableTask if TYPE_CHECKING: from langgraph.pregel import Pregel +V = TypeVar("V") INPUT_DONE = object() class PregelLoop(ContextManager): + input: Optional[Any] config: RunnableConfig - checkpoint: Checkpoint - checkpoint_metadata: CheckpointMetadata - checkpoint_pending_writes: Optional[List[PendingWrite]] + checkpointer: Optional[BaseCheckpointSaver] + get_next_version: Callable[[Optional[V]], V] + graph: "Pregel" submit: Submit channels: Mapping[str, BaseChannel] managed: ManagedValueMapping + checkpoint: Checkpoint + checkpoint_config: RunnableConfig + checkpoint_metadata: CheckpointMetadata + checkpoint_pending_writes: Optional[List[PendingWrite]] status: Literal[ "pending", "done", "interrupt_before", "interrupt_after", "out_of_steps" ] tasks: Sequence[PregelExecutableTask] + stream: deque[Tuple[str, Any]] def __init__( self, @@ -69,6 +81,7 @@ class PregelLoop(ContextManager): checkpointer: Optional[BaseCheckpointSaver], graph: "Pregel", ) -> None: + self.stream = deque() self.stack = ExitStack() self.input = input self.config = config @@ -84,7 +97,7 @@ class PregelLoop(ContextManager): saved = ( self.checkpointer.get_tuple(self.config) if self.checkpointer else None ) or CheckpointTuple(self.config, empty_checkpoint(), {"step": -2}, None, []) - self.config = { + self.checkpoint_config = { **self.config, **saved.config, "configurable": { @@ -106,6 +119,7 @@ class PregelLoop(ContextManager): ) ) self.status = "pending" + self.step = self.checkpoint_metadata["step"] + 1 return self @@ -121,10 +135,11 @@ class PregelLoop(ContextManager): def tick( self, *, + output_keys: Union[str, Sequence[str]] = None, interrupt_after: Optional[Sequence[str]] = None, interrupt_before: Optional[Sequence[str]] = None, ) -> bool: - print("ticking", self.status, self.checkpoint_metadata["step"]) + print("tick", self.status, self.step) if self.status != "pending": raise RuntimeError(f"Cannot tick when status is {self.status}") if self.input is not INPUT_DONE: @@ -132,42 +147,57 @@ class PregelLoop(ContextManager): elif len({tid for tid, _, _ in self.checkpoint_pending_writes}) == len( self.tasks ): + # assign writes to tasks, apply them in order + grouped: dict[str, list[tuple[str, Any]]] = {} + for tid, k, v in self.checkpoint_pending_writes: + grouped.setdefault(tid, []).append((k, v)) + writes = [(k, v) for t in self.tasks for k, v in grouped.get(t.id, [])] # all tasks have finished apply_writes( self.checkpoint, self.channels, - ((k, v) for _, k, v in self.checkpoint_pending_writes), + writes, self.get_next_version, ) + # produce values output + self.stream.extend( + ("values", v) + for v in map_output_values(output_keys, writes, self.channels) + ) # clear pending writes self.checkpoint_pending_writes.clear() # save checkpoint - self.put_checkpoint({"source": "loop", "writes": None}) # TODO + self.put_checkpoint( + { + "source": "loop", + "writes": single( + map_output_updates(output_keys, self.tasks) + if self.graph.stream_mode == "updates" + else map_output_values(output_keys, writes, self.channels) + ), + } + ) # after execution, check if we should interrupt - if should_interrupt( - self.checkpoint, - interrupt_after, - self.graph.stream_channels_list, - self.tasks, - ): + if should_interrupt(self.checkpoint, interrupt_after, self.tasks): self.status = "interrupt_after" return False else: return False # check if iteration limit is reached - if self.checkpoint_metadata["step"] >= self.config["recursion_limit"]: + if self.step > self.config["recursion_limit"]: self.status = "out_of_steps" return False # prepare next tasks + prev_checkpoint = self.checkpoint self.checkpoint, self.tasks = prepare_next_tasks( self.checkpoint, self.graph.nodes, self.channels, self.managed, self.config, - self.checkpoint_metadata["step"], + self.step, for_execution=True, get_next_version=self.get_next_version, ) @@ -180,21 +210,14 @@ class PregelLoop(ContextManager): # TODO how to make this work for both # - online case: we should schedule remaining tasks # - offline case: we should just bail, as other tasks were scheduled before - # assign pending writes to tasks - # if self.checkpoint_pending_writes: - # # if there are pending writes from a previous loop, apply them - # for tid, k, v in self.checkpoint_pending_writes: - # if task := next((t for t in self.tasks if t.id == tid), None): - # task.writes.append((k, v)) - # if + # if there are pending writes from a previous loop, apply them + if self.checkpoint_pending_writes: + for tid, k, v in self.checkpoint_pending_writes: + if task := next((t for t in self.tasks if t.id == tid), None): + task.writes.append((k, v)) # before execution, check if we should interrupt - if should_interrupt( - self.checkpoint, - interrupt_before, - self.graph.stream_channels_list, - self.tasks, - ): + if should_interrupt(prev_checkpoint, interrupt_before, self.tasks): self.status = "interrupt_before" return False @@ -210,9 +233,10 @@ class PregelLoop(ContextManager): self.channels, self.managed, self.config, - -1, + self.step, for_execution=True, get_next_version=self.get_next_version, + # TODO missing run_manager ) # apply input writes apply_writes( @@ -239,9 +263,9 @@ class PregelLoop(ContextManager): self.submit( self.checkpointer.put_writes, { - **self.config, + **self.checkpoint_config, "configurable": { - **self.config["configurable"], + **self.checkpoint_config["configurable"], "thread_ts": self.checkpoint["id"], }, }, @@ -253,30 +277,39 @@ class PregelLoop(ContextManager): self, metadata: CheckpointMetadata, ) -> None: - # increment step - self.checkpoint_metadata = { - **metadata, - "step": self.checkpoint_metadata["step"] + 1, - } + # assign step + metadata["step"] = self.step # bail if no checkpointer - if self.checkpointer is None: - return - # create new checkpoint - self.checkpoint = create_checkpoint( - self.checkpoint, self.channels, self.checkpoint_metadata["step"] - ) - # save it, without blocking - self.submit( - self.checkpointer.put, - self.config, - copy_checkpoint(self.checkpoint), - self.checkpoint_metadata, - ) - # update checkpoint config - self.config = { - **self.config, - "configurable": { - **self.config["configurable"], - "thread_ts": self.checkpoint["id"], - }, - } + if self.checkpointer is not None: + # create new checkpoint + self.checkpoint_metadata = metadata + self.checkpoint = create_checkpoint( + self.checkpoint, self.channels, self.step + ) + # save it, without blocking + self.submit( + self.checkpointer.put, + self.checkpoint_config, + copy_checkpoint(self.checkpoint), + self.checkpoint_metadata, + ) + self.checkpoint_config = { + **self.checkpoint_config, + "configurable": { + **self.checkpoint_config["configurable"], + "thread_ts": self.checkpoint["id"], + }, + } + # produce debug output + self.stream.extend( + ("debug", v) + for v in map_debug_checkpoint( + self.step, + self.checkpoint_config, + self.channels, + self.graph.stream_channels_asis, + self.checkpoint_metadata, + ) + ) + # increment step + self.step += 1 diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index 6053a4e5b..7b56eba46 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -1,5 +1,8 @@ +from collections import defaultdict from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union +from langgraph.checkpoint.base import BaseCheckpointSaver +from langgraph.checkpoint.sqlite import SqliteSaver import pytest from langchain_core.callbacks import ( CallbackManagerForLLMRun, @@ -27,6 +30,8 @@ from langgraph.prebuilt import ( ValidationNode, create_react_agent, ) +from tests.any_str import AnyStr +from tests.memory_assert import MemorySaverAssertImmutable class FakeToolCallingModel(BaseChatModel): @@ -56,14 +61,121 @@ class FakeToolCallingModel(BaseChatModel): return self -def test_no_modifier(): +@pytest.mark.parametrize( + "checkpointer", + [ + MemorySaverAssertImmutable(), + None, + ], + ids=[ + "memory", + "none", + ], +) +def test_no_modifier(checkpointer: Optional[BaseCheckpointSaver]): model = FakeToolCallingModel() - agent = create_react_agent(model, []) + agent = create_react_agent(model, [], checkpointer=checkpointer) inputs = [HumanMessage("hi?")] - response = agent.invoke({"messages": inputs}, debug=True) + thread = {"configurable": {"thread_id": "123"}} + response = agent.invoke({"messages": inputs}, thread, debug=True) expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]} assert response == expected_response + if checkpointer: + saved = checkpointer.get_tuple(thread) + assert saved is not None + assert saved.checkpoint == { + "v": 1, + "ts": AnyStr(), + "id": AnyStr(), + "channel_values": { + "messages": [ + HumanMessage(content="hi?", id=AnyStr()), + AIMessage(content="hi?", id="0"), + ], + "agent": "agent", + }, + "channel_versions": { + "__start__": 2, + "messages": 3, + "start:agent": 3, + "agent": 3, + }, + "versions_seen": defaultdict( + dict, + { + "__start__": {"__start__": 1}, + "agent": {"start:agent": 2}, + "tools": {}, + }, + ), + "pending_sends": [], + } + assert saved.metadata == { + "source": "loop", + "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, + "step": 1, + } + assert saved.pending_writes == [] + + +@pytest.mark.parametrize( + "checkpointer", + [ + MemorySaverAssertImmutable(), + None, + ], + ids=[ + "memory", + "none", + ], +) +async def test_no_modifier_async(checkpointer: Optional[BaseCheckpointSaver]): + model = FakeToolCallingModel() + agent = create_react_agent(model, [], checkpointer=checkpointer) + inputs = [HumanMessage("hi?")] + thread = {"configurable": {"thread_id": "123"}} + response = await agent.ainvoke({"messages": inputs}, thread, debug=True) + expected_response = {"messages": inputs + [AIMessage(content="hi?", id="0")]} + assert response == expected_response + + if checkpointer: + saved = await checkpointer.aget_tuple(thread) + assert saved is not None + assert saved.checkpoint == { + "v": 1, + "ts": AnyStr(), + "id": AnyStr(), + "channel_values": { + "messages": [ + HumanMessage(content="hi?", id=AnyStr()), + AIMessage(content="hi?", id="0"), + ], + "agent": "agent", + }, + "channel_versions": { + "__start__": 2, + "messages": 3, + "start:agent": 3, + "agent": 3, + }, + "versions_seen": defaultdict( + dict, + { + "__start__": {"__start__": 1}, + "agent": {"start:agent": 2}, + "tools": {}, + }, + ), + "pending_sends": [], + } + assert saved.metadata == { + "source": "loop", + "writes": {"agent": {"messages": [AIMessage(content="hi?", id="0")]}}, + "step": 1, + } + assert saved.pending_writes == [] + def test_passing_two_modifiers(): model = FakeToolCallingModel() diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 805794bdf..9b32fc149 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -521,7 +521,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert app.invoke(2) == 4 with pytest.raises(GraphRecursionError): - app.invoke(2, {"recursion_limit": 1}) + app.invoke(2, {"recursion_limit": 1}, debug=1) graph = Graph() graph.add_node("add_one", add_one) @@ -533,7 +533,7 @@ def test_invoke_two_processes_in_out(mocker: MockerFixture) -> None: assert gapp.invoke(2) == 4 - for step, values in enumerate(gapp.stream(2), start=1): + for step, values in enumerate(gapp.stream(2, debug=1), start=1): if step == 1: assert values == { "add_one": 3, @@ -6166,6 +6166,18 @@ def test_start_branch_then(snapshot: SnapshotAssertion) -> None: "my_key": "value ⛰️", "market": "DE", } + assert [c.metadata for c in tool_two.checkpointer.list(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value ⛰️", "market": "DE"}, + }, + ] assert tool_two.get_state(thread1) == StateSnapshot( values={"my_key": "value ⛰️", "market": "DE"}, next=("tool_two_slow",), @@ -6875,7 +6887,8 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> }, ) - assert [c for c in app_w_interrupt.stream(None, config)] == [ + print("yo") + assert [c for c in app_w_interrupt.stream(None, config, debug=1)] == [ {"qa": {"answer": "doc1,doc2,doc3,doc4,doc5"}}, ] diff --git a/libs/langgraph/tests/test_pregel_async.py b/libs/langgraph/tests/test_pregel_async.py index 1cf5c35d4..0e927e518 100644 --- a/libs/langgraph/tests/test_pregel_async.py +++ b/libs/langgraph/tests/test_pregel_async.py @@ -4735,6 +4735,18 @@ async def test_start_branch_then() -> None: "my_key": "value", "market": "DE", } + assert [c.metadata async for c in tool_two.checkpointer.alist(thread1)] == [ + { + "source": "loop", + "step": 0, + "writes": None, + }, + { + "source": "input", + "step": -1, + "writes": {"my_key": "value", "market": "DE"}, + }, + ] assert await tool_two.aget_state(thread1) == StateSnapshot( values={"my_key": "value", "market": "DE"}, next=("tool_two_slow",), From 06f83710c8bce3437dde99f704202469a10f9f78 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 15:49:50 -0700 Subject: [PATCH 05/28] Lint --- libs/langgraph/langgraph/pregel/debug.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 33b7f440e..5a317bb26 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -70,8 +70,6 @@ def map_debug_tasks( if config is not None and TAG_HIDDEN in config.get("tags", []): continue - print("map_debug_tasks", json.dumps((name, step, config["metadata"]))) - yield { "type": "task", "timestamp": ts, @@ -97,8 +95,6 @@ def map_debug_task_results( if config is not None and TAG_HIDDEN in config.get("tags", []): continue - print("map_debug_tasks_r", json.dumps((name, step, config["metadata"]))) - yield { "type": "task_result", "timestamp": ts, From 73b4b6ec5b38526d5e993cdbe05bcfc4bb81ff9e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 15:50:45 -0700 Subject: [PATCH 06/28] Lint --- libs/langgraph/langgraph/pregel/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/debug.py b/libs/langgraph/langgraph/pregel/debug.py index 5a317bb26..389c59e81 100644 --- a/libs/langgraph/langgraph/pregel/debug.py +++ b/libs/langgraph/langgraph/pregel/debug.py @@ -110,7 +110,7 @@ def map_debug_task_results( def map_debug_checkpoint( - step: int, # TODO remove arg, it's in metadata + step: int, config: RunnableConfig, channels: Mapping[str, BaseChannel], stream_channels: Union[str, Sequence[str]], From 909bf4433fc508ab4ffb918c3df502b854ccae4d Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 15:51:37 -0700 Subject: [PATCH 07/28] Lint --- libs/langgraph/langgraph/pregel/loop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index d618ffa24..003204b4e 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -139,7 +139,6 @@ class PregelLoop(ContextManager): interrupt_after: Optional[Sequence[str]] = None, interrupt_before: Optional[Sequence[str]] = None, ) -> bool: - print("tick", self.status, self.step) if self.status != "pending": raise RuntimeError(f"Cannot tick when status is {self.status}") if self.input is not INPUT_DONE: From f974471f7d031164f9b09896bb8e92432a9a1498 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 15:52:20 -0700 Subject: [PATCH 08/28] Lint --- libs/langgraph/tests/test_pregel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/tests/test_pregel.py b/libs/langgraph/tests/test_pregel.py index 9b32fc149..2646a64fd 100644 --- a/libs/langgraph/tests/test_pregel.py +++ b/libs/langgraph/tests/test_pregel.py @@ -6887,7 +6887,6 @@ def test_in_one_fan_out_state_graph_waiting_edge(snapshot: SnapshotAssertion) -> }, ) - print("yo") assert [c for c in app_w_interrupt.stream(None, config, debug=1)] == [ {"qa": {"answer": "doc1,doc2,doc3,doc4,doc5"}}, ] From 3a53843185d64a2759fb422c74e967d462315246 Mon Sep 17 00:00:00 2001 From: Bagatur <22008038+baskaryan@users.noreply.github.com> Date: Thu, 18 Jul 2024 16:13:06 -0700 Subject: [PATCH 09/28] langgraph[patch]: refactor ToolNode (#1066) --- .../langgraph/langgraph/prebuilt/tool_node.py | 113 ++++++++---------- 1 file changed, 51 insertions(+), 62 deletions(-) diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index 1aec005fb..fc70d261b 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -1,9 +1,9 @@ import asyncio -from typing import Any, Callable, Dict, Literal, Optional, Sequence, Union +from typing import Any, Callable, Dict, Literal, Optional, Sequence, Tuple, Union, cast from langchain_core.messages import AIMessage, AnyMessage, ToolCall, ToolMessage from langchain_core.runnables import RunnableConfig -from langchain_core.runnables.config import get_executor_for_config +from langchain_core.runnables.config import get_config_list, get_executor_for_config from langchain_core.tools import BaseTool from langchain_core.tools import tool as create_tool @@ -60,47 +60,49 @@ class ToolNode(RunnableCallable): def _func( self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig ) -> Any: - if isinstance(input, list): - output_type = "list" - message: AnyMessage = input[-1] - elif messages := input.get("messages", []): - output_type = "dict" - message = messages[-1] - else: - raise ValueError("No message found in input") - - if not isinstance(message, AIMessage): - raise ValueError("Last message is not an AIMessage") - - def run_one(call: ToolCall): - if (requested_tool := call["name"]) not in self.tools_by_name: - content = INVALID_TOOL_NAME_ERROR_TEMPLATE.format( - requested_tool=requested_tool, - available_tools=", ".join(self.tools_by_name.keys()), - ) - return ToolMessage( - content, name=requested_tool, tool_call_id=call["id"] - ) - - try: - input = {**call, **{"type": "tool_call"}} - return self.tools_by_name[call["name"]].invoke(input, config) - except Exception as e: - if not self.handle_tool_errors: - raise e - content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) - return ToolMessage(content, name=call["name"], tool_call_id=call["id"]) - + message, output_type = self._parse_input(input) + config_list = get_config_list(config, len(message.tool_calls)) with get_executor_for_config(config) as executor: - outputs = [*executor.map(run_one, message.tool_calls)] - if output_type == "list": - return outputs - else: - return {"messages": outputs} + outputs = [*executor.map(self._run_one, message.tool_calls, config_list)] + return outputs if output_type == "list" else {"messages": outputs} async def _afunc( self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig ) -> Any: + message, output_type = self._parse_input(input) + outputs = await asyncio.gather( + *(self._arun_one(call, config) for call in message.tool_calls) + ) + return outputs if output_type == "list" else {"messages": outputs} + + def _run_one(self, call: ToolCall, config: RunnableConfig) -> ToolMessage: + if invalid_tool_message := self._validate_tool_call(call): + return invalid_tool_message + + try: + input = {**call, **{"type": "tool_call"}} + return self.tools_by_name[call["name"]].invoke(input, config) + except Exception as e: + if not self.handle_tool_errors: + raise e + content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) + return ToolMessage(content, name=call["name"], tool_call_id=call["id"]) + + async def _arun_one(self, call: ToolCall, config: RunnableConfig) -> ToolMessage: + if invalid_tool_message := self._validate_tool_call(call): + return invalid_tool_message + try: + input = {**call, **{"type": "tool_call"}} + return await self.tools_by_name[call["name"]].ainvoke(input, config) + except Exception as e: + if not self.handle_tool_errors: + raise e + content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) + return ToolMessage(content, name=call["name"], tool_call_id=call["id"]) + + def _parse_input( + self, input: Union[list[AnyMessage], dict[str, Any]] + ) -> Tuple[AIMessage, Literal["list", "dict"]]: if isinstance(input, list): output_type = "list" message: AnyMessage = input[-1] @@ -112,31 +114,18 @@ class ToolNode(RunnableCallable): if not isinstance(message, AIMessage): raise ValueError("Last message is not an AIMessage") - - async def run_one(call: ToolCall): - if (requested_tool := call["name"]) not in self.tools_by_name: - content = INVALID_TOOL_NAME_ERROR_TEMPLATE.format( - requested_tool=requested_tool, - available_tools=", ".join(self.tools_by_name.keys()), - ) - return ToolMessage( - content, name=requested_tool, tool_call_id=call["id"] - ) - - try: - input = {**call, **{"type": "tool_call"}} - return await self.tools_by_name[call["name"]].ainvoke(input, config) - except Exception as e: - if not self.handle_tool_errors: - raise e - content = TOOL_CALL_ERROR_TEMPLATE.format(error=repr(e)) - return ToolMessage(content, name=call["name"], tool_call_id=call["id"]) - - outputs = await asyncio.gather(*(run_one(call) for call in message.tool_calls)) - if output_type == "list": - return outputs else: - return {"messages": outputs} + return cast(AIMessage, message), output_type + + def _validate_tool_call(self, call: ToolCall) -> Optional[ToolMessage]: + if (requested_tool := call["name"]) not in self.tools_by_name: + content = INVALID_TOOL_NAME_ERROR_TEMPLATE.format( + requested_tool=requested_tool, + available_tools=", ".join(self.tools_by_name.keys()), + ) + return ToolMessage(content, name=requested_tool, tool_call_id=call["id"]) + else: + return None def tools_condition( From 8bd2d88be42327fd2af270eb22771f6f376ea42b Mon Sep 17 00:00:00 2001 From: vbarda Date: Thu, 18 Jul 2024 19:59:12 -0400 Subject: [PATCH 10/28] sdk-py: add if_exists for assistants --- libs/sdk-py/langgraph_sdk/client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 14c812e8b..8b5c476a6 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -257,6 +257,7 @@ class AssistantsClient: *, metadata: Metadata = None, assistant_id: Optional[str] = None, + if_exists: Optional[OnConflictBehavior] = None ) -> Assistant: """Create a new assistant.""" payload: Dict[str, Any] = { @@ -268,6 +269,8 @@ class AssistantsClient: payload["metadata"] = metadata if assistant_id: payload["assistant_id"] = assistant_id + if if_exists: + payload["if_exists"] = if_exists return await self.http.post("/assistants", json=payload) async def update( From 7ed5f9e4bcdb713f97e87f26fd8027596a717b29 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Thu, 18 Jul 2024 17:28:44 -0700 Subject: [PATCH 11/28] Implement AsyncLoop --- libs/langgraph/langgraph/channels/manager.py | 2 + libs/langgraph/langgraph/checkpoint/base.py | 8 + libs/langgraph/langgraph/pregel/__init__.py | 343 +++---------- libs/langgraph/langgraph/pregel/executor.py | 5 +- libs/langgraph/langgraph/pregel/loop.py | 480 ++++++++++++------- libs/langgraph/tests/test_prebuilt.py | 5 +- 6 files changed, 384 insertions(+), 459 deletions(-) diff --git a/libs/langgraph/langgraph/channels/manager.py b/libs/langgraph/langgraph/channels/manager.py index ce0d21189..524611e5a 100644 --- a/libs/langgraph/langgraph/channels/manager.py +++ b/libs/langgraph/langgraph/channels/manager.py @@ -61,4 +61,6 @@ def create_checkpoint( channel_versions=checkpoint["channel_versions"], versions_seen=checkpoint["versions_seen"], pending_sends=checkpoint.get("pending_sends", []), + # checkpoints are saved only at the end of a step, ie. no current tasks + current_tasks={}, ) diff --git a/libs/langgraph/langgraph/checkpoint/base.py b/libs/langgraph/langgraph/checkpoint/base.py index 2f73b52a6..8cb278f32 100644 --- a/libs/langgraph/langgraph/checkpoint/base.py +++ b/libs/langgraph/langgraph/checkpoint/base.py @@ -54,6 +54,10 @@ class CheckpointMetadata(TypedDict, total=False): """ +class TaskInfo(TypedDict): + status: Literal["scheduled", "success", "error"] + + class Checkpoint(TypedDict): """State snapshot at a given point in time.""" @@ -85,6 +89,8 @@ class Checkpoint(TypedDict): pending_sends: List[Send] """List of packets sent to nodes but not yet processed. Cleared by the next checkpoint.""" + current_tasks: Dict[str, TaskInfo] + """Map from task ID to task info.""" def empty_checkpoint() -> Checkpoint: @@ -96,6 +102,7 @@ def empty_checkpoint() -> Checkpoint: channel_versions={}, versions_seen=defaultdict(dict), pending_sends=[], + current_tasks={}, ) @@ -111,6 +118,7 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint: {k: v.copy() for k, v in checkpoint["versions_seen"].items()}, ), pending_sends=checkpoint.get("pending_sends", []).copy(), + current_tasks=checkpoint.get("current_tasks", {}).copy(), ) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index b61d05982..41b517e0a 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -59,7 +59,6 @@ from langgraph.channels.manager import ( ) from langgraph.checkpoint.base import ( BaseCheckpointSaver, - CheckpointMetadata, copy_checkpoint, empty_checkpoint, ) @@ -77,28 +76,20 @@ from langgraph.managed.base import ( ) from langgraph.pregel.algo import ( apply_writes, - increment, local_read, prepare_next_tasks, - should_interrupt, ) from langgraph.pregel.debug import ( - map_debug_checkpoint, map_debug_task_results, - map_debug_tasks, print_step_checkpoint, print_step_tasks, print_step_writes, ) -from langgraph.pregel.executor import AsyncBackgroundExecutor from langgraph.pregel.io import ( - map_input, map_output_updates, - map_output_values, read_channels, - single, ) -from langgraph.pregel.loop import PregelLoop +from langgraph.pregel.loop import AsyncPregelLoop, SyncPregelLoop from langgraph.pregel.read import PregelNode from langgraph.pregel.retry import RetryPolicy, arun_with_retry, run_with_retry from langgraph.pregel.types import ( @@ -839,8 +830,7 @@ class Pregel( debug=debug, ) - # create channels from checkpoint - with PregelLoop( + with SyncPregelLoop( input, config=config, checkpointer=self.checkpointer, graph=self ) as loop: # Similarly to Bulk Synchronous Parallel / Pregel model @@ -852,6 +842,7 @@ class Pregel( output_keys=output_keys, interrupt_before=interrupt_before, interrupt_after=interrupt_after, + manager=run_manager, ): # debug flag if self.debug: @@ -871,19 +862,16 @@ class Pregel( # debug flag if debug: print_step_tasks(loop.step, loop.tasks) - # TODO move to tick() ? - if "debug" in stream_modes: - yield from _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_tasks(loop.step, loop.tasks), - ) # execute tasks, and wait for one to fail or all to finish. # each task is independent from all other concurrent tasks # yield updates/debug output as each task finishes futures = { - loop.submit(run_with_retry, task, self.retry_policy): task + loop.submit( + run_with_retry, + task, + self.retry_policy, + ): task for task in loop.tasks if not task.writes } @@ -1063,7 +1051,6 @@ class Pregel( None, ) try: - loop = asyncio.get_event_loop() if config["recursion_limit"] < 1: raise ValueError("recursion_limit must be at least 1") if self.checkpointer and not config.get("configurable"): @@ -1085,211 +1072,59 @@ class Pregel( interrupt_after=interrupt_after, debug=debug, ) - # copy nodes to ignore mutations during execution - processes = {**self.nodes} - # get checkpoint from saver, or create an empty one - saved = ( - await self.checkpointer.aget_tuple(config) - if self.checkpointer - else None - ) - checkpoint = saved.checkpoint if saved else empty_checkpoint() - - # merge configurable fields with previous checkpoint config - checkpoint_config = config - if saved: - checkpoint_config = { - **config, - **saved.config, - "configurable": { - **config.get("configurable", {}), - **saved.config["configurable"], - }, - } - - start = saved.metadata.get("step", -2) + 1 if saved else -1 - # create channels from checkpoint - async with AsyncBackgroundExecutor() as submit, AsyncChannelsManager( - self.channels, checkpoint, config - ) as channels, AsyncManagedValuesManager( - self.managed_values_dict, config, self - ) as managed: - - def put_writes(task_id: str, writes: Sequence[tuple[str, Any]]) -> None: - if self.checkpointer is not None: - submit( - self.checkpointer.aput_writes, - { - **checkpoint_config, - "configurable": { - **checkpoint_config["configurable"], - "thread_ts": checkpoint["id"], - }, - }, - writes, - task_id, - ) - - def put_checkpoint(metadata: CheckpointMetadata) -> Iterator[Any]: - print(metadata) - nonlocal checkpoint, checkpoint_config, channels - - if self.checkpointer is None: - return - if debug: - print_step_checkpoint( - metadata["step"], channels, self.stream_channels_list - ) - - # create new checkpoint - checkpoint = create_checkpoint( - checkpoint, channels, metadata["step"] - ) - # save it, without blocking - submit( - self.checkpointer.aput, - checkpoint_config, - copy_checkpoint(checkpoint), - metadata, - ) - - # update checkpoint config - checkpoint_config = { - **checkpoint_config, - "configurable": { - **checkpoint_config["configurable"], - "thread_ts": checkpoint["id"], - }, - } - # yield debug checkpoint event - if "debug" in stream_modes: - yield from _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_checkpoint( - metadata["step"], - checkpoint_config, - channels, - self.stream_channels_asis, - metadata, - ), - ) - - # map inputs to channel updates - if input_writes := deque(map_input(self.input_channels, input)): - # discard any unfinished tasks from previous checkpoint - checkpoint, _ = prepare_next_tasks( - checkpoint, - processes, - channels, - managed, - config, - -1, - for_execution=True, - get_next_version=( - self.checkpointer.get_next_version - if self.checkpointer - else increment - ), - ) - # apply input writes - apply_writes( - checkpoint, - channels, - input_writes, - ( - self.checkpointer.get_next_version - if self.checkpointer - else increment - ), - ) - # save input checkpoint - for chunk in put_checkpoint( - {"source": "input", "step": start, "writes": input} - ): - yield chunk - # increment start to 0 - start += 1 - else: - # no input is taken as signal to proceed past previous interrupt - checkpoint = copy_checkpoint(checkpoint) - for k in channels: - if k in checkpoint["channel_versions"]: - version = checkpoint["channel_versions"][k] - checkpoint["versions_seen"][INTERRUPT][k] = version - + async with AsyncPregelLoop( + input, config=config, checkpointer=self.checkpointer, graph=self + ) as loop: + aioloop = asyncio.get_event_loop() # Similarly to Bulk Synchronous Parallel / Pregel model # computation proceeds in steps, while there are channel updates - # channel updates from step N are only visible in step N+1, + # channel updates from step N are only visible in step N+1 # channels are guaranteed to be immutable for the duration of the step, - # channel updates being applied only at the transition between steps - stop = start + config["recursion_limit"] + 1 - for step in range(start, stop): - next_checkpoint, next_tasks = prepare_next_tasks( - checkpoint, - processes, - channels, - managed, - config, - step, - for_execution=True, - manager=run_manager, - get_next_version=( - self.checkpointer.get_next_version - if self.checkpointer - else increment - ), - ) - - # assign pending writes to tasks - if saved and saved.pending_writes: - for task in next_tasks: - task.writes.extend( - (c, v) - for tid, c, v in saved.pending_writes - if tid == task.id - ) - - # if no more tasks, we're done - if not next_tasks: - if step == start: - raise ValueError("No tasks to run in graph.") - else: - break - - # before execution, check if we should interrupt - if should_interrupt(checkpoint, interrupt_before, next_tasks): - break - else: - checkpoint = next_checkpoint - + # with channel updates applied only at the transition between steps + while loop.tick( + output_keys=output_keys, + interrupt_before=interrupt_before, + interrupt_after=interrupt_after, + manager=run_manager, + ): + # debug flag + if self.debug: + print_step_checkpoint( + loop.checkpoint_metadata, + loop.channels, + self.stream_channels_list, + ) + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload + # debug flag if debug: - print_step_tasks(step, next_tasks) - if "debug" in stream_modes: - for chunk in _with_mode( - "debug", - isinstance(stream_mode, list), - map_debug_tasks(step, next_tasks), - ): - yield chunk + print_step_tasks(loop.step, loop.tasks) # execute tasks, and wait for one to fail or all to finish. # each task is independent from all other concurrent tasks # yield updates/debug output as each task finishes futures = { - submit( + loop.submit( arun_with_retry, task, self.retry_policy, - do_stream, + stream=do_stream, __name__=task.name, __cancel_on_exit__=True, ): task - for task in next_tasks + for task in loop.tasks if not task.writes } end_time = ( - self.step_timeout + loop.time() if self.step_timeout else None + self.step_timeout + aioloop.time() + if self.step_timeout + else None ) if not futures: done, inflight = set(), set() @@ -1298,7 +1133,7 @@ class Pregel( futures, return_when=asyncio.FIRST_COMPLETED, timeout=( - max(0, end_time - loop.time()) if end_time else None + max(0, end_time - aioloop.time()) if end_time else None ), ) if not done: @@ -1307,13 +1142,12 @@ class Pregel( task = futures.pop(fut) if fut.exception() is not None: # we got an exception, break out of while loop - # exception will be handle in panic_or_proceed + # exception will be handled in panic_or_proceed futures.clear() else: - # save task writes to checkpointer, unless this - # is the single or last task in this step - if futures: - put_writes(task.id, task.writes) + print(loop.step, task.name, stream_modes) + # save task writes to checkpointer + loop.put_writes(task.id, task.writes) # yield updates output for the finished task if "updates" in stream_modes: for chunk in _with_mode( @@ -1327,7 +1161,9 @@ class Pregel( "debug", isinstance(stream_mode, list), map_debug_task_results( - step, [task], self.stream_channels_list + loop.step, + [task], + self.stream_channels_list, ), ): yield chunk @@ -1336,71 +1172,36 @@ class Pregel( del fut, task # panic on failure or timeout - _panic_or_proceed(done, inflight, step, asyncio.TimeoutError) + _panic_or_proceed(done, inflight, loop.step, asyncio.TimeoutError) # don't keep futures around in memory longer than needed del done, inflight, futures - - # combine pending writes from all tasks - pending_writes = deque[tuple[str, Any]]() - for task in next_tasks: - pending_writes.extend(task.writes) - + # debug flag if debug: print_step_writes( - step, pending_writes, self.stream_channels_list + loop.step, + [w for t in loop.tasks for w in t.writes], + self.stream_channels_list, ) - - # apply writes to channels - apply_writes( - checkpoint, - channels, - pending_writes, - ( - self.checkpointer.get_next_version - if self.checkpointer - else increment - ), - ) - - # yield current values - if "values" in stream_modes: - for chunk in _with_mode( - "values", - isinstance(stream_mode, list), - map_output_values(output_keys, pending_writes, channels), - ): - yield chunk - - # save end of step checkpoint - for chunk in put_checkpoint( - { - "source": "loop", - "step": step, - "writes": ( - single(map_output_updates(output_keys, next_tasks)) - if self.stream_mode == "updates" - else single( - map_output_values( - output_keys, pending_writes, channels - ) - ) - ), - } - ): - yield chunk - - # after execution, check if we should interrupt - if should_interrupt(checkpoint, interrupt_after, next_tasks): - break - else: + # emit output + while loop.stream: + mode, payload = loop.stream.popleft() + if mode in stream_modes: + if isinstance(stream_mode, list): + yield (mode, payload) + else: + yield payload + # handle exit + if loop.status == "out_of_steps": raise GraphRecursionError( - f"Recursion limit of {config['recursion_limit']} reached" - "without hitting a stop condition. You can increase the limit" - "by setting the `recursion_limit` config key." + f"Recursion limit of {config['recursion_limit']} reached " + "without hitting a stop condition. You can increase the " + "limit by setting the `recursion_limit` config key." ) # set final channel values as run output - await run_manager.on_chain_end(read_channels(channels, output_keys)) + await run_manager.on_chain_end( + read_channels(loop.channels, output_keys) + ) except BaseException as e: await asyncio.shield(run_manager.on_chain_error(e)) raise diff --git a/libs/langgraph/langgraph/pregel/executor.py b/libs/langgraph/langgraph/pregel/executor.py index 4d43aab60..352b8cf51 100644 --- a/libs/langgraph/langgraph/pregel/executor.py +++ b/libs/langgraph/langgraph/pregel/executor.py @@ -6,6 +6,7 @@ from contextvars import copy_context from types import TracebackType from typing import ( AsyncContextManager, + Awaitable, Callable, Iterator, Optional, @@ -78,7 +79,7 @@ class AsyncBackgroundExecutor(AsyncContextManager): def submit( self, - fn: Callable[P, T], + fn: Callable[P, Awaitable[T]], *args: P.args, __name__: Optional[str] = None, __cancel_on_exit__: bool = False, @@ -101,7 +102,7 @@ class AsyncBackgroundExecutor(AsyncContextManager): else: self.tasks.pop(task) - async def __aenter__(self) -> Submit: + async def __aenter__(self) -> "submit": return self.submit async def exit(self) -> None: diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 003204b4e..b8c3fdc4e 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -1,9 +1,11 @@ +import asyncio from collections import deque -from contextlib import ExitStack +from contextlib import AsyncExitStack, ExitStack from types import TracebackType from typing import ( TYPE_CHECKING, Any, + AsyncContextManager, Callable, ContextManager, List, @@ -17,11 +19,16 @@ from typing import ( Union, ) +from langchain_core.callbacks import AsyncParentRunManager, ParentRunManager from langchain_core.runnables import RunnableConfig from typing_extensions import Self from langgraph.channels.base import BaseChannel -from langgraph.channels.manager import ChannelsManager, create_checkpoint +from langgraph.channels.manager import ( + AsyncChannelsManager, + ChannelsManager, + create_checkpoint, +) from langgraph.checkpoint.base import ( BaseCheckpointSaver, Checkpoint, @@ -32,15 +39,23 @@ from langgraph.checkpoint.base import ( empty_checkpoint, ) from langgraph.constants import INTERRUPT -from langgraph.managed.base import ManagedValueMapping, ManagedValuesManager +from langgraph.managed.base import ( + AsyncManagedValuesManager, + ManagedValueMapping, + ManagedValuesManager, +) from langgraph.pregel.algo import ( apply_writes, increment, prepare_next_tasks, should_interrupt, ) -from langgraph.pregel.debug import map_debug_checkpoint -from langgraph.pregel.executor import BackgroundExecutor, Submit +from langgraph.pregel.debug import map_debug_checkpoint, map_debug_tasks +from langgraph.pregel.executor import ( + AsyncBackgroundExecutor, + BackgroundExecutor, + Submit, +) from langgraph.pregel.io import map_input, map_output_updates, map_output_values, single from langgraph.pregel.types import PregelExecutableTask @@ -52,11 +67,17 @@ V = TypeVar("V") INPUT_DONE = object() -class PregelLoop(ContextManager): +class PregelLoop: input: Optional[Any] config: RunnableConfig checkpointer: Optional[BaseCheckpointSaver] - get_next_version: Callable[[Optional[V]], V] + checkpointer_get_next_version: Callable[[Optional[V]], V] + checkpointer_put_writes: Optional[ + Callable[[RunnableConfig, Sequence[tuple[str, Any]], str], ...] + ] + checkpointer_put: Optional[ + Callable[[RunnableConfig, Checkpoint, CheckpointMetadata], ...] + ] graph: "Pregel" submit: Submit @@ -73,6 +94,203 @@ class PregelLoop(ContextManager): tasks: Sequence[PregelExecutableTask] stream: deque[Tuple[str, Any]] + # public + + def mark_tasks_scheduled(self, tasks: Sequence[PregelExecutableTask]) -> None: + """Mark tasks as scheduled, to be used by queue-based executors.""" + raise NotImplementedError + + def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: + """Put writes for a task, to be read by the next tick.""" + self.checkpoint_pending_writes.extend((task_id, k, v) for k, v in writes) + if self.checkpointer_put_writes is not None: + self.submit( + self.checkpointer_put_writes, + { + **self.checkpoint_config, + "configurable": { + **self.checkpoint_config["configurable"], + "thread_ts": self.checkpoint["id"], + }, + }, + writes, + task_id, + ) + + def tick( + self, + *, + output_keys: Union[str, Sequence[str]] = None, + interrupt_after: Optional[Sequence[str]] = None, + interrupt_before: Optional[Sequence[str]] = None, + manager: Union[None, AsyncParentRunManager, ParentRunManager] = None, + ) -> bool: + """Execute a single iteration of the Pregel loop. + Returns True if more iterations are needed.""" + + if self.status != "pending": + raise RuntimeError("Cannot tick when status is no longer 'pending'") + + if self.input is not INPUT_DONE: + self._first() + elif len({tid for tid, _, _ in self.checkpoint_pending_writes}) == len( + self.tasks + ): + # assign writes to tasks, apply them in order + grouped: dict[str, list[tuple[str, Any]]] = {} + for tid, k, v in self.checkpoint_pending_writes: + grouped.setdefault(tid, []).append((k, v)) + writes = [(k, v) for t in self.tasks for k, v in grouped.get(t.id, [])] + # all tasks have finished + apply_writes( + self.checkpoint, + self.channels, + writes, + self.checkpointer_get_next_version, + ) + # produce values output + self.stream.extend( + ("values", v) + for v in map_output_values(output_keys, writes, self.channels) + ) + # clear pending writes + self.checkpoint_pending_writes.clear() + # save checkpoint + self._put_checkpoint( + { + "source": "loop", + "writes": single( + map_output_updates(output_keys, self.tasks) + if self.graph.stream_mode == "updates" + else map_output_values(output_keys, writes, self.channels) + ), + } + ) + # after execution, check if we should interrupt + if should_interrupt(self.checkpoint, interrupt_after, self.tasks): + self.status = "interrupt_after" + return False + else: + return False + + # check if iteration limit is reached + if self.step > self.config["recursion_limit"]: + self.status = "out_of_steps" + return False + + # prepare next tasks + prev_checkpoint = self.checkpoint + self.checkpoint, self.tasks = prepare_next_tasks( + self.checkpoint, + self.graph.nodes, + self.channels, + self.managed, + self.config, + self.step, + for_execution=True, + get_next_version=self.checkpointer_get_next_version, + manager=manager, + ) + + # if no more tasks, we're done + if not self.tasks: + self.status = "done" + return False + + # if there are pending writes from a previous loop, apply them + if self.checkpoint_pending_writes: + for tid, k, v in self.checkpoint_pending_writes: + if task := next((t for t in self.tasks if t.id == tid), None): + task.writes.append((k, v)) + + # before execution, check if we should interrupt + if should_interrupt(prev_checkpoint, interrupt_before, self.tasks): + self.status = "interrupt_before" + return False + + # produce debug output + self.stream.extend(("debug", v) for v in map_debug_tasks(self.step, self.tasks)) + + return True + + # private + + def _first(self) -> None: + # map inputs to channel updates + if input_writes := deque(map_input(self.graph.input_channels, self.input)): + # discard any unfinished tasks from previous checkpoint + self.checkpoint, _ = prepare_next_tasks( + self.checkpoint, + self.graph.nodes, + self.channels, + self.managed, + self.config, + self.step, + for_execution=True, + get_next_version=self.checkpointer_get_next_version, + ) + # apply input writes + apply_writes( + self.checkpoint, + self.channels, + input_writes, + self.checkpointer_get_next_version, + ) + # save input checkpoint + self._put_checkpoint({"source": "input", "writes": self.input}) + else: + # no input is taken as signal to proceed past previous interrupt + self.checkpoint = copy_checkpoint(self.checkpoint) + for k in self.channels: + if k in self.checkpoint["channel_versions"]: + version = self.checkpoint["channel_versions"][k] + self.checkpoint["versions_seen"][INTERRUPT][k] = version + # done with input + self.input = INPUT_DONE + + def _put_checkpoint( + self, + metadata: CheckpointMetadata, + ) -> None: + # assign step + metadata["step"] = self.step + # bail if no checkpointer + if self.checkpointer_put is not None: + # create new checkpoint + self.checkpoint_metadata = metadata + self.checkpoint = create_checkpoint( + self.checkpoint, self.channels, self.step + ) + # save it, without blocking + self.submit( + self.checkpointer_put, + self.checkpoint_config, + copy_checkpoint(self.checkpoint), + self.checkpoint_metadata, + ) + self.checkpoint_config = { + **self.checkpoint_config, + "configurable": { + **self.checkpoint_config["configurable"], + "thread_ts": self.checkpoint["id"], + }, + } + # produce debug output + self.stream.extend( + ("debug", v) + for v in map_debug_checkpoint( + self.step, + self.checkpoint_config, + self.channels, + self.graph.stream_channels_asis, + self.checkpoint_metadata, + ) + ) + # increment step + self.step += 1 + + +class SyncPregelLoop(PregelLoop, ContextManager): def __init__( self, input: Optional[Any], @@ -86,13 +304,17 @@ class PregelLoop(ContextManager): self.input = input self.config = config self.checkpointer = checkpointer - self.get_next_version = ( + self.checkpointer_get_next_version = ( checkpointer.get_next_version if checkpointer else increment ) + self.checkpointer_put_writes = checkpointer.put_writes if checkpointer else None + self.checkpointer_put = checkpointer.put if checkpointer else None self.graph = graph # TODO if managed values no longer needs graph we can replace with # managed_specs, channel_specs + # context manager + def __enter__(self) -> Self: saved = ( self.checkpointer.get_tuple(self.config) if self.checkpointer else None @@ -132,183 +354,73 @@ class PregelLoop(ContextManager): del self.graph return self.stack.__exit__(exc_type, exc_value, traceback) - def tick( + +class AsyncPregelLoop(PregelLoop, AsyncContextManager): + def __init__( self, + input: Optional[Any], *, - output_keys: Union[str, Sequence[str]] = None, - interrupt_after: Optional[Sequence[str]] = None, - interrupt_before: Optional[Sequence[str]] = None, - ) -> bool: - if self.status != "pending": - raise RuntimeError(f"Cannot tick when status is {self.status}") - if self.input is not INPUT_DONE: - self.first() - elif len({tid for tid, _, _ in self.checkpoint_pending_writes}) == len( - self.tasks - ): - # assign writes to tasks, apply them in order - grouped: dict[str, list[tuple[str, Any]]] = {} - for tid, k, v in self.checkpoint_pending_writes: - grouped.setdefault(tid, []).append((k, v)) - writes = [(k, v) for t in self.tasks for k, v in grouped.get(t.id, [])] - # all tasks have finished - apply_writes( - self.checkpoint, - self.channels, - writes, - self.get_next_version, - ) - # produce values output - self.stream.extend( - ("values", v) - for v in map_output_values(output_keys, writes, self.channels) - ) - # clear pending writes - self.checkpoint_pending_writes.clear() - # save checkpoint - self.put_checkpoint( - { - "source": "loop", - "writes": single( - map_output_updates(output_keys, self.tasks) - if self.graph.stream_mode == "updates" - else map_output_values(output_keys, writes, self.channels) - ), - } - ) - # after execution, check if we should interrupt - if should_interrupt(self.checkpoint, interrupt_after, self.tasks): - self.status = "interrupt_after" - return False - else: - return False - - # check if iteration limit is reached - if self.step > self.config["recursion_limit"]: - self.status = "out_of_steps" - return False - - # prepare next tasks - prev_checkpoint = self.checkpoint - self.checkpoint, self.tasks = prepare_next_tasks( - self.checkpoint, - self.graph.nodes, - self.channels, - self.managed, - self.config, - self.step, - for_execution=True, - get_next_version=self.get_next_version, - ) - - # if no more tasks, we're done - if not self.tasks: - self.status = "done" - return False - - # TODO how to make this work for both - # - online case: we should schedule remaining tasks - # - offline case: we should just bail, as other tasks were scheduled before - # if there are pending writes from a previous loop, apply them - if self.checkpoint_pending_writes: - for tid, k, v in self.checkpoint_pending_writes: - if task := next((t for t in self.tasks if t.id == tid), None): - task.writes.append((k, v)) - - # before execution, check if we should interrupt - if should_interrupt(prev_checkpoint, interrupt_before, self.tasks): - self.status = "interrupt_before" - return False - - return True - - def first(self) -> None: - # map inputs to channel updates - if input_writes := deque(map_input(self.graph.input_channels, self.input)): - # discard any unfinished tasks from previous checkpoint - self.checkpoint, _ = prepare_next_tasks( - self.checkpoint, - self.graph.nodes, - self.channels, - self.managed, - self.config, - self.step, - for_execution=True, - get_next_version=self.get_next_version, - # TODO missing run_manager - ) - # apply input writes - apply_writes( - self.checkpoint, - self.channels, - input_writes, - self.get_next_version, - ) - # save input checkpoint - self.put_checkpoint({"source": "input", "writes": self.input}) - else: - # no input is taken as signal to proceed past previous interrupt - self.checkpoint = copy_checkpoint(self.checkpoint) - for k in self.channels: - if k in self.checkpoint["channel_versions"]: - version = self.checkpoint["channel_versions"][k] - self.checkpoint["versions_seen"][INTERRUPT][k] = version - # done with input - self.input = INPUT_DONE - - def put_writes(self, task_id: str, writes: Sequence[tuple[str, Any]]) -> None: - self.checkpoint_pending_writes.extend((task_id, k, v) for k, v in writes) - if self.checkpointer is not None: - self.submit( - self.checkpointer.put_writes, - { - **self.checkpoint_config, - "configurable": { - **self.checkpoint_config["configurable"], - "thread_ts": self.checkpoint["id"], - }, - }, - writes, - task_id, - ) - - def put_checkpoint( - self, - metadata: CheckpointMetadata, + config: RunnableConfig, + checkpointer: Optional[BaseCheckpointSaver], + graph: "Pregel", ) -> None: - # assign step - metadata["step"] = self.step - # bail if no checkpointer - if self.checkpointer is not None: - # create new checkpoint - self.checkpoint_metadata = metadata - self.checkpoint = create_checkpoint( - self.checkpoint, self.channels, self.step + self.stream = deque() + self.stack = AsyncExitStack() + self.input = input + self.config = config + self.checkpointer = checkpointer + self.checkpointer_get_next_version = ( + checkpointer.get_next_version if checkpointer else increment + ) + self.checkpointer_put_writes = ( + checkpointer.aput_writes if checkpointer else None + ) + self.checkpointer_put = checkpointer.aput if checkpointer else None + self.graph = graph + # TODO if managed values no longer needs graph we can replace with + # managed_specs, channel_specs + + # context manager + + async def __aenter__(self) -> Self: + saved = ( + await self.checkpointer.aget_tuple(self.config) + if self.checkpointer + else None + ) or CheckpointTuple(self.config, empty_checkpoint(), {"step": -2}, None, []) + self.checkpoint_config = { + **self.config, + **saved.config, + "configurable": { + **self.config.get("configurable", {}), + **saved.config.get("configurable", {}), + }, + } + self.checkpoint = saved.checkpoint + self.checkpoint_metadata = saved.metadata + self.checkpoint_pending_writes = saved.pending_writes + + self.submit = await self.stack.enter_async_context(AsyncBackgroundExecutor()) + self.channels = await self.stack.enter_async_context( + AsyncChannelsManager(self.graph.channels, self.checkpoint, self.config) + ) + self.managed = await self.stack.enter_async_context( + AsyncManagedValuesManager( + self.graph.managed_values_dict, self.config, self.graph ) - # save it, without blocking - self.submit( - self.checkpointer.put, - self.checkpoint_config, - copy_checkpoint(self.checkpoint), - self.checkpoint_metadata, - ) - self.checkpoint_config = { - **self.checkpoint_config, - "configurable": { - **self.checkpoint_config["configurable"], - "thread_ts": self.checkpoint["id"], - }, - } - # produce debug output - self.stream.extend( - ("debug", v) - for v in map_debug_checkpoint( - self.step, - self.checkpoint_config, - self.channels, - self.graph.stream_channels_asis, - self.checkpoint_metadata, - ) - ) - # increment step - self.step += 1 + ) + self.status = "pending" + self.step = self.checkpoint_metadata["step"] + 1 + + return self + + async def __aexit__( + self, + exc_type: Optional[Type[BaseException]], + exc_value: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> Optional[bool]: + del self.graph + return await asyncio.shield( + self.stack.__aexit__(exc_type, exc_value, traceback) + ) diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index 7b56eba46..2e3c2098b 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -1,8 +1,6 @@ from collections import defaultdict from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union -from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.checkpoint.sqlite import SqliteSaver import pytest from langchain_core.callbacks import ( CallbackManagerForLLMRun, @@ -25,6 +23,7 @@ from langchain_core.tools import BaseTool from langchain_core.tools import tool as dec_tool from pydantic import BaseModel as BaseModelV2 +from langgraph.checkpoint.base import BaseCheckpointSaver from langgraph.prebuilt import ( ToolNode, ValidationNode, @@ -110,6 +109,7 @@ def test_no_modifier(checkpointer: Optional[BaseCheckpointSaver]): }, ), "pending_sends": [], + "current_tasks": {}, } assert saved.metadata == { "source": "loop", @@ -168,6 +168,7 @@ async def test_no_modifier_async(checkpointer: Optional[BaseCheckpointSaver]): }, ), "pending_sends": [], + "current_tasks": {}, } assert saved.metadata == { "source": "loop", From ec2892098b82611bc9028f5896bfad30d01d0eea Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 08:21:58 -0700 Subject: [PATCH 12/28] Update client.py --- libs/sdk-py/langgraph_sdk/client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/sdk-py/langgraph_sdk/client.py b/libs/sdk-py/langgraph_sdk/client.py index 8b5c476a6..ef87b5843 100644 --- a/libs/sdk-py/langgraph_sdk/client.py +++ b/libs/sdk-py/langgraph_sdk/client.py @@ -257,7 +257,7 @@ class AssistantsClient: *, metadata: Metadata = None, assistant_id: Optional[str] = None, - if_exists: Optional[OnConflictBehavior] = None + if_exists: Optional[OnConflictBehavior] = None, ) -> Assistant: """Create a new assistant.""" payload: Dict[str, Any] = { From 29e860acca3c2cf67a21f8e7cbfb5f3cc3b3c004 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 08:52:08 -0700 Subject: [PATCH 13/28] Update manager.py --- libs/langgraph/langgraph/channels/manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/langgraph/langgraph/channels/manager.py b/libs/langgraph/langgraph/channels/manager.py index 524611e5a..2492551e2 100644 --- a/libs/langgraph/langgraph/channels/manager.py +++ b/libs/langgraph/langgraph/channels/manager.py @@ -61,6 +61,6 @@ def create_checkpoint( channel_versions=checkpoint["channel_versions"], versions_seen=checkpoint["versions_seen"], pending_sends=checkpoint.get("pending_sends", []), - # checkpoints are saved only at the end of a step, ie. no current tasks + # checkpoints are saved only at the end of a step, ie. when current tasks should be cleared current_tasks={}, ) From 9d14f4f2f1cea55a662d6c18101fcbf41da828f7 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 10:40:02 -0700 Subject: [PATCH 14/28] docs: Add note on langgraph v langgraph cloud (#1071) --- docs/.gitignore | 1 + .../cloud_examples/img/webhook_results.png | Bin 0 -> 330238 bytes docs/docs/cloud/index.md | 6 ++++++ 3 files changed, 7 insertions(+) create mode 100644 docs/docs/cloud/how-tos/cloud_examples/img/webhook_results.png diff --git a/docs/.gitignore b/docs/.gitignore index 00cb44d70..e39d4e7db 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,2 +1,3 @@ *.ipynb site/ +docs/tutorials/**/*.png diff --git a/docs/docs/cloud/how-tos/cloud_examples/img/webhook_results.png b/docs/docs/cloud/how-tos/cloud_examples/img/webhook_results.png new file mode 100644 index 0000000000000000000000000000000000000000..c0fea310ab736477ed22fe84009c41f75e0888ba GIT binary patch literal 330238 zcmeFZcUV)~);=1#fLK64K#I~7q$tv>f=chb3P|rIgd)uX(t9U>NDm>j5IP7-6;J|% z-g}i2db!K9&pF$De(vAB=X;*}-Y z)IlJUX%GnS^(8{!Ot^MXAqaF?%0^mRRZd!(N!8WK(#GBb1d_;*=%mUph>e!I zbWKW1ep%^S47rs2C9Z%yZjRf;*W|+kv&^1TQySLkHV2i2-t|Z&c)ET+w1J1}Mm)Ie zh0bJ7K*>qh!Q#ncum6ScZYJMS@3;>UXq31_H$BW26wS^N{Tcs+FE&==W{#s2{!LW^ z?|`<2g)luewHZ+2r~R#k39^8WQP`D-aR2=aOm-*r{4Pi;n@M?TX?KT70v}}DNufXj zV(RkTy+>e16GUmP@FL(EPyCD`K}r0KJHaG7DkRKQdYmL6ls1X86JP4=;XKW|dc}ed zlSE~4?|E+ENSRBm$0Z4dTy>%SEb#5wBC||7sOrhJLG0u?of8a;?g%i2i`2X=3Z19KtWW-@R^k0D6mXE zttVFQfxL;eQPA5S^_`_^@*0yA1W|vwXQKKAI5FVkK>I*g`7HVEJSls%yNd4vbm`67 zPt%8ab9<7r?|XdCijbjY4x7TAy%~6S*yKYLrKRj^v-tg7ucJ4+Uj(d(g|ExM%3|W= zmWg33PH0bPzq<6=i0ZxW_8XDcr#U}9)%+m92@I_h>YXy0&8ytovmU z=VgZv=>oHBv&Q>fshb_|4%CnCevd}s|Gd+Q{6RSzBYv~NnM;w)?WcFmlo-1NeF~PWF4h^#`+^;Y`x1wrtTTB~ ziH1PiBh9b@f&AuVx^y0jLBZM>ilbH9Ju2oXcB0iw7c)*jI=ddGY-!F7%uEUhf z;qx4CpJn?|2rnNj;L~dpe4IDTv%--tghYkwKujl%ca2S)vKOYvWKbKTf?HQX^=U+e zgpse7o}}RnZ(Tw8&p~>Fh$ZQZK<`oZ=1ji?giO+2?3cuRAa#{;N; z-~}?>ka;Z>eVMI!xnf!*Dd64fu?KjvL6vHGQuf?Z91w_w3$vG1m;wOCq zJ5!fraR0#F3xrJby=G#08~W|K?`Qlnnf2>dZ$l250%e8B->@*-$gldZMvSb0S}#8?LL5xPuT$%vQCkr$R?cI768xp|B(Nn)T}sh;jV?MN+Cq%Os) zUY#=KV(OFk6fPpY&GgeGS3b?TFi6m3zT;`b%!zJ`)KfNvN`5~1;e^leDf)%V4;JT3 za&Hw|Zag9WkX{OPPIji<%;jzoy%STQP{~%?+K{aHl}(wQo6V7FlQo0M;~FEyi5yF; zL}$jlQYedy96=6wzevBMI^POOVc6c|r&?cRzUZ;avOay9qh73DQm(L2-9?#T#gW-b z`%TER7ug>^@D*)XT%)jCZ;7Ik>z3zK4MXso*ujSG!Q0)#9hI%nUVC194twZ5ZinnD zq`-4ggE~c@D33ai%*e#ZFy+_1nots`xd^m2xg>%6xu6cDa1&>Ot~N7>hRmFd;S?Txb%Ew#I8#BhO zUabnjj5qz-ukly%g+EAltR5`wDV^#VzB?>yU)j4HuZAcqDa&-J+UA&>YZGU4r(mS` z(8&>J8Mkzqo-iz?TesWVVlRa@H7HqL36)%x<|j}g5GKeeFlOg5m2TGisE^mgV$y8V z%GB(^kXg{vh%fXC<*ew3-L4S}dff-wL)szQq{XZ7S~#{?cHtw$K_ColE$bgWT z`vCYdp+N1pYi?a6*l2om_IR*oLTOfeZD`JMCS%cf#V@X~C)|wNGGmv? z^U&w(zR>FScXUrh^KQVBiSZR)|5;i)E-3L5xorB>!qFdyiFxN#BoN}t0;!EV>Ohcw&`=>fhX8ngR#K+>q<9zRcNH1NvOJ;p_fF%5?A>$!a z$Njr}1#AU8!~CA!KSHiOk=51Z=P;hE-X^GX4i>o)%fkr`hzsoBuay*V**d~w`tbS$KCbn3GAi0_SQ9Jdoh8i<#0 z4)P;eux*}e+ZyeenJ;*6Lt4s0DnrIYh|rg8LTu{#8=aYJw8qcfmAx&z8KZg(T75np z%pqnEF4Yro;`cqQ=hJ@aZsl!N@^!s)r-!3&m9qTZ+B-zKbh)5KQ(wr}kr=+{wB$NL z6;>WDgjx=3*L9@w!;D8@_26M+!#kVl>%v~xc?r4Xmp&4)k`hfHr0C8&P;w4AO|pzs z$2YWZ_$9~kE=+6o6lNDXpx5)bQumAR7A&Jy4&5jB5^3`I7JEH~;`rUV@4wVOE>0X- ztXS&XOr;RBS~Bb!Pp@gB7Ntbo^%LcBF-E`oc;vPydCq#6luC{zm+R~yFeVk?`b14*CgDKk*ob8HR4_`=(mK+f9cqEApjhoiPv~}0s~d!4-Btb* zw@1I%@L5{fA$`DD?p7lOyKl5MN`0FH6G;d7>ox`8f{Ah+h|`EfsY>nE(Rt;W;FY)Z z*1{cPx;`B{stcyaGyNhtj`~I>zS_Q4(yr3a&8^K9;&jk6%#qJMZt;ujyC^OV_s_-8 z?M7XFM9qJYc`b8UJjH9rrTcSYU4E>xkzItNgCxDj@pwuWUw&_SuR!Lm1Z1y%>8OqL zD=C(Gf_n&AQ$>G1fjwy5aNGbVjq~su?pKcwJjk0nq@3bpM-Uo{yG6Kd%wpfK(F@Kx zjn}4lSL{+}0-LNxg_at~^cYtXOqi+WaW~SUf2+%4ZV$8|oF=KtV&%u%J3enkBQOSpr8E5f_Dd4T1H6I1hAM5-vzylxEY=fa)i4K>Rcy5wT3QwtKz)JFoBd7Q5n~Q)s7OU-&{6 zpnrJ5FhP3Z=!jeDL?`C=*jJweq^1d)6$6QQ@2S#@!8-dA%AdSl3h4Q6g5K@_)O0VD zZ|SPb#W-$C5$R#g88zGaE7{&;TOz=v2({3avs6+7aRA4cK=?ScAOheB2Y5^1(Ea=P z8O~i0-apRcfnU=rZw<>tL3LBhnuBpN|DRAkWV$JP){edH!8DP*m*aSrJtmPYZiJ85;*+%z!o| zgoK5}{&D~Raplhze<`W^XUT_xLXUne`pc#NEUM*Z;VSLq0JP~Y@#lQ~v+%DM|5;Fs z=jWsUG8BJ|^FPi411&)!#`Et$h|M&)uap}Ll zGZw1>fuulkGLJPqan>h@Ufi%aZr^5SVYw~Ew5Pm$w_iq8SxrXmv1;l0Rq+OmKJWe4 z4Q5|Ttc2G>G9cQTp?7tqqzKds`$gZy@Pk zUaM1Ph@6rV7db8NC8hw-fBEs2<(f1|kcFM<2?z)On$&;w(M)o!dEv=_SLPoLFkQyM ztu}p8cK5&R?9a!;cdjA)FZ=nk%}de&Ajg}!_e1~N0Rq~@F)aD7#|CJ#{Bd(2FaD+JpujKL{{uk)rjqu5r}A$G{eP$O|3y>($6A@yg;#Br z?h?ep%9=2io1T7459J8GZ&0Q9@ZrNQqYMHLldO8TgtRnHIsyCArF&dlO8XmQI!Dlg z7Oo8M)xy}=*xAjQG^fV0c65;zCykJEmZ%Y;bp@SYH+T8B*6_!3^5x;OkGH@BYMZy$ z&PMwrPQKC~T&c7lQIC{7_uOJh5%VfvA1-Tk(g)M6j8x>iZ%s~YBu-u(IC=f(Z!9E} zW*`UI^iZ*$#-#7TgzAY|5;{7+gwLj*5v z4FMoV!)q87Y421}15Fdp$dSh*Cjuz7< zTz+r+4w;G5{m+jcnti@>5WTy*YcWuuwpk6yQpuEX9dkf=v5)vTaO#xorD;z*WAK{4 z{ynPn8&0YK2uY z^9Gk&@}z3vl;gzs z7eF5F9j)YN76hz?^8ViD{$DIP(~ZCaB4^3NX}lC6m+Uc=H+EGHVchU?`P+y7961U| z?UiNo=EYvET=yIR%AzpVHb8JlCo(L%qq*w@LX1C1s}( zWP-KnY(H&kPxcvy>*bhr70k~W^}U}_alSeTTUbnU@_6R;^cR~GXv=nuE>qJsYM+uy zR?XC}aMwSIub!cIyM5q&9Ieat<_y*1Qu53lh}bwzlkbgrx$liF&Vp(d-I(_Z(aE%B z7LNWVj~iFq9;bBkQUrF9zgH^YBEAP_m8c21vACXi`V}7AC2r5PCkeg&^oFS0BFSh`P<*(YmXID$HusHDtx=Q{ z4;k&FXzK`?dis4l!pqSTzQ?kyL1ej(6Sc8`xma0Q$zJot4(gPf zwi4DAb7KlM*z0`3F8bQqiHeyL6OpUi@mfkkx*);irv158yOJ27anZUm#H#QS`N+kiYPj*q{zgUOYIcGHzm%b_?rBsD^Rw_$Oc+|O;R>MEdX6B$ zbhjD%G@W0KL;-DJDwtNG&J>bRv)$n1AwO9{R&GA?vQuG7cd@)FIEDV1*xj-a@WzLA z{Rh%jy}l-^5V2>ew_GB9JUJyBjYxLO$G4pc$<_7c$KsyX!R$}>pN>>`g^2oP5$WGL z6D&lJUThVx9&1+i1u^ky`|_+PG(FIp_hqFrePWI4f{azF9)o1&@cKv9l*mt_FF7d0 z@Z<)rr6TES={owxErXb^CfCswO-s3sG3TiDnUW_BWPMH~DMxQuskf~%)||^P^L`K^ z(R>OL>_4fE87FdAG^nz_*G>;fUp{-m#a(LAP32?a0ceP8E195ddn7&d=|Tv5Ml>)x zb#pWOC@q+_YrhGamUA~&I3-?CPKmA z>@@+#jC{u!`}9nLz>5>!Yw&WjFCx*212GyTuKj`qi#7P>eQ@sHmS3Rk>tT$Nbt zNuzepP`=C`uy+4sHjL+3t7TAi$7=~{C0$ypSl!Oh`=Fpt{9xStO=m5le>9d=VZN-L zA@1u}#k!8+GV={G2dks!jJ}$SUsE(JL4xDIlet6h9u}}{C(i}Jn1Pzw2eOZr#p#sx z`&*X!T13tibd@mz>=X0e^zk@`sDnE`0$i~XXp?YH^tB$ zT_KoGSNV3Q5-WI_ezQuT$Mb2q0Jn4dNAWDJgRHOy_Tr-db3o>r#p=*xFX=2ZORepN5UHRJ4O9{3(>DtXz?t9`Vxwp^E6w8out3w zKIQyV8E0NX=Lx%(R}Zw!Y~zJCD@U9<@b9MSrQ?NTW2;g0jg;f%@LJNfjHwYP6k1nI}6=U zeV^FS&JOy0|Gi_D8M5tUX|`KC(6u&Uje|x z!^;<6@8h$GaWFXRA#Qa~Dt{axRsD#ajh1($3m6lBWWv=-+&7al9;p)h>sX8lN-$^Y z)vi+0xy1S5T=t-$hK9yM5v*hf7Jkxnai+4^m&sEr3Em@&K&o%ecgAgSKdP>ETT-@y zuo|}(2EaJinENZ`5h(GU4(5lDO~bJy=O`ma7**GUN{2D+&FYWUT@jlXz?xYV^)f6h ztU>}ney(-7?Y@Bd8QE^;b@0M5Y#3qKtns`!JDBk0`wLJ>;JJ3QxyMjYMWD|cy$PTl zVyuB|oWzo>S!>m1FN!CbNtD`mCenwUzUpwW%E+=23>n{U#>xCL+1yFwIF8@3j-*}p=E&7~z(4zyUqiKPyqqn` zk84?fhR~VWKkCcMQ^q}CbtrCi2$_Bw$#=n%z)i!4S{_^{+9a+IbDs6+_ZPGmsyCs_ zHXj~K7;JxcSUjUTr42hAUjdt)Q?FWujeQ`w5`RFW<+|$~k+N11KgUxerg7!1)C<7C zNqFK_$4p{2Ch8!c4{)67HUtcCFk@SK6_z8arIY6Lye|SEiS!=0)iL|1dQgd9;KiVh z3G$!}ne`gI*SN*kC&nBS60(1ef*2bcXPj<0`Ny4}#x`X_v46o{zu+D`_G?&>tbYDVk7h#+ zEK1Ag$0s6a=RS7o;v8iyr4waM1-!%qcth@uRcK{VCGP5JT!^s{C3y?kw5=ra zP8q5J&>n#wbSq_6mX?V=ms^9cQo|G1AHj2G$mR*PCkI0-1;?G=7>SL89Fw?>?nqE8 zUwHlnaduJObIj=O5cs;p^|zAEF;qK4U7a$W+nr8DnQE9qJ{D zk=swS$o?ek^bNUCmL#WWs8k2vXi|;_K#pvyBfG*YSH6>ulkkKo1$j)MM`O^2$NglM z2%ikgstGkS3R?x^XQ}JPyKizX_K{=(dG5l-v8muh+Oq9(jOId*F8^5X?$zusvaVwi zvE%2ma*8%w{FSfsZ1g_~Ku%~ob;ZYSp~Xa%{P9Vrha6p!$9Rk^(czevQ?Zw zRCl8A1-Sj=gKx9TO>)qoh6y7tyz1W1qg+=K65EY8!{vL27Rt|IOXJ%BXp%qc#miCd zW*|?}oVwA2eJ6RaIdacOfad|u<~{Z3I)ln`=d<1~kGtS*Q-Y>bTpprGT4Yj?c<4oA z<5`jl3uMV#R0Wa5_B?%I(+N|)5{oo#6lJhyYBOXetAn!0DT&C{6DI;#o;R@9D7L~E znZuL8QM1zM;Ddn=7C>`uxxOaOM%@r+6JPS8Nwet7`?4sKwg_q~>t#6M_b&sxfh$=PpcB6Q@-^6CWU<5kSv=COaa~_xLP33@6&`sJ%)-C1zG=0T-KXTwUNhG#wR=uIJm=ab zsC*-TsRi z{uj!S!cr6@;*0}+`ly7lBBMhdmoD;+%Us)$%@x{5vieo_E6N5)+k|ldH0(6vUF`tC z=B%>+R>OhJ#jXIEd*rQ}i|^@{&n)nr8T}4Tx3$d%Nf4v#`8G%sr)bUmC>p*tZPRaQ z<$+PpyL)@9w4AYQrP}?2t5uOv6+PC3&PZ~H>AAj2^*0is-2LVk3rQwjWYZ$;tMveo zQb-kWu;qMSfYO#zmfty>tK`;*0Rg@bc!O+5w)Y)c()ju>Y+jZt!4u+X+ko;jn}a&h zp{$QVct{&cP53m874C~qsf2ujz6Yr{7Olocpcjb4uwdaa?co>jij1J8M>mG&-@B#D zsarouEerW#^26QoWeL>oNp{1lPs&!a=a}#WJ|%0g|J@GFQv1<^iyY&Va~y@ED922v zqGRrTEiB+B50>VjOaO0Y;;mhU;*Y}{yO}&JMITJaM%vhhuv67E-TZ;`U=uON{3NPo zxZRT$S`K%0UjjDO;txX&dSeRq2oCnQ}AGt=Ld>nE;tE>Xv6NnPMF& z{$=YQ;Enh)f#K1v`)idBP2qx{h7jx4A2V8WKi&Rf@_CSV{|ZIhK;9>5G7(XSjispM z7b|R5m7igR9tM}Ib!XQExE}PSWKq82uU@Wv#&$T-{MHG&JEI6O-i0_N0@&E4Po*03 zWNwm~CAXW&%8k`n$KN}O?34DqcwV3tSs0-mt?uA~ReMDnVVXmJg3d_w03~zAb!KQU zgWYS`wg)L~tO>T$$nn}pogUd`%oL*^W`#DjXQHf)TRp}tpYGd1xwgX<`{R_{B}2~J zked(MwC4sbb`(7g5LWp%0lIHKE02jBh*P{j>!rc^M@;h-Swe&QkKeAzpN$XF9~Pyo zQM0zdo^4JEw{d7>mE%@Z)*$FS#ej6Q0cTi{Av8}#y>32ML8i4-`KR-vW9;=sBI0XG z-hqglo7-9AI%4-p!&UnG0WboZy~a|Mk^A?jegstMUTYXc(w8vmv1_FgPq~Ai=Nb>>T$884yP+gMe+gAZb??nnMFF!wwRw?D- z7bEUeobhIo#!+J5$~3|^*gW@~c80D8)|6oyD&ks^LbI3X>cKd#FKe;{z|z9@4DnY; z;(i6CS>*VD4y8Kl#!Xr!bL6(y_@Ilb#{AC)-u99I^l%(66&Dn!?kHU<&nwI;j0%CI z@?6@_qsJ0DLyvmxN~O;ma0 zg?gPOWstrrBUv*B6j|XOoO}Ux$N33Q>998~TQSO0kD@m;vmC^iZRaLC1!2~iFC7mD z-)cgJv5yp2E4d^a?ViL*$_K|>t-&aGjFON1=n38{zb`R*rILTOLWq9 zS}SkIg<7;&8zx!TXH+sT6<$g6>dlC@0VI(yfd8dlg|ay$RoJH7a)&t&*D1A-V{`)F z<6^THU6ijcT$M-s(Fbp4va4wlR8p5sb9Y1kJz{KF6SQf8+s!rMu!>Bt=sB>MtK4cI z0dvlYQx#twJs_KEWZ{ae`K*Z-c_5nUT}&Wc$2z9zJLn9qOw|TE2#y~}4ES26*geNP zovG{+f#`1tNxG%MZ>{_CY#Hw49-W>jrZTvc7@X;!puAgr_eC%>Y`(pT$56uMEb-uD zuSy=t_DG`h_w#)6Q~qRE#5+;<+$QqUIKI%HBhcAtEXkmH^pNjS4xPM5o-jfmX?PKq zv!N@bwE5EsZnD`?0JzVpf6ius%T-dez2N?b!U8ULiA$+hgE7Vwth#ZlC&z30MuQnqSGTOD0Ub0kBV> z$LZJBgKR$!K8nh9njy4@!V#IrmGkx{797zTjvO(z?^G#b`9FOkLuVuyY7ncp=FpD9 zmo)Pq;gk(hFz}l8)05w99wb50F86fH#@(riczeus#?fO7a-IvD}oajh%0X!&(Kw5qoM8KAb#`k4EqKmqWdh@r zWzpznp(+T{x5!#WZvS{+xz5SS*=w!-ss*OpNdey(oVqWb_pEp$H0UjI$KmllcfOsP zFWKd3TTiqDTQoeX_MmE=tNZ0gV%Jqy+WB?QMRUjKVYTO`L$OtV8@c2Cb=5DtWv%BXV*n0SL-~UG)L7sTop`scd@FB z)t24YqHp{LMV(<(zG8)XG+#C9%*MVlkr-k+0F1>0OpkWO)=itCgFXNq--Xb#zq)m^ znQTG-TZA>iO0B~c)EK&`){ael-LWov>jBvpyKlm78)9@U4qodPj_)kVst||E+I5J- z`8)x|t#!}cuNYRhGoD`S0b@IqkfuI^PfcCZPA|MJ%PQ+(uTgJRg21H`9Dg{|`0-o2 z9&EJP!K~)OU{M$Cx<{(ty~mQ>U!UIDP-&Vv8~8PUdCi=5&u1V)hP79mARMP(o*Z#nTp29@#iHrSTOC zHpNmc#&P1ugnp;n&zqiqSh?dcS{Vaix6Tj23>^A}f~5~zdZD5VKWQujPM&(&mr0vB z&bZaU6HzxjrZ+#o-DflDr#U~|EgD4URIgTfT!P-;oUnrt@)%aD3OJ0i{3Jo>9PgeJ zOr>XXBP!ns_hw&|(gqQgS7nzE|0RL)1TQv7{v7 zN!mW$LZs--BPWm!EVLv=sEyDKO0bs3WJ^Jj2Ts-C*KS{{T5)Iz{eqL#Q!!Qf=?OGx zn0JupGRkX+Zt?oM0Z9lyF)SG^{ClW0yVt(sV6pG7edaEvph-!aGa<1qfB_XZ_x7BfkBV13NU3 zHGAGPWt*jEI}m+XtHdgdy`(|8rwehbOYGyt89z`esLd~I@h%hfZN;3x##5tBU!d0> zzvM5 zWAog2w4`vUFn`jf#9^v|!^jvzdU}6U=!|uw!ltakHbkc(qvb5z_B@}oXUUWiV(`vO zFMCP%;Yt>kU)VSA=B%&U*&JIHtFin`9hYsKL*x z@+*1y3s$AA)KYybm1q_tX}8g(9~0Z zyb9YnTFM-=iD9T4O@LhXZq;KHU#bKaM7_q-9R;faiOt@VzyG z&KfS&=(nn(&@&mA+@Z6_N@N#R!Yg^r6LoWGirDYPc&fewD*$(tKGZHEVMc-Kb5@7NZt z2rPSq8nOFyt7~jjgwaoyImZ^3r%h`c53Oc$_VAmjzwq!YWrV9#KeOfETz0f;#aQ^` z2p8&6Uj!hQMBJ*TEK7+q1?}ygAJ`k)bAkYSEnP$9j+YCt&Buk)KKCc;O^_r-Xj*qs zIa)B|VtCiifz!heTc-e<57*28P)X+zHP-YZ^Lno$S&41Im`#o7(6C((Pdfdl8nk?G z_Y~*(opMK$H^&=66C3OD{f&(5b(pVZgGl6SI9l7^194PLqx#A>wXY;0Ga0+bR}oIN zX{8Bo+jt6_7SYPW6NEPTgstoD)f~NUQ3V3Qe;>`KRjqdQ=o6g#;XQa3CPE?NN(B_#g>j9*rN}DtbH| zL`GM;%Wqf|sKTBFyR8ZTx>^aKRh7M_>&(yI3q}|2wns5;1zD>|p1IeBS7bDL<5lZ# zYja)`Qfh6YZ9W4+V)o72cxC*G;9L2^IO7`1!cVzt=E<$f=NA`8&w5k&yR~b?_lM0L zl85x9@P0~r4H>S9{~5OTrNN*UUQ0?!ibpEp6J$#XdXEiWk=gvi7T{6q&yUvx!s`K2 z-zp&s@d>xOHfz#-C{Hk0LtTA5t<_`poDql=#@LnAv^A*RS}7wUy-vi?3NY+WO+OHV z08AWzVBU5};Eq9 zctRX^OeoFfl}dGMzh|jrdbR|5`8Jg5WJ!rsmf%D;w^i#>kc zfV!Qzu^#53iuF5zIEGFM^6)2xgLk2en$1eKLxXQUc=-GecKmOFO&bfp+UncC+D^L7 z$sAC(-h>e{Dn-yM1*qeF98I6&;t7C#=;x1Pc-X$N_R&Z>rta&O|xqH*Y z*!b9+!mM|UF_nuPE--p5Gc~p5R>h6F-3_4MjdB{8Wp^Uu6tXq2N!UA<_yM+doCx|Vxu*8_ub8u;#YZW{ii_YLu!x4_ zEcGhpgVs+l#ehNLfVBrgT)AsZZugnf)#!u< zJGC!)Z%MoyavESVm#-Nr0#lr{Efv&WghqCAlHNC*IbJE_FMLe4f_qXfB%3 zIDnt!&wWLzj_${0nH`#3SbG}D7+Lt^4XWVO7q5)^sKSWQex3#%i1RYfCp6L)@0^LJ z5^<>8ivJ|~_{Wcdq!QUMsxO%{dU|>S@vs9~{`sOoFrDDO(aDuCLQ^2SfNtA&Srw2! zo(+TU#KtL7k#iuR)Cq)cz^b!za}FR~>CehO3kfztd3jf_dM&Lek=463D|!8#o7J2tIAnFU$y!duwz)$;VPDYmhVFR&b+YMmEW$s* zv^5k4k4I+oy5UySb`1tt0YQK+gD<@o%O9g;VDm$815A)5l;e_!#+yP~p`quo^d7^e zet}a!lzfbXxHdh=oBtABv5f`6#4{UsCCJgHTNI6S@^(80;#Xj{k@Kgn>$QP9ku< zz1yTR)HiMn`>yraz_W|Byc?7lALL_RS2^QVQwmd0O7MpNifk`3$wDFQmVr<4%@nWOxO~_Bq?zG@`M{||SvBKgj?knl* zf}7$QK%PX$YTqMXz46pNVFQ4jE2nUU6!Rr0`9Ctg%MWLB&tj^h%8RjPsr5=SZvT74IT(|5=WyfPoiP2f8>hcqPCaJK?-+ra4{!Gcq zIwsbHfi_#OO4>`uUqS1b__FfU|9H*EL2dG?>@v;6&-7OXN+Dau-@9t`KMdIb>rxKc zmbWy#xBdkJhzbK<$9&CFM%?i5@Qbif+baDAjEyo#ryZHv0ys#kIx&NKy(dL8T4cpP z-LTF#L{}35cKwkxQg6#!|7^dcZnOM@3ADu5cYnmn0ja;`Dst;E8QFXk13(eDKC6nv zQ4YM6uDU@ss-fQen`$u%*Lm6xuN~`M{iB5x?of?mIH8@hbf=UtocT|E3y@m41dw!V zWCB#F&*5W5@S~9`bC;0L{(jBt5fKLjUn(EIT&&If$k05v8t)=^uSz9wP~G5`Zw!D~ zy5EVZ;*3X3HQpUD(2-sJHg<;lPFYJ2^+t{MM~2Qst_i{TnQwt!U0@}h-`4qqnDB&( zm)+?Xi3O##tLQqVXqO}-WloR0D>Xev?LZnrgi7ocW=0M*HQ_TsjmHCqqCJtvVL(~~ z7&C}NrinOR7PvR6Gc73Yob%}eyWB^HFDCdNMa*liKw?$SMklHo2E@@HdvIsi7L`g^ zzK-Aas0|9hDEYR%<@7zcxe7jvd$e)lzbRs%bm);pJR=~seq-0f1#~}_q`K;OHTa`a zELz85)Q6h`V9RVlkP}2^M=vXS+&p|SU@Lvx-h*atQyYOW>dsd%ZnF$gDCUgt&e(*A zuk&S!0<@!z^kjm|X1XeJBY@ZtCAP0#IB}|qlBAs-;zd=>S3Q@lIH#`OO~)Y{LO^$t z`Se%qsx#G2cHCq*?L;3>!6kiPIHc28iXU0J8J%$W28|L~8Tn#^088v`!69Lof@8R$ z#Bu;`CHwBZg}2&Z$fcR3QcZ%ii2-1bZCFkdfOoOyc~lD4=scwwqmw_z)(3CmVuI zVE1_-xd)k`+7QLS$-xnutB}+Mxch4;M!+S-40hoG**Xp(?Xhsfn*Ja+MxXTuC!V}U z9wX%zu0R^sT9$)8QX4PJyFw?p!Qo(YV&@CBNk=fF-(BxF!(+$THFh9gdH~yB{fe=@ zdMs24B>Ct7e*dy!6~gy;FGlU-GdUpZ>?}{`8Ay!NR*=`Er$^B9jv%9M_$i&Z@+9TwGi>>**Y;9|N|Z9U!!$+_yG;WD0r*zU`(9 zyR9G5^I3-1)iX@>2(C$H9O(njgkI)fGsnaN0Y8J{ABkUIGqFliN@;{QDipP3$=(k- zdLoK^wsP}?i6UyR z@*^UkdXCnZly9qt{=+1-z158(l#Xw-I(q&|6G67KS^=L#srpD+qpYe5iU^#Tuv?TR zqbP-=i|Xhq2fwY+YLq>aWLCnJuG9bG-naNv9sRaut!l%(YK}CR>g87DC%1+w<{GL> z>7Gb2mDn+Qo0B}rN5eIOZm{iFt)BeF0|&Y`K%hVhsqNas|7q?kQZmJP=`zacJFT_% z=Nff{d31CpVcdcLq~}Tbh@nr)BJF@S&5|-I4qEjY-w&Om{ZUgxs|u=IuEaTIOJJC9 zbFHeigDCe|`US=v*^r1)xjAFz%Q0IF zk-JA)#*Da2c9(S02%T+!rC1q+a$D-%@4o1r#Ad+!0wsRZ!!J#kfoKhoB{lJ3Qgq1Y zz5cfk#tpMY^WIKcq>|Q;U(ZGHoKJWILB%d0p=ClMcx*})$b!Wqo5=tV>1E%ZYxYSO zG1Yj^KeK}1HYQksBXT9H_rY~Ry_Pvxe+aLVTaxRk`x97#bObnc?L?!pww zW2a{ZY#oAq+~}i=3~PHy5z%84?XUE?Jl1~rZL{(imGdY zqBg_YSs_azq6{Xe5U;&c=%!eHmrY!2U;HsN$}K;Me_D+DT#CRM=>)_nh9#a&uH+Lk z3HF_%_-GR_0Ja>x?zGTbZ$xJS#12PoTlG6p&kVQY9OT#Bhk}l>wfvykO=yr!|B}7c zfOeqK_IGQ#Ga--QDt^$vWxZ3mR+-kS=}bC*E6mJZGc$s*)ZjfJFBizOwXbj7HX(@|8L{MHXl7 zhg&m^jiv6NR1DZnO+hs^HSfaOrWX7>YWQCNMCl0wm2qeH-CtKd7O-pl5zV=#v|#D$ z)bDqJJ2Q!aho1y^tLSp}uKLa+?J$XYnoK_~za5)u#&*+Uu9fb?fh5N&&o@-|-y{fw zlDbC;cYiO8)6K-UDsjfUSV&m|3{pERolcLfq~J zOI+eWHf*yCw}-m<$P_cU~-%Q=hoA2>LNArxOo&^SikBs zDtqjNR_xfNATAE5JBL}ZTCI`PNs^`70$l$KBT1}H=SX8$Z&X{9<#VVL$H=}L1bJNH zqog$i-K^&Or%N}RpIv;s=XkGU(t`6yV8bVpc<%V}#tz$ic!DO@Tfuhpi!ulAOurV} z%hk%}Xot;6Wcg+{d-_OT=mtG`=J-G)^#*7B&xl1ygdxPw3lGQy3*F}|GcD4q6f!KN zw1OnBxgU(y_@%vnkf5z`MffqTyZB?<&+L?aeC@G7?9o2uBxAj2bWu5KUjJJHKX1{e zx%mO+P}p|r3IP6+3X2bPUvzjKod}L}4c1=u_2=h5dyl{vK6eP4-)+`IJv8QbpDH~m zj8$!VXD~Q8QWEq6)pMd+j^c)})gS<8koL0EkW>7W7pByIbRD&B60a$Oqga!F1#P>-Z`)x^(a6;? zVq{diU$-6{ByX2m^qIDZ>uYE9Y4HZzat%uVmYt$~J+Zq0YG%D5Wo+Ms)IxIRS(J2Z zlYWG-{{hu^kaMme#bE^h)@`_LErQo%eukVQ*P?h&n;^RWo0tku8i9TvcgLK1;C^dZ6cu7OXbG|FJ2JaC3<0p}w{Xxi#I} z4?<{rUdexYOq07ds3>1%YASA`&tJnUxW}&e4*fjw*95J5il&YrnuEwzA0tC zzZlAYDOzCHwMz`mTt3~zdFsvpqn?uG`r)WIkmo(j&id&BKSjG{*tF}&^Jc@}l>KK@ zMVY{kH#cQ{q%A9ZhG({q#}LAV2f%l3?KHnED8INTB9~;yo0aT`wMQ>Ees4O-v;9$9 zsG_hL$$yTf`8{j=kCpBVp9d<2< z{%_h~C6QTs=D_Mv^CWFH!|~mrF3n%9!@r)YC{U_$bFD2WFON!VaND(_s_5m&-*iky znn_>K_M^-F59`d_+}!bBbP0c7=e2?=Tmk|DwH3Pm+c*i`1nd(-xK)J2ufG1DPu$F? z7XaF1JX&{K|Gi+Sasfs`I}9zw{x6I6AKeLzCh()iF=Wu8+Bvd({QKD|2*Ukdt7c$y z1?p2fxS;s$>hEuRugRo;05(_G&bHWO||7gGfyA&(_dpq0w7#J@)%s#@)$ zh{SyDR`BcU@1yJ|kN+jx|7?Xx_2=9XPZM_WMgA#Xe>qyG{>O~r0$x{bI`MHJ2$v`= zAjs*WAi)W*F99GfB>#^c^F;E0)XGr6_to$B_9APP^rsSn1DOI0jC%3Uq(Fgh2zcr}z&6|L4yu zB!7s>&3}uDGcU2~S=IbVHi@GgMc#i5sEo=VJ%6^9Ue5me$rS1US_R~#6tF}8k{6b& zK_?^$Kt@WzrfezyAGW>&7|#FwRs<1Ugd|E3Eh0$tPJ-yY_lQogh~7(-2!bGb^yqc< zwM2`$QCD9j>ROA%M!&D`?|bi^`}^OS9VRnL#(aF<^FGgc&N+|i|2&Y_{~kzm{gBlE z|3H?%Q~YzzVXttoow+frR;#P8x4HOxkkV`; z^szIPdbysz`I-^sZ4bgP{x8I%Ck(Z}%x(b8!EQ0&!ply}@xy(qLa$_+d%3zLs<0=V z_b#EAmUoV$(9#HNJD=nZ@kmWIdF5G=C( z9wHx0GhS^WJ=R~Pk#Iu?<&8`CP{BV~52K@iea1vl2^;*{>f~ELvkQ@<@~zd`qWNCD}(Dq zu-(M5veSw_cwN#wgLUG+egkF3Ovb?+aX|XR@%6kX+he1}zLE9c6W33i0mI_GrnB?e z$vGp7!BveuCH1pL1niX?>wkg3fBtw8|DT)jMV?De->6^QYuC_Wu@zicR5aiTH#0EG zjI$TNVUfq3B{;g36J>OP8?Cdq*G+s z9eF~C@ZDrc7xQ#1RVW`a10*)%wN?tkOqnSuHs#iUH+G&atf4gzV6^oB53S1L25^FZ zM#a%bnbH7B{s?e~A{sgq0WE;`jcZOsbnEZUbOySR z;0x~>T4cJzPh5W~09`ls&}AVnp; z7q~bDv5@_|nK4#kK-Z1)i+u7m=Q$B^{!*M+oUuxjbhZ`teBQuS(R_OJ$4Op=Q?+@0 zympg=xYCd0CoOGFLwnriY#zv+A1Bon*}ap|DXnqyikGQoY&=p*Cxxy_iw@8&-HiXIVZ~k`8OFV4%C8Db=ekMfe!8;GfQ(Nb1b-`Be(pFRHnFIVZ*u zLwTHQmD8)F%;K-hStqM_SAycBT3tlo`f<%Z6KP-F6S##v+U}#co_n`wKCqv>0+k~2 z>;Kr<{c8119x_u)fCOIG?;h80g5LyGpd zT$7EC)#RPopecuiX>)$RMVt4=wo=fO)E09+n5xU%61|4M(vNvfcmBq~s*xcZ*l1iw zi4@*!%aw25oA)D39-la<_eNe`wZ(GaJu3TzeN(u}u84h|@l_XEdP!fdR~_?m0#-|% z(^oSAMHt7iVOcDpf>{0KY&r4aL%mK#~+}hRREh>*eKj_mKSoARM~+t``(V@^5#vX zsppj5lHPm7An|g?K_4BT^PB+mSefp$;F64cB=o;^YhwU9A(YAmLi{f(H5AuICG)Gj zDNt_nb*Fr52V-}%61E*}h*&dgmcGn$%&7649#W4nz4@UAcb>AUX(`ozI%LrHIhmp>we535bLv0QeNT+7R zc&a}2`WSGvzrF5KB;T}V-C4vtv)-Q~CEyLXO?o1wvpdnM*vv^#*M1H+wl zl=b{_qw*eeUvtEq```2?r8X7?KZDwCS{~D*)7U!4lRIo1ddCrwrI14GugsB6`RaRZ zb1g1;Emi2`BhpxvVe#Xt0qqj4%zG0MD*fQ=#EZXsW=l$Jkz5!E<#r3_SSbZZVS|`% zeCx6nohg~A&01LXAGk*D3uxLj-e{5{>dB%-3>lv4{y8Mr3VqLlkI1ln@XayI=*&M) zt8Izxq}FPL_i!zEufc-#Zq_WOfvyO7_ra;!G|cF-y^@lSBZ?F<7u2E}-nx|P)bXOg zVrdM99wfQ;$9BE=Po1g9c~g_=`I4->7yn9OBr;#VV>3kLuYcTeyGa8F&Y?Xm${a%H zGxg%ipOSFB-s9I7X7?rEO?=5efb)Ph359?{&0K*A3185KPpL^$)y;^<<%_VPo-s#A z+uJVObg4kUn9IGzHiM1PV<^X3Wym=&uvc<%8Q_%$)Y-~_zx;BjW{Kf)EGWt^A=~o* zt!6@Rb5T@6ll2{Nu)FY%nv@Z;=QhVn{#sgz0RgkWcNUfW3~X?NR?1)8Zm#qrOD*6% z#P@c|%sY1N`X6@$cvat&% zQB^S**uGG|9b+LCR8@LmaA@l(@W&V{W^{Gb#4%j;PI++=K<+<79HS{=?eD$Q72zJj z%a81**EQT1QjAw-;ZdD9E?AxFL|Kj2;rchT;ehc>{9^mP=<0$$ihyhb$E6UiUjs6I zF#>`O71w`ujKemer9dP1p{v+Hj}lVIh=BK?i*!GU&BaOtl`oswBl3 zyJ7vSLfEx#?`*T_4N5t32qHe_cumYMv6kKQ?*+T_QKrFVQvU&oYqV;);A0>rV!R{J z|J(wQt=a%;#ry>B>kACbW^;5|qxia*4`YElt+Zn2fCV4rIMy)wWsB*JN?=rD0Ef;a zep^Gd=4gy?$axd=QY~J5Ph<>(Xm+z|Lw0&F%rN#*{;0xTf{l)!Z2?+D59`A}19hHA z{g#akr`^>ez~Pvb^e6ZFqU7?qD&SI20RfRPD6t3TK6xaMC1%K+zMO2A1YVmj?;`Fl z@7SfgNPh)nAm%}*>q#jo)Ps&iJQ#&@2IFUV{QuR`<@4M;djIkYW0alT7O6Ncr*EK} zY(O0-(Wa1dUE|wu;BfMCgO-9gz|_qbqvBG#Qv&oGepYtmv_w;b>2pDX3GJhckVP^D z763$0K<)7*+2yKJYROzYkNUPj;B~EU0G7yM`dEWqKt%bTW68|b<;56F-L45jI%oE- z)>&RcB%@!yoKhKLc+ai7T4%kf+Pq1R$m9dvQ`C7b+kw)6*AlLd(uaeuX}chxW)iRX zTj`NFpLwy+PR{7g-F}<)aG_zJ4qX@>1%B2@u$E0D z5JCs!z-YZ&d~*UBTiNoEza65^#T_#L-P!w5KzEz%ON)Ko+r_jU)5Y*A0_DI;#bPPX zjzIDqpORYzfA;J5YU&R?_}0@B7!$LRG)mz@i-nb^m*Wi+iC#HZL8tb>K)PI_YDJFp z&}km*kX|Zr8Br;**x(Vo4LTCluJ+{+=S4N8AEIK@<}5GAYMjYM$HbzapW9}=H#trg zhl$rXt+^(F10mw2&k%HpIdn}PmL!wWl@3z`V!J_#NX;*yvg9fN}PieKJSy$>{9;AOA`g+lBbK7qCLL>KU1Hv}uN=o!k4^*W* zhhxt7iTvSua&T(4(}P{cP5ggav(WH==&AD9NUA4JWBDH!K;5L{WhrRbyU(;NFdaSe*mq=#nS<_6?`^>_q|)G1}8P=c%c{XWYhOlVIc&Pg3-lY%fZ*@ zh#b+Y@Yc(_Q~?(HaD_&vU;!E0iW^1FO_ImJ=t&@_UV;gilHoh7nqEL?0kKVHSVX?l+ON{Lri8`AOMC{RZqIaUxwS{Do`9`kcQiUkdE zH_hlN12)#QJ3Np{?_3!!L=fN&_wps}kp@x+guazi5fKy1->yHCCn%xp^kn^i9Noad z{bd1&!VVYeJD`%8LHoR%PGdJg*2Xvcboc!aiASX2sf(+_ts{>8Dj2c5Nfyl-LbmGz z@w<@qD)?l@1S_YxV}JjrJq}6l>i5IL9&nZDN@hW;a4eT$!pW{+Aa`~BxdCeko$7u9 ze;iKLb;~r5o^oG2U`e;pXdIl!HRC>s?RVRY*rGFp!CDLp~oD*UOqT~wr$Vk(Z^*l zYr|p&tD@hRE!TGh&CLUP%O&5gNHA`h{`_PywF=n1bb?G8sZP~=QP01F^{XKOak)EZ zdtJkYcIo4sc#5W&g&vlL%9r31FFR5M_Q)bcW54=vbB7==-)O9plz(mak#bL^iH!=vNsC#2hps)6RLVMyVfLE`0X zb}D8~28eG#3VogE4{-8(Wc70s|M0ZCL>e48G-S!{%tE->#6VQLZktJ878oCygVLWrZ-eoqGt`> z4j^rI#Fwwk&rDi9&o3LDz^T*^ncN_?nY+6YrC&2e)Mo6f--K*vLwC&Ue}|wN=ig6F z0s`kxCq~$!DPORI=UjEi*`U5%;A#xV!cG~WJ#Lh4ZFe!~u9|DeM?tVlP6;r&PB8xn zIqCe4+hnzc#ayE^QZ@AyvpK)-0u8#{wOXg5qUsg(BBeqW%mX1ic)JVKLl2*Fhm@z- z0z9`m9N-ru9!Ui>^FO<>%K_R0rJ+=A=OPR5$Vb0%_MlhCym!~LBK)hspI(rLhQI1O zJ$x+^NXi!C59r^905!FX@cE6|d%Q>mwSVIX%K`@`afd>MwVvn|g_@pBJ zu_+rM4G&0H2%Jto$|p5)XcSj1ajJNql~IZeO?`?@Ni?G!vW#<5dEY7g8KX+!J(uwz zy@O|wbW#inR#*SJ!a%1q?-w%)Xt?}G%}?J=k)3)vrIPRuk=UCdvJI9xzok2|@6J7X;>V)ZTU%V`wfhUDRzhoZ`(>rVx2bCpSs023>N9PO$u#y zSthl3f*HJmwoXguS*PM{zXeriol4v3msYQss~B^F)zHkvuIKOZ7dE2Eo?`v5dlFfg z9W&njIUNJnzpiDa8znAFG$#D7{1WK7V>ZrUvnlM-5a~Ygd8tS#Fk*YWvmJ3P$!NmA zq}#plzT0!C1d8&O^fRbH)Ww!w_{K}xH7e)%!Zz&ezI_3{XJA8wReiPw(p0c{9n0t@ z-V3ww)X_tiL8+jN;r@*o2indI0oxx8@gF3iahZve9VMB|3$k8y1gv1h+|RqqLfext zebdS1LVeY&lU@9}Y9@q>La?sQuivvV1d2N6WWu?37DqM<3B;|2kDuvv z&I6MMHUJAVYq_bwihSee+zYSbWns*4@P=$*9>_~0%FDKo1IN|S^GHqUlB0UF^0pm+ zS!pPQdmLx4#=ql5io402np(iy=+eE`(KaI!O3!b1FCB)2(6z8V(XOi&Ohv7Se6aKu zy4OJ?ce5!z{zz&~e>-5oV)qD*XqBJ75~%IrTUYyCmQ%V>-_%n2D;r>&df>{b5Z~Do zNXJ;j>@mc!Ss1?fIYFC;%u#+iopLQM}jo(4vCS7%$aO^mHRA@Os z?ke@<&nL(Yw*Og}37@@Y$%55(58D|EV~mQOPoSrx>(?E!D}Hnf>O-BT6)vl zg4K(9HWw9gn)gbNbR3z>?3gLdv2If|G@asMecI%zqMBz<9DwqM+H~R68>$(0j3L}2gby54o?c0>i~`9fI~^P?SsWSn^@BH9U#isSw9Hzz zqeqvQZ{Tf!539%((cuLFdOX7`EOgbM>?Ru%P@Kg^4gz{)v9`|`*>ESj9N%IFAj`{Q z9gsJBFqQxrlpZYrfwo9g0h7LNzlqiaoWy3$pgf85-Fg>8pz~ADX9iq38cO8|vCsyV z1c~vvEeSh+G5VW2oo>WRNnPEG(YLz)Zy%j&wA9Zf2V;~$!aVQ@OnGcH)Qu|1kAmw1 z%_)Dx*dbSlP}((h~woUsjQ@adItMj~&3Cgc4?02r?A<%a17#{prMlom^KmXW! z97q_=IUck&iF>9ys*r$wIG)eQJI{`A9j+N_d@<$1ySrq%!_k-0vgb}U7wBY}QTe%? z*s*9~h}>1raoHek>`5c~o2S5c6VF=;B@?=M*G)UUye>J4`%U8m7N5cb1#; zTNbo->DNv)DCY^2l)r){D+KTINT24d#6U7ZFw1b;;{S{}I}Xsf1*UvUI3KnR*~O|-_OPFGO# zYBEy~nkP=F4~_tv-3#4nqkY&bQrYYM6X&4CF=apq5@33ywcM2vc|Z7(w0l#4W9%JBKZ#aW;JBa@*?$ZDeqgV|LYkTSpfm1|G10E< z`ITMjY0z)vqQgp#ft_R^Y5R%lpyC@TiTMt4APBO{sl=g3CGf(_NDXmrOsW|U>&mE- z-h^WRLJ~f-bVpvuwq~!kwV2QO@dJTy4H=a}q_kqMcp+EW&mz5wPxC?tZm1^(!5T@- zu@eetQAOvc3`o^;B+gh$(+w4bc)qbi)%?xWoA(&n6o1?@HS^xePL|^ma(w6pP^@em z-B|LD(GK99sq`8}F#02q;RDJekaPwLsQ9 z^+27KX)O5xZN;U{g_RfM665XbgI3Z#WCM(m!9g}+>2#eG`9R~yczW5{!FlMA(YkNQ z;OrwP3L!jFF`;_0VLNDtB};{frtj?l?dPw3|Bm~lZ+Fm3Fvf$x7P$wYpG9q|GY3Qc zu*B94OhMym(i`8njLqswqKOl!##Vv%FWcPv=H=d%6s!VmpquO{f0^`Mm<@Y9yeI+S-@Yi_jr zH;E%2?Oz9CicSM{=s(}ZW`G(8{*8hdyMHq>wSTN*bgrzZSaXXjaLQ{!P54w#chbQa zU!O6s%Hq$oG8l;9Yt(ZS{81We*`4AH+` z0N9ict8B}!#6pEh?>IeGGtt8b6$CzKQHcNQ0h5G3P8NRa%&~i&w_?L_pJ7LIOb>Ev z0R#hkUz4zCF5;B|QhqcT$W5K^o_bNWry#p$TzUbh>uYlBZ=5OF*MR5!SA|i%X$GKK z9mPF6t<>wy=B3;^0(jz4zq6tU#^8&wN8Fg>gcI=wRTA6(8tTdE-Ap$tf!BRHuSNs) zCfm)Sg^_f=23c9}*+YV#Id2vZOiXuQ~+jA^g4=`QGRRqEwDf zX3tKjyY+4{N9xRoi&D%I3J8r!=TB-9_~6I4Z?}t8Hk{IqmODM0bR@3$vK2^Q7x}s$ z5%jt8zT*QS>8uxDGlJdwUpBUW*Js-EI58W2lAq#PuckNd$1T=!KIS|Ugkc~1)H`?c zmg&`$gaF^nDw!&nyWgP5n^xrQTJ_y0-&P3GskS@Sv1VY3)JDL#PFFc3N)8hvk`Kht zn+cB-Z4flq-6{GkCj<^3EX38O)_TM^i;XSFJ@x`~o5TY1OQUxp+Hkw8jc*CF7e^k6 zZo25q9=%^oAUg}D((yktcV!wK! zN->{dM6S1R(fRMPCfbehWvc*fNpTHEjp~F_-fD;SHEFY>ujjw^XW2mvhHPRpLa&W9 zs$5flf2Obx{VH9ktza=ZnQK)6MUyx#HGv-<`Gg+vQRjnic!(cH2#z@+-gM>(i$gsb zFqMQff$3RT!wWrou$te#uE&DYE~tjFR*=d$WU=&D)zFUEO5+VtdX+PI+(3)1QiG;< zZW5h~=UX~zczXae3ixhr@Or#I*Z5AF#YQ^sGi#KgYk9G^}>a71!AU8U$*-R%~=wIx?8G1YKSGL&ujNdId;MU%a=CWTbO=fT}Eq;S=6LG z5&u{JzaMAnwJ$rGz#dXNFOS398tR+PIED+tRg?F+j~&||EH9bVt#p^MyyhucBg;Y6 zmT1_!?cHZ1;njs?-=nb`M;BdQKRK%M0-IkH1|{l*qf!_9vh_;DW9_2L`9Tyh%tEe9&q1Td zG&lCXdfKY0UsxdSXUjAA`m%`!p#<{X0E*uvAmL++&E)&8MhFm!(0jg&IEU58Ik5PrEpGU5G!T=F-2P#U{hE3(|EmvSNzGhd`yyeoy+y5-_JCgCB>w5!!GsgRI z;>%33t@i^-PfNO1XNw&7xWZr!Jx&*!8QQC39C^RIBp5 z3hFtHP5W~RZ5&wP-&{X+#Aj8YO{43Mx3c?F5B1&=_}ike&Y8=SUBBSH2X!}&2~rc_ z{3`Df=N7Y6v!@4U$X^EG{)|gBd;K6dqMmm{DF9CbznP-`osSJ6l7iw;!0#k&h#JZY zl{6Tm%#n#EHh6;$ ztp2QjRteqU5zzv=teD!iQXsT}-+cm}O97d7@|M&w!&u2u`q=?^ zk;aH%^lzmN^bvbo&QD7B%|QphC0lagdFfz3rG^*Ucm1?1qM6-zRQ;lD zm4$X#LGiNm2&fwSQ@3YNOmluY5rdc?hW(w1W@4@=4Z0Rjst#ab^`x?y?I*?l!IzdO zkr!5s3P?e3aeho;x_`^N^`IJ2T-^V)Mm(10{_DFyTAh!_oIU=iNPDa6MUNL@bIGXwXhfe4ZTIAw)S%JuapI8I zy#|Ktv^XD?n6lt$o8>#3b2 z-v z*eQ`+C`;-rcR4#Go;VNBRN3Uj6QZzx5&Gj`*bOdDxBj&S<+iSiI@`kE{GDPY4f>>! z%2ydwEDPG=TApk3v1jD|Do5*z=5zHu*vmR}0p`kR=Qg7UG5#!4o9y>sFFl@rTU|6= zyCt{)Dw;oXB+Rr6I{TiJARvB!$2HieI!}ip$0!>3k0)Vxkncgunr7t_94@$yES;p- z);d79RFPSdh4dF;h}-{MU3p@cH9dl`0W+S;9j3PF84$DFd5d=oR)ZL8Gk_(vAX=*& z(*9o2Wd{uG!*RtHGu_AB3dTI3BJXW~cr;jKbIBD}->uhBR|>&X1U^jTc==PBblZMW zCgv&6^a|}X3^qGrs5pr5%)0RCX;&g*zexqQ1_0w&^ZMC$+ARBE{N|ekvMedomFpY` zubJY@D<&Fc2_VDS_^T~m^Af$W*5d_B=C`tVT%8|%DuHk`v4p1FTek^*t@g%3h;GW- zhkYHId9PbD$R+keI`7W#?_wTS;RsK9pp5SDB+I;e@ zhyk$8A(WqIb%+=G72ueWz5t^{7`n-W%kbDd=+q{7;8;i^>Kt&AzI^%8?~$ib>j#I< z0O2`t0VWFe{|jyZyg_bWBD8=`EmPnXezP<5VPyoFLs;a7N=Pg&k)1MkHrKl6AZ@Ex zuI@mJ`6s&;?W^-(=d{bg+Ua<)(-ba!t+Z$gWdHj)UH}eLHeEQGL?>GDnm2h&lQ>Pt z1`h)VD_0iq2QsCKzQTg!L&WX+DMcS4@2TgJ5P@EWQr^XFB)tDllyG=i)4snFc2N;Y z?M3=hu*!mp&2+Iy_?(LVNhlQ6`1Zp!RK=#RA3NJNi4l!8NIugbnZ$r}>88q4X?6vX zl1oNWc{OT*HTU8jsOOtaWtkQPR*AB^VB~U2l@_^0W`V*my0YF*FZba2`uopIBIy{5 zljomMlDL{*c6jf;v!2LeHnxsV=|wJRu&p6Nes{O{1cl zl87`##hm)LH$Ng)2Ie7`X^(9mr*lB2jN|tfclF77oI$T6eiOU=dN@aCnI*;CKzFLG zrtrd*PdLD_#w)A$a4QYzI94`mTioa~SkZkyY?#`Lpog*}Qy}k8c(@mfr^c!wGkXLR z31u110~hFcQ~9kUJLz}4ZK7>d1OhZ$8dIm|CrK+K3Y2j+dy5X;adVS1$OE6AatV!* z6;QN4UaVItiEF?SmL2`693rWo1__*%Ly7HGYB&D&or}8m5PLG?FqYWc!B~r$T#>Bv znJKUH4qQ&q_A>I>6!6=4u$X6k`U5-Zajf89KZ8~$oOeTK%Glwwv&6lfsc0(sLB8|x zjM+V{ipx_xc$;;$&i?DN*&VpQDVIp~oBbGd2_MG4v5E_H2cp7>KJn>m-I$iXOY}4E zg)={DF|e91G~Can3O!Ff%@}=a=?9!}*I(WM)eTnV5z3Y43FYlDI<`=F0BcUv?82RP zfmC2xUCQ;rlw_a-MmFSxskY&eWV-aUPy}`1Pth#@Tp3ID6~Q%a9-$3QhXe;>C@A{- z`ozeZfku3M?@NwvJ~tp8tnIWE0k(&n)j+bz-~USIFA2Cf=ogFIJPkriPHFK32hFy5 zS4#l?anHb&O>+R)RO8w+}c zIm8%t=gb#KnFo8Ews>q70_!f`djB*E^Lkha?VwToLOaX z|NBFsggbU7><~r-LjSc5qdgPw{cRd>7c_;naWE2Sn;c9V<3u{GhxprVXC-kKsXWA> z6DPtva}u8!-SY)|F)M|=*1c#!{`kDmY%!NM%q(mY-^z|yO5U`_au%JYUZ6}Fs{iTY||pmXP&o1If$#$@JQv6SFTj3q?r9L}TwAJ*Sc&mh!&9UG`4WloIaF z>_c;Z?$mnhodb&~a{Vnm@l*{h^(L6(l?XW@i6H4%debkHD}7W@lyjdFxb3x^t6@)PWe*X>^0qI3IEcWtMQ{QBg^TPb9Guo< zDvR2APj>%Fzo>!axs zdd8lvX&>;+m5=S$Vbvo(*{Z0O<-eUq!DHOQofx|ej;jXzTq!$O$pt*N9gvz z_?uRQ=i|1cEF}qyUwWZOWAKDhfru55n!;)HqLb~=f~zT`>YxDX$v{edWs^v&f5H1x zr^2QC9v)7mUg%7}$D>ssA3;ioTQ2N`cPu`j9+06y=p&j&B)Wlo?_|e57Rww_TRLu+ zoW)iq&eIIq`}lT>Ffn}nJAU=fV`$-FxcDAQG}yKuH9G zknWyV99q~Tc{YlwE&9jw8yxMk+TzItvT_3^;tq-HeMaHkCSNqVEF2@2*dHgv3gJ7%<2^9 znJY~pKd$L;l%Ls3;P`6Fz^`Xfjg{j(r+!^)cd{R;zGcO~ShUPK9y=HksmT=hHn-OW znwGyqC-)TBRHW75{oaNb?l7AoUFPb-HSElAMId}os z)ufE*2=r{caVr24{B>|Nuxv&dAPd)*yTa#Q)!L8c9|D7rtp;F-;VxH^lMX40O?Wzy zHbX^L_2&Cc#_cJfmXj#IXZJ|9p?h{pWciJD2%Vjsl)j897D!*;t|X>;{sqv;$+eBW z!59J?wb<5&(%_yxdo7#RKpKaeqm%e!oe;V^K0&(^7SuNVirHH&dy)~#Swb5FKb%QhV^QQXPG2ju_t zpv)hnNoNXhPH+)6zcRjB~1B^S2I#g29RC6jaH2BPE_UE=o zBu=g|8IZco9;13?4HnK$$Pc{R1y4IY*Zf(yXK)utl@03y3fru>3}@mU?=p8XT%Vzo z18Nt!PIj;mZv67b$@v$^Nzk2jtq+n#*vk3L;=YQ2&^sl&5=30A93M{G$@Iw2G zQ1iscFJ)v%?wI05go6%9g|5v0ws}2kz#qvw1o~?h^NL~yJJh9Y#l%5(ieC!l@*Ejh zq=OB50Y^$FVdzeTFXtT6FBwJRMJM7nOr!sP1S>}1!Q&oT`*w!QLr_f+3?N{ zHtjh3_?lS8s}@X(s}D}KVKG&w?s({174@`C5!J?8 z;@&Y_m}Q&HuCkpj$9sNn*^fQ)MeRm7qrJD>d;WJ$un##y9fx`y1!?!o{S(xsHj!0z zb|`udC*aDTw=}s;Yt+5}Gp{b%HH>%f(cHQ}m+KN%Bj%~F8jI?1XfJu!|dPF zmDYzWogY11n|-?>jRBJVs5O;r=C+1iVY67lgU^rQ)wMaou!rK6>B2n24)uTZ@`osm z-xE@MmEq~(Xnx)GC1bP9B#i(*OAIfEes+-Irn8z&y3Y%bL zY*Lm#NJD2n)=7(`b>E`DwdrGGU1nf@^)kNN5csX;nH!uSaJ^2iHu~GgHJ>%1*9k!@ z8U7d0O1HXY$W(hmxku{atLKKdc&aut(L3b}O&(%4vJzKK?h$>*=tl=F>=Hw(e0RU# z+_fox|g!t!hp6le(PjwtYD7xK~=1m^+<*uvFwU!tU$K7_w>63G}PO3Qr!2j z&`au=eJiZMF_wl$Hf*dwVd!_BELG1fVVl9PhieJaWzZGZaa>En2?>VBwJ0F^Hc2&| z&*9)vmM;dVX}0j1$D~q$VZsdM(W~Z*e#YRL5(`-OY$JdoemS@cB0kNl1gt>Ewm?8i zYaeAC{$ZxZlFwy9r2Z+oqx?;j7OXP1jJsSVjmLdwlG82|`bfkKSk7lQnj^`TAaSUe zwOP>A4rEwNpLYt0E&H+bc>Z_kBZC_gBhxVFNo5!!u5}k=_7naVcrKc+#x?$KejlE9 zlQSh~GBa<{#Af9gX zVwWVl9rT-6IpjQHuAdOXKn8wT*7B-}@#rvD8d6A)F_pDhTyn~CfBC!+eg{+UH1#39 zL{7IcghMx{5$D0^2apiBo95%ATP{`i1o7Vq<3+MW?f){7v7bH}IrXE8{F8IB$Ka0S zm;L&7c)j7TKlYhB^U3F?3YUHXbQ=e_W{w8>gbEMtz$fdo;YW6T?7_P}y5&xCZ2TiF zCC^;Niy@49Inv+!(#*e6oTI8l3xf0!6-z8*uyCVKX4Z_r|m)ksjRl=k=2$bgKJvkqC5fkz2 zMXCGq%DjjS!r<55&6i6{6YI*3c+L3F5RbicUNYa(06s{xB#Ya-G@g`D=4*Vydgbl$wIcU>ku+6O8 zkPFt=wwnfp$TXV6U0MAT%SMHUO|=&_FKB* zG)GHu^Ypfo@&|uiFIcJHz1$ZcrDP!7H0c5}SoPe~MIRj4~{ptoyn! z-6P)=wH+MIs&1I{-Sr5}ZkaYqs*LUXRi}j(Q(s|suF(nJeZ81_S#^qPA(Ap{ThWap zdrAn9Mw_j5x%@!9?VCry&3)C6LVTz>7*6mf+9bCW*SvFY`L!HLsO#9AH?sY&sHb&x z$A))h)?P)u-KvM$%sMFNpI!^712SY9XdS7}M8dckpVTEtohg#5#Y;f1uiCBe=AJ1&*)1QELC0t-f!l34F1ZLRejWW1>ASsGOeScq3U%}SDa$NDzMDQ}Wg zeL~Xj=(iED=!t&w(MM8XS7kD=VOCZ3G(qy@8%v!aOtj6PCet2UohG|4?831 z`=9g@K%u{4GDfxDH~hL_4gO6Qy4o92L*jq)D4a?XE|P;JrDh{vp)(p zKGMUyGO#M}@|6tIoIA?pcql{{_LQ(kSl1wx77Rj2A1WBOPr3`%$OH zP%EXIvqA_0$S01tT)N`&T91$J`wUy|`+~=OICTe z(07WCyGXW0}=e(0oK}5=ewsV zn1xjq^*^lPDcH%bR!+=dIF`U`w$q?bC1b46#46nLOyJdlZ$QkkAM<`Wd7c@%hT{Yc zRA2raY{kZ*^{Dc>7X7EK-qN5I2S-oDFPE!a#c)($#zcb2+-{erwepL*i%!C~J*zTJfGnmp6B1IC+lbzG=U@??4AHcn~ zuuAVAD}!r#aXAyz$#b>*r^X|5L#qsmdJ8`^TsHBCFUVZ{03@#xP}#T!zaCsTX;!E8--xKlzDQa2C48VoxQN2o5rgQY1b@^93#zE=;^svD;_-y zYhxQ6p@BEwjdmUDDwLs4BitM>imlU%+Y9mY^?=AreJ-Zh6SO=T;{3NZl8e0otO8Go z(39JVdSJ(?KfuyS&!pFit=0qgxX;Rr!DU_wAJ4g00kLorHP;~J+wlLL1)#rm`mXoW z0HBns+B~@Jh5`y7@V9c^Qtk0N8;Nq^vDX$V=h%vy z77UO#022;qtjm_On3t_TTZsmqHFynI=#{6kbo6>@+%XN_WR=>aJ%9H_<{evzo!twq z#`i`8-(D7|XGjMJ^;6PgzxnF#E zBNMayd=+I~x4*-_p@!4(>ePpd2hSa{^cZ=UnAT4F$|TI`3a5fWl$kL#Du(T@a`ar11l1}rXZn$0EA2W z<$8hoOKM|DUN+Z2LtfeE+ar~vE^WQY)`-uUo0m_{g|5~C;^`v?UONDLE9!H5C}~eg zX1|Xzlt0yBKV~-YQ6Hj?-#m=YYg?&{-e2z|x@Ck(<4~^q>eI2h(9Mah@S%eKbht;> z_sE@Q+GbDxNoNb9xyQlpV*+Ee)C$hS9q^w@vKjnQ?$MX4uw3)W_&~o`mk}Q38+BM)jlk2D_QzgOLkP4-6%nghl)?dEqV#e3>10dfig^*BPqChp$-$YgR@M5IPPwR^fUyAHOEGU(kRCEL}~6;_1%o3O(R zZp5Jx_m}>5?u(!9HI#P~nR7zlXp-cyjiv}|1%dq$?hC9Vfi$`8B7LB$$_Asur{2GL z1)k`TYnTHZ(G<)xx{kSr4x@Y<#wwMjvNWf}baX_$UANJ8F$~I{C*D*axIpJ7PV`q@ zTv~6EvUbWFdoa8U0_bv`Fc0`0Ktok`|0!2ouJ>J>IK#HLDI2(0ub!Ql+<^=w1Nv`A z%E!0!-XgC&zJi47JFWryVZBC^c5=Te(wpw7Xsq4@us1Z=5PEx9NOg>PeR>ER%H3HR zgqZ(ip_2V!!;akHfxS56h6hbG`*H5W9|m+|-hj@5&Etx%HOQyL&rAE&>LAJ@^_{0_ zd_iBY@q^WMj(b>wPo#hx4526Pb$=|uG{{!!M zhhXuC!wiD&w1|yu5t&_;&Y!9X4!$yjSSQ?H2E+f%Plx%&$}`Lsdy~KkZcis;z~-FasE`)W4~WZ*$7_?TvO{$8@2z7w6}nYs$JiQVF2k?x>F<-q(Qnt5CxH; zLPU-IWKfLFh_jle?Yklim%jMb&W;T28=YH;e$8}xz zVt0~iz3cWC+`vPJDjJS~0~dAkz=2(+C!C~N^@_q$&`C&9_c!pK=HE<&PE%ZvF*$LZ97`mc4j_=&>4-uEM;~Ts$O-+}2goaloyG4YO z;Q#{B#%DtH?+}Ppprz8He3No2V~0UCu-c3(X<;kd;@RnME|)&1aW*0K?&T*ej*ai{ z4X!m+T#ivH7g0U^yo_M52{+f>sx4(HS7|-J%A8~=e9@odA0?lzrBx)z4ODNv? zSlpf|75=aqbT8BX!&ev*G}!XK$`D1$ zG)BOkb^}PhYxM8mudRCb=D4wNC7)pRZfH$e|Nb(2pGyLPcA`=3&(WiMuT{`b;f0R=qHcMex027bQ#=!%g|l7`6=#wsGaT>ossxGPa!TC>1;?lsaXhf zt9-M5CM6S*IPkzBeBf9jA_z;Q@-ty30Q;4*>fYRQ;u z_|a#U4J!L~bWhizE_!S7B;PpfO%U%cXBx!Ld)Zu3^I$aA@Xhm`e$kjRNBXVm#m@v+ zrN=OYRZ-;lrD3Wt+@UAa0g7JBn|>D;vt^deCjhH)S(g3{0g+^z8R<*?N&njrZmjB! zf+3x^x$6yb1NBJhsVQ`AeX^){d08kTh>bqziL+P^%_wfKu*bw!%epz2BbO;{B@M!2==HN&W<$->SNzN!JZJV>d|$YfrY)zi%sS-Cg%I5~ z*9}lX7sb9Pp`o8!vw#F#oUQphI8NdAih4W^yjr5=0~OBUkEq0+%kWmSJK0a!hyLiT z{az6sId$xH*$d(eT*1lURXn!U(HJX1TgFrH(=XqQ2yhCMoz(sL^svzZJ!H+TDq+>k zUkrWrr$XZ=QC+A##^F*oKBm5KG!ucXeqlx+&u)<+@oX1ahD<9SVfj zewvwAI0%Nqr*!4krmoEt7^ekozcmu!!vrv^;d{?XUz9k zx7YOiTZS1r6|)BMM0=CJ8`MX8WO{uf-)*Fx(ibWE>`Kx89)tv};GbYLGU)JLl%3yrKC%4`C_y=Yx28)2Sz_su#ivKHankCTt zHQby}Ufi3J8FQQO#7-x8X`oGyg={{V$K_@?;_ABXwvz7pMvM0yWBg>DWB&Nf(5J!Z zcOcLj$)2e+@9jn-6rZ&w6fnhw&Cy)|DgxZ>?Q*&CT@M;gWWfCm@v$~}$IXfA2d{ro zC>CIj7@M`vh}<+7mmF^zjDDBJ`?@VRoQ&=y$IJ5>T~Xm*l7N+Oo5~m-O3DLu?AfR2 zEeXaY?*>xc+icD9-^jRMSmaLv>RUrnU~mHa<8Cm&gl91S@%xA(+8Oi=eb^)3rv)XeeQ&|DHK4U-y`dF z5bPNIc@C3bQ})SOQ%J_=eZ}(b5^p-O=E)xvQ8H6%*LUJPm$cssaA{MIJ7DAZAQGiN zy{)tHPF*F}iYXp~#4TTWbwKJLeDC#4nxPT;JacO`M!uqpO{`6~^4@pE;R_-hm8+Ub z2sF0|c2!)Qc@;=D$M!f$<1KG04vmWXFU@)VFc{+iKZMshaEDaG$vm;cWKFFu*8&u? zAE$VTrjn`R!?4{LiP~W^Y-|U1rRf%o6Tfr{;(yj{PKo_`ZBAv;kf7NkH8!x^Q-|0Y z8oL#Th+ax?e&A_gSL|1SK3~Y+MXLQh5J=#s2eE}p4~oVKTvp`tPeMY3&}24T5QRoP** zrzQS=VC8C*7=^+db~0NWy2I-8RWK}v;e`xb&aUJzcB>iT4Ihm!6Fq5|)?zC5%{@3l!+7ib!6X2^)UPkYp4IpUOriiitq-L1jt%Wj%eIdILYT3C}x5 zo$0za^)9+S`lTNoDlRg~WK5~ku;(rYFD|dvv@eb*)2|hb*JQq}+sVz6xcyrtrA0?_>#o zqj#fgQs6nN?Y=G>F7hcc=~=C2Z#egK_wt zVO)`vrPBAK&TO1B)UR1RMi%BwNT1BsJd0}Q7Up6O;42tNUN1cpIa%aILGPa4g z3;C5kQn@NkSKz0ubr9d4Uo*}*%E=TtCA8MdqjjKN>kKxX{oSG)5ChNg|x?5YL5%dlgfnME#7au zcqnV<;w+4vPK*2&6(*W^X_8;VK!zB1W22ca2PMW|jIgkW0Y2Kkf55=@F7%Bj_Q?{ux~>=s zfw*e2iSl%ZV2^zAz50-^Z?LC&RwZfR-`!`nI>QV6>V8Qcqw{?>Ce?J&YBFzWKWy`F zfPZk(y;VbJCJj#!w0SWO<^<4)Eqms&)J9@gq1bbFS_x-do*(}Ni7>L$9J^`~okQ?r z42I0BG`q0G`@h6HduCrFyn#d^`pXxce=qrAJEU0Q@-J!0<+V)4v2&=$;b=jTk^w_!e zBN3ijj0d^#h@`!GY@4PfgPLtPo_<5GjkX*Hyc^Y3fEG4&GOdKWOFS#{Yw#jT8E5p) zN0E?}>zpobs*f$3839_pzeamcr0ZTcZ?kYX>gz7ijP>Xp38LAPqi258>d_7o+3k5( zG$Hh3Q=)5i12zz>UqV}BJTmD58eL^RGh()~xiLO<(P<(+Uz_R=*DI^Vs0U<)F+v4< z{oHU6b`)$Ax>8@n_qE}-ct@4zJNTif0Cok+WKU|w6Kva6dqri_B#tM$mWJk<5NuHD zYWj01_P21+b#ZeB3;p}A{alO%CcWMomD6sy);0-u&)td}ODk&#uXQAJHIWnkySFJX z(?oD~DqWSTCz?ZKp5YDK&}jB}c!r;&yxhSp*QF|uCW$IiN3m;e(HGNA;i;+x zWfc;miYl+d9BhPM^j^C0OhS++%lIo3DhV9sdU;207+eT~Nuit~#Bi$FfL)Z7XGvb~^(La>;NT-{w)4)Aq6h0!4YZQ`^ z5SkP3ntcP+plu46xqTb{?(f_D z|K99Cec>PJ&3GB#FVbQFmbuuLPAT8~Vop-_6IS2U1k)*u_e8BO^M2(Cip=Zg^g58gKt9UciY_2pX+G5l?CKu(n(E&y^SW@yw zMH&SvrGJ$Aahr6KaqydQLuN`Ic%-dG3GgA zi@Ux|=%payEXk$EOSrJv?WG*OIa!3Ro@sf^pWphE|0V+p9jr;eQfs~z3CBD}lD=WOQ?J}9NS ze(Ulr7`Gf7{z_-_q=*WFWL@T8XN;c63fj|8+Bd13UZCC0l%R3H3Rh1QUejZZ9lQVw*9M2<0~b4{sFu5+ z)wU#lp6r0{TBpd&9n`ZrRt^x_hd%3BG&W+tnGhM`Tsa=YF5y3)d`U~RyQ*?XDJ>l_ zsN3HC)#^#wg?7KheD+dDV@bA)R&4g$)pq-%SDT4t&a@jg)62}hc0n$-GTzZ8H0So`0+cV4*ajw%w@`g)pVt}0dAj~EYQFLJpo$Q2#v9$QozRt&eK6>mbFLQl>2GOf#H_b+k|ZeeP}n3`ts$=`Jwv-XE*%FW4`l;g}>}uqfK@>O{GJ@|W5Por_~c1kMu9bHRU~ z;V&7dDl93dbG)Z}a=!7lqub0mhAjD{$JL#=lgYATF>X*r$}G)c)62hR(vsAHE;=4o zCFXhtPZR!P@N!rn87sw{FZfIuZI>Gw$ao==CpS^EUzTa@L5l846eCmMXubVN`Yjxi zL^*KeW~j=zx$f}NcjaNR)ni)OiC^wRm$BMS$F>8G|!xsxjo8%))378d+4Ikuo|N%E2cD$2ZVoE zU~3de9`UQ+2J^|kdb^|so$|Qh#)9$Dm@P>FZ=2S5IHF%+Yk-Un^5J)2#)gKX#~1m2 z&M}C!4@N=1Am>51^=ym}bo_Tr2n&%2r2oxm(-t~BP7I@;1N&u^mVv&4xj7OS8_G7T z?x!mJAHMVlu+|>?21UG;QHG$xft3*)cfvh&jno(~bF)sKbMqBaKF zUv%h^D7isy&$5Y|Zu$S)ix@ z!`axC#m2kyKOM(k4*ad6|ES{NQsBTyf2J5qfJ-WU2!XW)W04P1i|kC{LZJ0C)8BPb zWByV1`M-+>p?>u`TLDUKPrk2Gcok)-WRDy)5VVAU!p#k2zRN+?b)x>$)Bht~uvPlo zEyMolmdUWCysU!Yqc1NP^v#(YKb;6Rv!rDH+bI~DZ*Dy`3xb?H^W~*SQowC zm6$YJ`pHw*f6dHrEvyBG*bW56vRn8oeU(kDQM?eO1sIH%j4XY#*V z+9_K?zBL^^wBMWk*T3>-E;joDXq5mPZx8+d@aHUw)#C&MJBRAj_nsDUSXQpQyZw8h z%gljetEkmV{DBEE^CG;&h1*Tfxz4SYyVUtj0S&s37V_(U`5T3v!t344j4nY58p3zO z0fsR~O)xWBK}MdkZyR-_T}z8s5*41~ka1BvlCQOX?SC=DKW`iy_{;rmg_d^y&ocho zhq2z@EQekA+qb*fGWfLF6U_P>Y)`S1pYXAG}9DoppcmF>b z!`}^~)fs?m@vBEAl;{}#gUS49K2US?yRA(Kjk|=Q|7OyEt{Q^__)rv$>N~lA>-7E; zL1B~>6?oD7${(hT|NQ9wdK`8ae26(bA^5*;2Czw5on;w-nT_JJ(qa9ti~sX3{@VeR zO$wEV7vsO%^j|OTKR$CTf8Zn;TVFo;|8$8*9|Z2$R)+NT>Hn~{KYukUfr=%U zz^a<}wRZHfZ4X76A&%4!>-gey|3z%-_s`vH|FTHXjvHeMc}(~D-&_9wWC(vhKkN`& z)64U)7u8A%l7W7={g9>oUns`@N~Bpp=0i@84!bkZkMdnNp7NcNxx~_CKJ9+dZ*FO1 zucO#e2~KZ6_WSie{$T7XHp}CTG(_#&E3WbYAtux?n5n6$U8n5I{e*vcv^Pts<@d`V zR*C_#K}jD*RBObGJ~nybA(9|Y@ITg97A230<4G=GL|QyR-adeUCIM-3FyFtrIY6NV z`$RzNog##hBOBhea_N*~*N4!@eo+Q|qSy&Eu!{_@(;S5C8LG$3Dl> z^hMt*7ITf~WM)Te33~JojDm?<7hL))1!_d}>;LLrnD508iR9IXcK+ zKKi$>LCqkbRu~T^>3#_sX`gWd#n}A; z*sbVkVQB=jMyg=R!D>GRUof zot6S7@!liK%2uD&%~gG`LFap6-{cPH78L;eVx(L)erS$Jt9!kU2>%yDVlli4>*+OIEgR5?I;?B;+ylL! z>3C%@m~yeQS8{L?Kq9%c^VD`fY`rj4M%}9RI(qK<7h6(lL??mz0g(iQDU@)-Bruwl z@Pu$2wJv_t6Gc4arsvO<0=uj*(6eS4Rmjmo4^}* z!ga<>0vh2g1L*(+jZ8ai9c#`znnvV+PpQFmdt{Kvh1zLAz;a;q^3{)cc`$JHlL(E7 zL#N#pz>TSvLhGCeVZQE7KZhX5+#Btfpwz=r%?wwh-Wz&818+lA6(8xSo6Pk9D1=HiUQ#$p&34OTttY~!! z0C~^rX_}&En9~M(DF0#r4E|UoStz5JzdDCt>p9_2Z(1U(8Z82*BSgBGD$v|OMpMbI zn+O+x87f^iO?s_-S~in4cbM-At>2e$+sVq>#WRy*YA<09Y9m_7(e(zjA?IfdTp5pi zn_b#&0A?&j>^D;@!4^>d5zo4Smx*zopQcuVI+LLu<2X5)!hdIk0PgWPQ_?^onuen* zhQ3xPxSF>XBJ*5|@MzMuLH*580VCyuSFoShyPLVn39rE3pZ{!92wcU@v|%gz`ws$- zfxRwH+O;Hc>({`O%z34-rU8_%?sPfTdD}g$5{xn#&>$IVEc3ufN_k82(Ek<36V-45 z%Eg0nG>6~w!Ey$=I??FujDA+)?nPA&XD8C);vF0T?tk(94E>}^DGxr~h@TP;19>|W z35W5k%X8<{w$8Vt&gb#9fl}0j3JRP$#mR!^eshos>b{X3FiHNmyO7vfLpcC?YYFCu_@?H4ckWLKNE%>tFX!)vBKe_g7o#UZS?z1XlYw2-!_rmtfloB1PMs zt98o^C#r`hGswA~GZ=XvK2wT{neJ{AO*EDK=BA1^$3fGeTMGSc{Y^0&F#3i;BPfti z@dhZLI%q9?zNh2DxBfxEa^4HJmqbVJd3L-_AIc2yknUoRP*>>RY^wfPT;Vtvp`_5Up=etdtx;LTI@ghbX;}vej))~j2YgyjD zF$hTu{`52_l+~y%B>tYaj)72^-D4VL;o2jpAcB%w<#hQXo<)^M@%V~4BIL~H-7tKcTlqbA zi?xxUyx=b?n*K90gwczln(a?N)2@C5I_$Q)OJoAJ2FH_}rbW&f&4JfFoYb$cSU$6R z6Zd~#Dz zacwvl+U)$pZCt>$;4xmE%22FBPevcT2dsy@%=7q}Z-|b?Ve#VKd54%?J$tK;Lgw>V zSA@O#R@wZXB~5nOvcaBzzFVYLCV^l-qedu9_SQ>=Dt`*CVm(6X9{yhOoUgU7(anT#uGQ5xU3ITqXY+LH-M3yvK>PiZBm>3_PQb|b9AM1 z1J9&jYupSf9zZseHjX^{9Bi@?<$Sf2>~%8*>&pEcZuC=zs$dF=P1b3M4gK*nHL^tn zEouK|RsvvjmbeBUWdOQ+*Es70H6{T~0fuhD!i{PWOiB*Z7JY)~XCenFJH3weOtV2P zrn}1+VbaIZ_PTiVQb!07RM_tV-bUq_2sNoP!HmY8;e}xQ;@c^fOKrUT75v=+ zL{PTdkuYH(wrqey##Qjyf92KMP~qXlTu10s(XIR$fJB+Lp(Se<5e!okb6U;97wfA- zL;d(L8IM-u{P`=hAQIs6B?^Gw+nP z-=CY>JZ(bh@{1PCf1Ek8vIOMkGZDBo#&1o}mQ(ExTzXA+YBuzmYXzW+fAtnpkdJ@7 z4)5RijJdTi5-bJ3n`sv-Xs9&0{X71IG!_)n3utn&H&Mim&}xaT#Z#fE^HpbYuM-$| zo?H%%Tp}#!t75#SCwar>;+_oL*$c>OR|R5)>ovNS+dfsvlx%WIm*;gw-}-7yo#=Fn z{*kcGPKJ zcxi>D^>O)ypwd*g;kpW%la@4WHUGw6$IbVoi5 z{<_k8hvz#rT^tn2k-KoNniwYZ$@+tk8erbm4tdQ=0A5Ad#dOXG{Rt&*pi9B-KWo>M zX!q)L6*cajpmg^Sy~Ym$8%Iqn%4=I>HrpHDx1t`LHbqBc)jGEns+{OaRo>dSP|SGX z{HbkWW^IbVz5P6N%wm+|$+;>ZkyZJcf7C_>Nb}ycFIZowtlZmE+n+XN)y|lI;nlEM zdzFzI_@XphuEzc6h_2bi2HX7C!-#c?`v<=r9CjOjCSlC4Cl_(2RWKq9FFvcj`dzFh z$4@lzuBjv<(dsIfZ0f9NV$NXQY$EjdM;%aiXPrAE!j_Wr>C)d%%5hN;sW(rE^jzq6 zUeLRp@gvyQ@)NX{w>!+xIoBnhuqlYZesLs$MT3Wgd-%W5Y`1F3l3c_%+Jn);c5JB-}oy6k`G}wl^%ej{w7; za{|AQz;wbh_5|=SVH4VsD5H|Mt5mb5aB?V9j_|ERS8vmH_mhRxqyi}0@3Sl(;1T-d zThqxlEnv%m=k%if%za0J_1O!e-CteMyJgSGqI=0^*P_-iZLEX|unHKc9-!{tmV<+s zBmvBmdt6IR8Fwem0q9eS%>emH^NN%7TlZGJk{b=#2gEXibNr|*}GE1V|Xri z-tt=t#fK=BfMT}AfT*_s#HSrS2I5Gsj1?~6VSWils@8H~+Vq0B=s|>GK8#Y)9Ccsq zNDZCG?b`>eQ08e`*aO1bCmJTACS#Ve5`yB|aa~@XUw*cWM`P#gsb`3fj|aK(Du^GD ze;)di^NK(lr|~rgwpNY98H2XOobt zIiYTi?oQ%d-{#H8!T%tzdeN_&@@>}c7Ac|RN!HbCf3xI}F<-)D0X~P*`G{H3BW*vc z0(+IedJUl1F`9-#erA-S z?0fk=xi7`k2~t@KA_9J?A!vAmqm|~5g*0!-ypSiwa;606iC9>C{dYrSh~Oi0eK`KF zW#r(_a9(5@WDnufzGU{WX9|Q zzT^%%POf4O8>FeYKM*OGdu-NyC>YF)fAsS#(d*(B zcHj2li60tjIre#dyC~ppQ}+PYarJlV(pzxE#Y4K(J@Nd0>z$&$JxbQB^`g&w*}66& z-cBvrw0O)jb4_0zv`I$0@=VXKlFG}bwye9qhHyIn>iM9kF|b8E?h)?<99m&Y?xDxu$tK&8fmHky!0A2j~up4EskcJ^Di3e1}{ zAYl~s&%$t7V1YzfLnDNEFn(_!uww?GU-czaz0bt=cc5+ir0oh}WOEZX6Xg=YRYcm^ zXlR{b7NOsc9|YyY1dv|!eCzDMJBad8__M5qthg|smgXmuZYP4gch#Injpc)oU8#Ii z4T=y=w8b$Zo;5=9fpD|TK}0K-+p3od1gdUMP)RVT8H2yFRNh55DBn7UhFCmddux{j z$||*2A%QjaX;%A&d$(t8Cu`IjnoK3s_#>0adtq3wF6BW+hHhJqCEOWMSc^hBScrCh zeeA7?Z%!P(I0n}&sDCo~Yo^tcK+omE{EPuwB{fN-K?|RlooAS(?0+p* z!$Boj8!!VqxQ6C_GBnG_T!J+ERNND0=ckQ!wM(|qXC{feaEk=X|HhMqarUDGQGnF$ zYKDid=?7!1oUszq;B53a%!OjqI2Ml{_NPJmiVdjUrD9wAwKG2YxQ<%p0vS?c*j=$f zIQo$|T6|PeXc!sX7D4T|c{o;6Xw`x$%Z$4Z-`z^b@wwPlY{Hg}{n-B#T@upPMjjPG zZ zy~XL>ef|npgEq}sHiqfou;T&N;lAUS&Oh|?3TpD5b?HfK=uEH6e zW$VRH;=<_0>WpCa7#Xkx?HM@#I!Ei*3_!b-UdHzlL}-xyp@H&7{sTY++gy)(40ZzO z=EqH|MpjjUb%3g2;Wo_3Lhn#s{Jy3BVEt#;;v(a*2DfrNTS9$|cd3D&)-DIy*1OB= zdP2$+3xZXuVkLs5T;xeb!d|yM{m-Je_6F)3E5dTKM1kxh@TJn`C&h><6(V1$E4Lqq z;&`>Cw`W6Cqfh@V7+ethDc!Q6v#6DkqKSx}bICG&pfDocfF=4sHa5cR(d9wHg7Fv_ zWh;>#n@;!}Y``}5mwj2%0X3NVei3HHwP-Kt_|e5giDk2_ZV>V3GTyTgtX(bziGZ=u zXb1+51^FYVKEuk_QMRFgs=_!VJ^29na?_82lAs^I*4NqZ(thaGG0X5->@43qm4 z923!|_z6?V=I5w?Pb0Uk1P+v^ZF7{r`yDr@#fo$*2L3}MfhuklfQ|K( zaqj>Ri@cu$)n5ajh@Huru_RxZ{P=E0ltoVH7;*0tW;%SK%;~~IQlLZBokwexDriHc z2?&r}{cR9}(z=A6mvt8FWBfLv(pYgxecic>kg8Fs)n?lZo-Y~8+^8*aojYpkd%f4G!=*I#?;Yu1 zUf_5xB_0vR6 z@}2{bzCq1Jh227Ch36);iQBCH*OT1Wo<9r=DsmCA>iEvSPz@R+qy7~Y7$)lb$<|xx zUahC{sNMXeie?>iX;y_rN%WIVUodY8CKK$}4}{+0;8IDGtTxL?2g##Inu(ehBO2$3 z&zx_L0Nb6PkAh7#3VEm!*Hz^xMM(g(OPt>N5iQl7(_M6X55`y^b34Fx3QJDTj$MlPJ`v{DvZ2SX`}Rzs`z!8>^Te-HRopbL z$DbYHX1mWhoE%j*6VvX=y0nb?Rpy|l0bvny4Vw6;zo7*5l`TF{`i3SeUkGv|m{YlV zFRIFostey$7a9@jT0AVjs3midrb2^#pOGfU-jMMi_zP_6fF6wYS*IxX`h~gnp;^1) zPEo-Y{pkL6X0Y$7qZe%Q-X?>ma=GEdEA2X>^{Y9w6{6$2FZVCzChqI<%FX2?ax#_t z!_|i~cz(czUVQJDNNw7BXfx6{uw4E7&7}H7`Pud(Xs-^7#yh6!dONXIbGb9?(}RcS zGkISt9Eb6i2)veZ^nZVgQR=Qrsdf{Hsy~|bORtvF8>VC(S&@kQEa&{Tt}ZFJ*HEef zr(@})MLl9@-zw2Vl7`eVJ)~W*T*BP8p5twmyJ;So9lh4^`H7JJR}oA%I`aBYN%by> z0u7Rf0=m8C7J1@dpMDT@m~SOGUFm(q(o$d3?~l@jtHryuvcIBXIci@^Tj0z4^v#q( z*+k>4kh8Dd@cx_w4ofTkG9~%yH2+~QnYMI`;|`Dixzk1psftpw7Hr$(O#lHr1pObH zTT&2)SWaCYMZX2r;{~RnX%;N-A$TC2eVea+& zYz*Z558ez-VI^K-oA>(!m<)PjK(A*R!gh2CYI3-H@IFPKUGK+OC58db9AStgoVz`R z2ncMq;dVHC;^J}8<#Y~XE&9*?j6~eQkrnoM002nLM8zoNP=>Qm5eQO|v0hG!>uuZ| z5np(Uuzhw$?WCK8;!9Zw@|Y-&X~#NfQiuuZC0coy&<8}_l=~?m+r(A1)Hzt6aU{)U z=PbzM!Ot5jcbbR~10(8JJkeKZ6d1;?Kpr8w4b}Hs=XfH2Ij5nYr$*_ITXDHn5rekt{g9t?3^2bc%dY*^25dR8` zzDbuB(qfLekwNNN1bMR9S66{2P5UROmAyy*85<`eb)FzV^?#Dn} zJ|yuEEdX*e=DzQkA1!3isj6qk0;Y#o%cD#_HXhyz<;Rs2&ALxi%IW3br0JV%6s zN)hrl)D0h^3evpKeCRG2s9IZK(~2@d(?Y{bG7|=(L<@6E2?Sg+yj7fpuR~W78IDwB zt${v~fFstD_`<+f+%x3MCgR!l^lHmv6P1G0N&Y&+>GGFFzfQj$9tYIjVrZ4{+$)em zu7IMR-^x+YiyE-(XeDE3?%q>brKU!9Y~?C&oA%Ery4VDqHC{R@Lhb^Hy0%l96u9EO zYIkV{s>8?}&+9n*Hvl<3FRFm&U7v!QKImz#O2jLv8Y71P^Cj@im zs(GY&GZ4A&KIo-$L4?+^9~Gza)~SAVRyb^AzgPY2g4c1Mi#p(ngC1FekbYS0``(R# zqK0GOm0DtNO1F)t5{LJ8MC9h^(`W;-wt4t2)bbi3J2_zZjhEs3W!&`S1PuYFEi zi!&6S>$FF%3VFVJel%kh(6ZfEX8B9v|7fbWMk!`J`J^(?;A-bRO9YeoPN@dOEWT3K zdw_zh?Usv+k3NMJdNT;Bm}q*V7l2^F=GoZ!u~3W)BZV{t>J|{OM^B%ZK{std4TtbX)i&Ertr>1nj1vpl!8GiXlqVice#LYbil1 zRX#=ck)W5;be`B&GSN~|1_#!LW;1xAQQ@$pa{o2+(N$AVXh%N}`$r5oA4&ikH)Olt z$mjIybW=vC@$GCYPbUfB!EEH@#9MZK{Dg;{UhQ&GSho_Vztl(*{wUwmCVFv;v$guv zQ>>6zJ+RH+FX%ll9TEvYBT}erF3FQWU5>P?!654V&}& zA$1@w9EO8{_^&vPBrLgSe$;FCKipCl_yWSianK!D5XP1A3US!TNM5DHlco*9mVK;F zdV7|NB#n*5A85M>v{I}P9bhT+8jD5tk*&|-z4SQGo};Iy+>q+Ehn|n+zXjqTD$6~o z=42foLiD?UcQ)LS#`M2M9wxyUB+pFhaj(p^IK3}H=pc`RpOXAGGb6Tc95&iL;V6~FPXJjAf1vRodC0l|5!dF_PX2xW>!5!U6WHuu`fK>%9v6^*iewKbLP`LqN*)M5Bf$ALp_3(`wJ@zJU9}aiy83 zIW)+MCo7?7+;Vl~h|GkUdfli6x;Jadn%ZG8`p2nxa^EQrc^I~kO)k+Sh1`AfJYn~T z(e5I)qQ;1*XS?A(-xyeHBv@{QmSrr9jULYNvqqlZuIx5D@hO~WJ0zyi_Mg(H#hpEz5xn2?+&dEa9GWB*zph<9#w~J|IW$Fw>moJe z8>Z%=47>>9pu{lZrZtOEedg9-&f&*dR;{@a--bY7dPufaawyu+ISbplu=@T}<6$E< zY_0-&nQhANy8)@@0vK69HvQa zJUjkz@R)vJ|09x=D57@rk^YKUm3NeN8o%7;LS(%!)w(ByT1Wi^>I^xN`IJg3Fxh#Q z=AU3!`Cegd!QlR~te(Uvr$h|BQ(D2yS7-0uvxg=CbqE>uG6bL2%8ll1krKkYB1>pJxY_y4H$81Uj<*F`Q zDT(}#xT>T<;RWlNggQ+-Ao1h~b>p-4-6El;UdMtl`t7l$)lWd6P9$AQ&sQ~l7QYiu zeWcl+@i{NC#g!4Gny6I=Uk=OSW2vicbr*tgI1PnRh|q+9T=q~$d^E1rS)G9bwN&CB zjd4~JS%5PZi+#(S{+)I~?JvPqZgiC9R_U6TV>V4L=PvM5wYt|IVu8|q?M@WLBxc9_ zXw!;{K(T>KUn<-#s14_-I_0;LJExK7U&cc?XnOl?qrdciGPFPKf(7S%c1M7#v_n+$ z(G-EXBR(Oi$5jSw%bRiMV7L@3pE|Z6Q1+YGCun9Du$g#jRO@oODivsTdo-ICE%sA~Mn}S`Wx!jR#JxQN256E`j%taLEnvLLv6_P&)6+jt>y1uld_aCV2+U92yT! zpaM5z>DHl0c;FBkOE!80xbVfnp9^BR6N4%pcjtLNf>CsXCKF6)aCu6cB)pjc{hP9xx!32B+UI?Qq^mPT(6-j5Pe%FC`4+mt1G@9O+45 zdbkz4cHCS@OQhH`q12E})gNyKKUJSFA<;;H71B;$zOE|>Dy=!$n`u$7RMd3r%Twce z>{eM#W$V+FXgQ&Guf{`z-G6ZFn}}{2Ap^8=^DD6m(Fc_XJH~c-H=OJbhcv7y`$_RP zB)3BP>72n?c2)le4h`Kg$tNyiB2}hQ88OR*NbhyYqZBf8kFDchyJ!`jJv7{3FmOo~ ztg&h^|}QHmJd^2jNzI0{@8XMLUgc6bY{;+^ARN7 zc_GPcX6GpK4sm*<%o>}KUAusJ__aD#z+I(D0~a>zGug7AKOLGsX$kNGN!ojI=_85s zYY(&d6@PbW`!zT&IW3`I_}?gLkG5j{D4*qM4SwEuxX#b=te%UfZN1f;2K8a*FF3)V zSx-Ln4f{{zHgxMlo^`Kho3>wsXBSy?^!AKQgo{>8xJ|uk%D0#|?)7DUcBp3=@KY^mSQAcRJLHe&qRx~+}-fA@EP!=tvavyVU&I4 zt)jU_B12689NB62Op)BM(dzuh{@Xi;4L+>yKLuk!Q=z-KtUjny@ctun7~#C(fz$m^ z7}@c{c!FZ0tOIusHH~=Y#~KvyU;`i6%~n>}5>CXHcosyht2mbMgAm9!S7=y-h0;l8!iS0$X!>u((OEXI!fO%mh@bU zS!W|0pHt-v$?diP<57x-{S;9TsiIg53d)V2MzUTF7u!J!%TGx-al1rIL|PSzj6t$huZdxwIa;}&xxyQtqC-c%@{WP% zv4wIx!a}Iuyqq5)+!4q9lk4^@$y9QrqH;)!MNX99UPQFWGR3xQ)oOqq#W##0WoOF_ ztSUjsC$CE^>$=SOz^^MRvr*-yzCOD8F5_mn$9#rNic6P`1e}NqwD-%0#6ko8S5j-J)vU+>dy>zLfNEx8jeJe!2iU*m$3h2gS`r zomTtqeo=Xd4j+k;o?%A~7ar}dcEEt^7@i+(UhHL%&v_+#G#SQP&gb(@ix_U+>D%_@!es2 zqVewLNk(;P?t|`hMX|Ru09ft07^jG%WmX)?cdz|Mgal*Ss-*fIxqX68tUn1Tmbyf6 z*CeNkyH_y3u>VjkfVxHFh*)x=78p>J^ge9+iTrsCUG;HpJuMwn5Igz)P7dCAmVW4T zTgwIec)rA3A?|fqM5pz2c&D|#e85y@&gT!$BMynTNQMXS;^w!sBq-vz-@?Whc6=K? zjjWQ%>w67O9UrIT1}IGC{Y3b>SlOs5j>l|$%c*xOw}Xm@Xu_pmotfTFKjb;y+L=TS z-SrmER$#IQb7jarg}c}GkgJ`VUQCyMjj8QNwJ3lRvVHcxd+%l3bF$Y~n5b9vC~%!`9eC!ibRkqc znc&0)FA+Z*RAVq_GWk6{!yneG0s<8BUhvqO(Fe3-PZ8N?m%4@0i?Rfy2d!ANKPT^|a_y8F%D?Tg=-ir|oMN4K^ z4;$JO1Y42vsIT4RWwJAP(d+n!Nh^%jn6I2KWu1MYyz;Q4w8=VC4vzFEoT; z=(l;g`xw9O*l*>Mb2X@%waA8vW{ct$DZ?Tp^&slq6KZ?pk%X#WWN}iAqX_0)=G%rr zZ~)NkKxjavk$YDizSn{?+ZBf={!1N{W+q}23xllfj(&?z=Qe)V>Y-np$k3Em=+ZCL zIq!k(9`HKhI@Tm9*;E^d0%eZQ`UK~LB9&B=rv-hEup&J>2^?M->)w{H?G(CwMldywr)I|4Jd zOLAHV7!jsWdni*__iHW`A+iH|-P?tLbvdD282o98wG-V`z-FACtN0E1vtM8a*|$TC zJx&xnCS*97_P(-5v6}%J9c0gxs)|N`R1HDqo^xeFp?QYh+x)cQiJq|YBd5=YtzlWu zMmMadz84g4Ff8@AFsE=DH2X+pX{p-WuelQxx3JTXc+tx z(QupYXP?IPwPT6}1x2kGb^K;Tx|;s3?3`%n(7Dh0WKHeNN@>BRPf%R8npZ*i+d`>) zw=3bW&0cfa#Tm6ZzxI56T2gG1#%vAWmvh;9ERbzoVu1D`!GCsDt}E_YfQSSG!%Use^N5$z;3SPnpj%yM_4>`r{8f z$Dt+8xjRt71}|c~Di20O8%O^OEVx9`knvgNtTW}2GnNLap*0iNdt5;Qdh>A?I`-U} z{CE(2RH6IaXj`I7>*k!{#y!_wqE-Qfc<=VjeeSrL#_($fGe$3@DX*ADPJby$7go;N zeTbuxunL16i(GFvvu>NvTCh*Ed==izB$t7MsygIw4#&W+r!F1{G({~qW7IrA<|v=VO*6< z?k>ge4*Uy=YGuaL`HlBIm!fUwVJCfyIm7Dy-XlW>b!fBWm76vCz!J^)0D>>LY3+!` zROK6ZvooaeuVUVTkQkV|RUc(j)xct5r3z& zhSMq-RAN74SAE=0kQk4vr%e;$D|JN_ARzlOp+tf>W)6T0*mu1TVK;caNYRE_yBP7< z%i6dpR9(L3xJT?UaTG87^{m^(@eDZ1e6cO0`OLHW@b29kxWv*$r(dsFlCtHpHe1~H z#HXnw;a;k+|on1!(ArJTa*LsOi9`;C?OiB zHt7~EWMmBR-WFE7-&~#!O@|pMU4Y19GsQt*-t$^Wd5pu?=0r59W&hGyx z^FLKO9mW`B5CVcLSpE8dmQYUfjyFeZ@vTfynB!0ssfpoz9$*Mkgm`+qBwN0|wje`T zxb0IGwElZ--^o;QsfbZ4nBza=kfkH`zBab=Glr*tSp#)P@;{CcK@`rEAc)v~6<&2=Nn zEgN>~mFw31&?b=(389jajKI6-oU`8$;SvBNf7k}dBt_(u?a+qzSGnWJn0y~>xN_Dd z=5TjFnX#K#!Bt(9J*MBXR3iy{-Wt0+r~V}051C7wBzVf(@_R4>+cA`?Oq0Id z2i_;(_fwcq>!v&M0WEo*AMy6-UBQ7%oEkxz`i}ayGxM#luAXQ_hucS`qD??{&r^pQ zQ|E66mHRo_#qw^QLpzUB*@us-NseGz~ zxNjz<@p9_0*uo`fyp_yo7rHES&4JY>M$Men`70R zdiurnpdZt(iV{~+Z0xI7gG;2Tja}d#S@mOLsQE?x^NhFEdW?j^zL(=JJ8_Y|dQeQI zN{e9|HFzgL+C`_xwVGDS8;+c{TB3%;Et|(7oyF6)C5-%jZ^*xa#bo!lRMG76Rhc4U z0S7~s{)b!jd=}+wxyQ*+<1W=;u7>OW6LkAwejC))XEW)TSsTaJ-C{W|@j=*;Z0p4{ zL)APGe0gr^^#|sr<+H)Vlp6sGNp92gCxWnpTKk2Vg3|yNLgchgo*I|{{f}1wBgUyn zOBD1JkE|(r%?5DbPry5Sa>mkfjBe17`QyU!{)3TAZvdWMKXAW!9N4AnVZs2l;MUn{ zHW!w~BQWvBMZKkM&x2nqSH5(6^rjTP#FunrLk_%=vpLe3t}%8F49=dKl3|u6 zF?*l|#^dTj@D0pFJjbUT=+ZM2nVAy@q9VO6O$8G1opLOZZxVGA$n2`EBwHn7JLzPE zLm^j7H?B-wMLZh0C?#BwFU%jEira)7{L1@iZGoWP#8XIj)(4_33y-32Se{d(y|2S( z6{^Cfe%Icx3~jC2)@SB*itIq##n6s@i_lfQtAUB-5|8;4@YKoK?lyFzyy+AGOP#p zR80(j^p!aQvL`j%0?Ne-RzGX75}dmezmu+xG+YQsV=!@Bb9okqrlO9Fbmr5tV*|C2qhoE-><|qBkOE#PqF%9F4 zl;WxowPSR~84_`{9;OfI8kI&FPc5saw?-Z>S;as4771gq`$!Icu*mkyHWlVfqwU3# z5#s8;aH`1Hk0YC4D*XUyXc@KcShmK6Aa#H*gjAU5D*+Y>!TWSOC^Q3Ocb7B zDXVyTZ6QGovs)D>6C34dymsH2DG_(8&KxYF+j+!1t{CAza}gwh$mwx&Uy>6;W;JZE z7JT0e_s69tcfSaqosHu9!hGKmhfJdujS}KmjF- z(ff1gsNB2f$6Di1={aI*_&kzcKYrEm}6bOH}e5PA9q-U2AGa8J@ z)zHg6@f`B{eQ+pu8usr`Z8z(v@Y)wX5-nMj6@ z1EaS7SY+9*sF@9FI~gw%vM;=9WV!!!Tdl@jK}Bea z-(BAIQ@Q7VWc8=SfWIGgZ&gBaCfnt1 z$U+bi0zl)Fak>M3q5(rFi7GbLu}+aTWjN=v(4agj(Nkm~PXEtDP2FGlh@1vs>=KLc z{qicXYM4VGk2GlVv#aKU_@e(3jsG1s`-H%*MEn?dIggrw@fkqu8{ArE6@o@$p`SKX z)=t||#(1DiCPQacTaTKXS(t2FWXp~bJJFUP%r?t=?pP^}&e_s6+Yg(gpXBvko{L1J zU~*QLT#rC>rhE6f=2o*B&dDxxUlt}ZHf)jPMHulY-W5L#-KEMYE0IXmW_|oqyPlN{ zea(aKORZka>?9XXyJ?B+_9hBtk<(Q&U%Og-Q*g1Dc#ntdF=1wW#a5VFe*Mi4O0T&c ziDZMoZz?FUo;dNV&>PrN2>$eJr0~*MzYwaf*l*Mqz8Vf>%d%)uN743e@6n^b)8#yl zGbkf<5F4rApLC1+cqZzX5oY4O$~Gh2@+*6!xVcbpNReTII|R*RtPkakwZz>v{%O$p z+nAu6SPXoe%Vr(yIE4*LGe8N??#5p6q1#`yC%MBg#aAq1;rlNIcfQeAMcnA;qm1ph z4K*-(ENE+MRkzoJbFFUu8vRGwe#uMvt423-u>B!8YtAMH{{B+=1Py7XpogHfTv0>H zWY)Pj+3`p3m1)!2LfQt0=P=YAE>>v;W(+=veXT&lwd;r4i;C@>?S{v;f({olbp&aw zjTr+#c*GHpMha#cDwViGzCsP&#S3&Q+)EVLUKO}dks4j?>-IYuD(i+So-;Cx1O`z&uNkyQ8XV~L9?)^j^d)Cr$p?WzN` z{jk(eEVg0JB_3@eZ0*Hg>VynEsOSw3B)SUwllBeJPQzXz~*`*$wt4{dnaGQj39t zbF;mt?BHbDrsUbE!n8Qhva0v{{AhitTs+Pb&-`?3(WJbkTZXxtgXK-A#Cz68C!D+R zQ|xeQ<$ui!$6MRYQZ{icc)r<4lmjz>?6IXU_1ob655-!j|HdN1X@%FvXvCBo*GlPfAP#B zEWI0uDm88;)LK337YDB~+K;OF-CK42U{?zC#g?!&aDcW_wH7xk*Dl_N4RF{xeE zb0Mw1-1l+^J@mU7n>b|Tdu0C7Vt*VQ5LohHAl`m2a2`pf-za}_66OG&BE%!jhr}So zP9}5dT&0vPzO6;(arWHNoe7g93~YUdCVPyY!BWId|JSg*BMOUi4kYA5@8pyeoLt;^lF3BAnS5~>Z@ zu(WL+$;qM605nTloz=Y1B4^)ZlfUyv5q=GVqr87SI0sw^(Z{n3G`LPNd2{Qa=E@jYpf1`SC>z|ta5 zsfsaS}HRbF`ig zj3o9@Ov`!M+Ds~$&Fzf|a^Kngo!QmhUPgg8QV=ANqW)S?f)JUEhMg*M1u>dznl}o~ z>`HVl?VyX*zZZ(KyqIbeM2u+BtbbASoTv9eyp2V_TgGU{&}2|EQ*1v1>;GL4`&_xg z#P%Ei&ze3++&)I6`_X)MMEBY2qrg;|N#7-ktH&hh6N2;`pldfq)+*B$Q6eO7{W2|Q zv^L6dUJjUcihwrB4KV)Ld|qI;Q3X9E4E^ZU((|B}r34x^mWka9J`!_V2K(0THePiv zgOQx@H!Rg7{+PMXXU={!X7A_KZkNpFVpYta?^a%i4werncwDf;N5z)6V7rYuJ-r0`cPpUJsZxDWJF*$l_`!j{}sL3^|tybop z>vDJZj9iiI6LrSVMsv}1o*%sr!#A&ZFDx{a@*6~^@kIR8Q01>jYm#$pc8=w)x9>Jc z|1@aaHKGD%m5Hd%Jt9={`<>rotXF~}Z@eRMwd7kpx{?0S9Cos>xdR0096r_Ir(zW; zXvy;J^)a>W4&5-?f6R2&&3xw*~+Px4IIR9a5Vw-_!H7K?bM&T@V>! zMQM}IYO#8(P3^Hk{uKM<4ZJ6Qm!|-Zlr4CZE)#zTw`x4W!=6_0f)CC$C}plT?n##X}d_3sxEZH1n=PFp;kPe32xz(h9cK-=M{)qxL6 zoa=$j1fSmkO8dG-cgKZBL$L7yH1rJiIxb;kqhK5$uPu;yzWlE`SrAA!3N2Z7Rlz8P z0c^~{<u3* zU6qgB!N8bF2LzYKSsN?Rf_pAL!B>*~5-K~20c9p{(T3!3PtsQaj1?EAB>hThvV-X) zJWTq2?m~4fmLc;nzS-IHPn4MB5Q^YkVT3US)k7PV%}5iT|Z`A^1{oBx!%@qA6}c_?&KpLC;4W$6ZKkgh#RO%bYAlL{`m6aPC*NZbJh1of#WS?*@yNku@PA^Ah>d%Zwbm5o-6t@$ z`J~z!CAM8ay3)UtODh;AVuy}_2+lbG0wG^HkZIJ(ixd6BYpS@kv#l9EjVHf_R#PYB zrZ}{M`l>2_09^WC>9NEvGF;V&&D_Q33)TH=2f4JIXQ;hR87FPRte+7Z<1TQ7aC=Nj zoH%Ez**dOygtvXRIldIS-%d)~xU@q(DUytqeBcNB?RMA6@p+r+__sW#9lQODX8SlZ zO|F+co_nnx^HAv{bN%=S8Ra&)mOWwhS~DgNddvAPd#iR(BXFLnPH}k6stv77e?uR~ zQP0DxD(R0|Jh1QSehW;wroxTcf@ejmv#q53SWZ__YesBU$3EF*FuFy7q2NAQ8I!3A z+Uql4yTc{apb3WlPOJV+NBqUFUfSzbV1~I*jL_;ydm36bp{mtw)C8}vaVWp)cY5q* zN5FKmH(MRE+J_OzPQER*^c$}7FI>53-3%%UK$90mo~S8# zE{{SF|C_G&vVZ^;3_4DD!C!+Ug;a$gEg!tPi*X8KHTKmct|}xv;z8dFLz^D*2;~;E z63a=SHqq0nDVdp})`nWC9h=Zk#^m^gF85k-=9h>PE8?yog!YrNL3==H%Oh${ZKj&t zt``!4H{wmN%4A2vP$uQOC6OZs`cggTRgf<1W{s{e_w)W;X`*C(XNBCChV8QHeB1$z z&pWVOAvm4U7wi`bZ6Lmr(3e^Y49f~?(AQ09di=-n30B5!vlS05l3Q(t%*Gw-)#`c# zBNGnP4BXztGXqY)&ut~R0}&lMX1(&hm3X7mxDESqv4Muz6-Gu&I@2s>%hlobLH!89H?ay>YM6?kDMFD zO@=G0#3n0uOO=hinZwY|4#Y@n!b`-*Eq(hMq%Z0aKO5a`9B$;QC!KBhznv6V(ZZ{< zs21nmZs~G0m<7+VAQr+1tievL(ZV<9DEljen8@e#g3=ciW``Vb_{^J<7t|P5MC6c! z3!mZio#1p;)VBpYgA3`SI`h~&4Es}DcACUur((22DZ*j6o`>0jM?J!JbuSnb#Kj5E zu0@<<2} zH3H=_XW2I2kXP#xfX%H&rDmUihFluUvz=V}`D1BComez{bJu2^BIl)4{4b`0$^ZlJ z5ne(gLu52|Wqor}lnT@*UOrI+eOG!t=ecUPC{-+r=NVk#SzRET{z}#kn;2DF)`4PZa8o0kXn%=Ryo_?#@UmWFgrdP!AFW*}iBfy*#DL@(|y58gH zPglq^%{)Y>FcLG!u7aGwe1!ZO~dLOoT=@KO1Mp0ixb5Q+Yb^{CD&j_Zg?Y5Aoc>)=>-c|ebPB?=O!o9koABT3_qs9#1ZMe>v(zDVq zmB@b5KkKoA-)CqvejnNij&-3?%|2+ju$~TjS6Zj7b11t?tf6{?8nYO3lPIIBIn;}ZOWmBwXFcBLBObG7V+9Ijmd$UBI-gJt~Qn>I~t*#hE5{g!fD>u&Nd*%`7JYS z-w(V!@L&iTvmQUsk&!QbV)%LK`_R>)9qo{?m0sWf=a`{`zp7zSI@Fah*u1d(jLlLi zlcwOGE-QN|^U({$*i=NLl=zYoBg|G1QA*DmFa~{m-_`MWOiLu|aZw**lbxO1%L|5o zB$SV(KDOU?+{FB(3&A+Uegj6Xl*s5sNL~?I_Fy2+#U5FwV)YtgU@3S&&l=>Fr^O>g zy}rZ}ig@dddRddsjJvQ`vi!Y>W|u6f?=Rx~Ti#iOa4nimYBDfWhk|~whE?E z^(;f4n1c-{Oj2<##F`S_3}5{8wb2$Zse2ZnuG80i2-u|p$D+|kSV!A46Gc_i*1kpr zbH0GLXWteEwnxC7icD@6MV@jq|KRHW`*yiK{y2Rr0RfFf77TJMdH;0uBu%?S;3K=l z&{rZ%7&TbzjlNvf9pe{gT^+aD@M0&O3qj4|uc!8 z;MTsYNMazYiC%d^-Up!J3sJf7f-NIKj_wObAHsn$FdArl>RP>e2Vc&Mtlaz>$Yh^e z3NGXszk6{P!$j|H*rR9vJ|S>UMFPuWRyfo5F%!UDCj=2+Re|U@NdEHR|2X&><`05D zTo~zqsFR}${eHBRU!*61E>y33vj1XJ29Ott{*QP)>*GE|Vd{J>n?tOtk*a6o^s9=g zgSM(Z8n12mKY&W z|0%52U<1#CGE;y!pc|W&v(gVMD6aVL; z|M&O)+b3W7(XkzERP>L-CBYa;N!`z%kGW7}LuxMi1+aS|DFAHuCJJY1jk!7)G7a$2P6FvAFG#tLf>uhKL6AI$Fl$H zs|Q2?3k2O5iOPRH=kz^5f-@aX+~OJ$^6+L6a$2q5-WCZzHR?5{C_0S{t^#xu#qZRL{)j*A}K!WB}8Z) z`hV?Z;OG4Lk~GAqJlIt9Pe)-{iTp43tEu);GiKTgoekZ-o(j8C>c7fTnvN?#_sor{ zsn0v8BJ)3&`RjUr9Y_|i+)AL{KLM@UFeYf49s@G?0lUIxqCeP=!_(-X;;NJOkZC?B zRT-o!f?gcES5moKa`IQX=#LF;PWtDiG7aRkkN=lP2LwR6Le`sigFx_OXU0){Ky4<; zH%V8!^zz6l*)~kGV?fkRF#!IHM`2Dj`jy}Ru}=bSf4Z*$wxizFx6jo7%l_ae-a`? zM)^7Z`}(58K)?MhfsZUUKZD@q4;vqm0#|7Ug?SZpRdbx8eP4%gQv&$JWwn24Vs)`U zAGBV*>6Z_GUn~DQ)+PT_R_T9}mGno0TKAZ>>OVANm(uH%-qaXN_`mdI01z7{8UbT5 zHmJA~|Cb#ycpq5J8hTu7fW=N`_~Z^O#0o}?2;Z3xF-g;pFpd=@H5@k&;a_j9%U>^! zaGP0Dkdbn&n>ymgX3h&Z7c{_6Y`cAh=oXaK!B?)afLT^m34YxD9k%=sEL* zceVa49_s%QPv%nI^M8rt?{9VK{}fN=-^C+|>Q8d57}W%SHNvpl_OQ63!LIzfC-9M< z;Op3~4X}h-r*dHby5+yWwIuoD_LY`gh#!G<@GafozW^quZGovbL~_ELpQJ*6-dyea zxlJnaD}Zz#1dKcHZUJR|J-@5tS)jIn0J!t!t?>sjaM+R8NBI(GS2fE?dI-P~pRhhD zs&Q}25hjXD>?%|c7!W;#--64yRx=dzxU6Q_523!J<&!3nH2@z@$1QKo&f$QB8hguh zCY)QKe6pckljGv?b_VdT2!QCpN3UmYFaP1p^lpElPesE&Hu)QR^@8~cLZrFSBVDck zt|dOIfWIc@RvCV*A_42v?`0TluT57Lh@7wG!j5k)S0!eFl8snxT8@{@M-_svtKO>- zkBiqd4L!~LsA#;m%5vCtfN_w1s7<4r>An=6R%+M5r$sB19B|~jogY1%hZl$QH`fzl zdrfyzQ5-m(rF%4DUU2vA>6-ne&RVCXj^M}*_n)SnF*^vCf#dCl&Ei_BoaI3PagW3o zJ5sOiepCPjl}u_G!IrZ;SID^##YqI!-l!dWpa|eEw7-;a{}L70Ws+sP&OfJAl5=K2C;YKfU>NOAz+X&GqgLFLuMN?AW6! zi~lJ*rd!$daW>8R9QMru%o@q?8(^>{Dwml28`SgQ4GO@@vJ~g1-WIOef%%;FQ)bC- zKE41d=XyC~;CcUj*^%O(8{d78xC-G}jw?vU$7TS#Sv95T+tQ16F5iXQHg*pHJVPpA zOg^!D(8IM`q9)7j2Uyc@6OT5Rx*nNDDKnqTrXBCh&TLms7&c@{TrNBaKL1hG*_{mx z&za@zVPCp&bS--TPzke>pD{SVG)rKMRPPw@zXHujGh52pW7bOAY@HdVwWH6K&f+Ek z8fK^4dB~4X&~z<71?#g}@Pc18T%T{2epeoWZabR$0+84pfOw6)h9h_GHku?caMciW z+j!~cGbK>)(A!XhWR-nlNsbowdkzpvi>KAG?a%cu_IZh#KT3Ayym#G1R~5l4>rN?$0QOjqiYJFZ@f1=CvydEHrL%gS#YLRghi9V%Rczjabc-41X z4+^d#TsUqcQaifR4o+=u18#Pcv+?pE2PWCPA;ZEJ>KpYWcXtaY-@BC59Vi+nvSaAq z`#zYj{%!l`W^u8@d_#CUK^ZB}AwU3}2^SSFFS^=^+HGuz{L8)q2a^2QqZ=rGs!6QJ#z90H}NBc+H()h`bJf|PW@!}6ewz#?8DC>6A`CTs2 z;xktS3k-#bZa=k+O!0^nJ8}H{?EW?YB6ks-_bIOh`sLEM4(up+`0?pt9<&%` zNaz%%*_C-&^*H!l=v9wt>P;24uwaY;9LjxSfHDd8Txbg>HC($}yHI!fM1%PHxWupB zBKhj-7X9{26O;}N!BL*YkKJ zTx0%8_<03=3x^rB@dCALq-_WE*X(4t^t1VJS-e~NwdR$TCr9J?#`?U-x%>Nyuzsih zz$CCj*6r)|JFH1ce((}BX~6c|NiV#?5W5|}f7{OktXsQiXp`nwXw^+*+st$j+T7% zT)(`4$xBN^#_bT=~C@aNQU#u2=XpZNo}(Kgd30;s+rjXK08E;_5uJ_h79Lv28S}calZntmB!wmuwG7v9M=3}|Pm-dS zkrxuIB>kv5sVygd@K2XCKoRR!@2cE|e?H(wr{_X`W_wB%c^WHU2^HiqoF?FS#XwjL zf+uE#OjnVB2(W-2!SjYk8AvQDPwds{rYSQ9w{^?Yi}@QvOTvD4qC}S>fIfIM7HuWZ z7XW5*LMO?uyn%@e-Yka3OWDkhZG0b6Z64&0h@lWmB{JikaPX{SE1o#)!J-nCa=Pu0 zaHEhHdkHU8tr(MT0Dox%pw<`L#Wj}r5XB3S`?Ppc@s;NO z(2MPccMNxpSv#&mV(6l-B%ul{ppGA+$7j)|Hv|kzg}gu!vB+Gn5D%IHAXm|D5?Ox% z%|DrHv@)F}g*CLM&^~<8y{4A0PAknvQGETchdmnV0cv_*;p+!-r1;IwxU1WcNcKgM z8sFmrBOwTHdLFi;lR4kJEDNj?0F@>Zlplt@j+g;fI_$Sgk{P%>1Ou_>DI)|G26&Z` zB8T!!X5jqROrp(Nv z^zpvqj*qxh%lplkDih;W|B50QfE`yfCRi)sxyxJ|AyaL-=aGTlVBn)b))2H~%+L9$orj`rgtKH>R)nE%ys=cI^j9gpcaJ!}gB%CbacPFGh02$ckC$M|Unf2J zh6sN^st?M#%zljXMMSl_eiU zHFM$S&~-m%$G+F>Pk>=Dd9I1yV4Kx=dbIDm+UM-7<)W9|F z0a;+S1WjdP0Pnb83lD=HBt$;tCwRrAg7n`0)iF-N5f^a-_UHBU#wVQr`QQUwF)Ght1~1S# zRbLVzJ3Oe-AuS??@UqWqY5R8FXPoHvLJ1Z9TM&PSaBwMf#Zewfg_G-otbhVj>gxmV z=2O0VDgATODnCl}s30O$N2t< zX?)x-?YZ;Ij%JCBsIXUkLtQzy2CS0FGRfoJkN~bl7Iy4e43QhMDPT^g(*rbSoRF+e zkm0UcW9`$l4tHpAdTPSrG{H4o@}Ch;}|!$>n#*>T4UVdtg2Q~cEK#G0Em;9@aUb?9jDhk3FjwG4wcp=} zh;AWt5jN?ge;Tmor{hir(!#R0!hEJjbV#s%Jj$v>DDJNgwC%K>oR*xjn@jiN@l7B} z88$c?K#=vcY>I3OUxIR=0X&~zJ;oF36SGR%Z%ooq$m^tF@o%CRiE z7ll_E0XT@^=UzI4=`du!v@I1*kP((6c;gok=6_eCJGzsf81o>D4%CiIn|=0O=K(Fp zJJrkdQ}*pSD>KJzC<@_nrFBml`M8Y?*En_RGAhB3t8VWdss6Ny&o^M|X+BtPsgSqU@qXDt=)VJbD%2l1nAyPDtel^H_BJP|N3GS!^rI2Si zh|rFvhvUs(JKwpM(vl!r#~+~jR$d+Q)n%_+z0P`*`uy; zi4qtxR;aLK&h~9MioOFjj4wKHbYsJb~sM2Ri7&va~n&y5;x9K#u#MlasB?@+x?5zbN zMOck1)uiQFDT3R?f@}eVNs>EMcI)2X#>WfhW-6gWI$-GH%bppUQ|(T?xOY;r=Nu;2{nIB|B8)x!4}`uR`Fn= zDEiBT8P%Y7!0gz$fp-f)b?1e)t%**zNxd`eORUTyDXgc}Ld&d^-Rv(3l|N%U0zGmb zz^D$1M&?ce3mwEW9`@A_LOe^G!Sg*2kw5S>=-C%>-^o(V8)rg!bnyL>Sjr$9pm#c1 zcAxk}BGHmHu9G4NLs@#C^?Z*5@e*ub53(ef_d6wHG#$@vF*U?#Z{yd!Oo|CE-c+fMP!nn{)m7fhIBGOhh)GAE-;e~5zBQ+B zf)ytOzk52)uwir2}F6y)>eUq_3b_@g~gK7zkzZsu9?atN1hDfX)UWd7nQ?>9c zc?PKQ_mQ!-NK$CWEyO&1wBJNx{8Fnw0D#MkMs^ov<79uK7R<_Z@^jOj)ma^p#o%`G zc`HtoIFaRNWfp8#qB#TaD9rxVc1Yy`V2RKIL-&UG9tfQDs@gCd-lm&O*a4W@-*cId z{zx|gsruz*e9VIAUW=ETn6y4foy8`mu^1uT4`@sNznN$?a-yNix?IE=Z&WT4UuLa@ z10@6Rof->z1B2)1V%;)6kyxayPu*Ja1mDWtU;oZcw9MOA6RTa2kQ?9zcIA0Q!5~8& zBa~w`9PUqll(z*Brl$IYCP=(OSP2SX$z*`eI}+17bC~HjhW%2eN~;#bQg-TU4s=zD zs#D{oVj$Tj&4St;?D%Onglh}o=Vv%aEI~_ZcYXqB2v1a|d(&VJ&0Z!xXAHWTb0H7a z$6Mx!OV`r}##zxXAM7?4nqs`z>463~(a}3I+mSCXtts+uuw;^kppmj7^foctTp}4 zA`k@iBt05Qa=L0ESS=GOumhT;N|3_%0|BC$Z3z~uxU2Uwo20P>%b(sq5nJc$*-dbC z1V@G4Ge+f<+V0Rp%|$$Rwk$$dy$!`}LsL@v3GbOZ*58_~zrP}gZu6>dA6r)^c$A9n zYGQT09cUvH4d10x4pix&Aj*fVQ2^-~)GVEe3}et&;$}-xURq!Lxv726GCc9cK?xby zXQDpKlHVTna4V6BbDMIa7fUFgvtaNuykBX&Pf2k9mPP!N?3Feyh6NI%qrUZ>0dn)p zJnkp@JGgL;5t09`o7J$Nq=chAifON3($ z6GeI0%)pCx@etlfbQyTE)EX3kt-4<_%=NA;$~!~s&KlR7`N>LqO=^}qm9D$^LZHKIJ9a|jt?z`1QwT8}xMO16H~ zs23|ka{gWiUnJdKCmhTXI?LrcIV>C_ALP-9bBr;_U*Uchn2T?~qyOQi3!NqUQ{xEo zReG4jkqB|ECZJ#|aOdm7t$~>fzVm())ec70fK1PQg=4UMic7*YCR^k* zMDx_COxDP_ciTFs=90}{pE{PS2L5_4fcr{xvE=hmr>km;3-%CWe-*bit16lFPgkr` zPQXuppU`f30Dmzz-N*l9&TCT$@&Q@M-AR=rnW1Yt&~*Y29+GR7ua0W!5DHzu`W#62 zWIEto%;|{k(s-^IAs^r`$=KXIfMZW?Y74XB;WD z1;i|cxeS+TyOc5i6thQho~X zSO#j#kF{x;3LqcP!?rF8jIyAZXf#;0+Pr9a=eI@M8RUYe;kxEC5HHKOHCk>B?ML7Y z59>~QS!E|CgiDDKH$$9#0b5wdmwa)Tx`gadNULbc|AIX3DYvlulEha>St5lyIGA7n zetkxz22Tq{38$0Z;0|OzZ+rm^aa$K$6};c?xKl$$jVBH8?#ofNdB-^!$VP!KvwcHD!NAN8*gx*ju&2?T8;>gsV=A0@0 zU~kdz$s|Ef_Rb1FXG`w-GR}F<#=eH;Yt~a3Jm3Q0cB0ev)8{Wc;;!&pLlth&Ng_;^ zS`3nwOqupDuc(<-J9ekOy)0%mP|3r9B$wz--%x)|-TIgG z^9^zhTd>m7_ChcX3m!w8RIW1B8|d+tFivv5%8aF<3p1fpTQ`30hc9*mKF`r1rBUg2 zYNOx`0_HD~C8WM;HV@cf1eM0n9ro{baE_5tsh?iB7Bn$^CP|vvc7b6r-uStxPM1jO z_WgK(JYks`Mq-1r{joK8DblIX4((YT`%Yk3V6u)mIhVGXP^LO`uQ#qol&UdGo6C$! zf=bmmpCS%y`$RZrn!8W?xhK$vt}rtxdXj4MOL&@{H->}jg#12he6gH#vvfyuSr<=7 z3s%l2G}1UxRpX%jj5|}da?Gpwsy zzdulCM))ulA2Y=iAxN;7K0*q9(%BVrCocZ9^}wAl6wn_F!gPvHG~C1I^m?CC&*hh2 zs(w2B1PoDpxw=c95$K-f*f+#XmX-=YsKpc7uZ(ZmyV{?#WG?*k84}#a`_7|Zsy|jK z0Zs0UB`x!WwEu^?w~VSX+`5JVQBq;k-HmjIw7{l8q@}y01*98kknRo%2@wJ5k`6^m zQo0+Y-fMf}(es_>9q-?73(%RpffRH6#Qs7U^MOh8>$o+NcFYz$L@uZ~g5H2sry%-;;*mkHu`V_c zR4G%PrNSlm1w*GJ!}@GG_)r`YqG3^!9dgP@vzFZ)If__dHcGCI9(R~1MAc5OM5ai< zi=Uv~tj|7Z$+-(d&f|ozz$|xwii8KA_1<9W+6202qM1H8x8|r>#+g)AR{}p1j<$J4 z?izTk;);^KF-}12y6AAPTzpw;5sV_@4R3Z`YuVwk^#(sX*rdUZ48Ag)uhR)x+SH#C zIY=C@w&Ae3dNB0yN=8ohGN|#qshZh%4cObLsRV}$6W`NJg)I)`O@y;CtyU6xTOV#8 zXklC47q{k#YAnTWX9tqJ1-cDZ4GeE>#P>@&{>luG+3H`EnoTNqdYXQs<_BK=zX~>; zJF*pNEPP%nfIdYQ%#WX&>t6lPeiNGXcZx~-`<7|M>+ZMsu2>DkL(#3@5d2 zk-#t@g3M7EbEzZ9h9Ap;A;lr;RM9Jo2EFcRIXm)k?~>VSGyzB6D12LEWVyb_YT}J~ z7#pZhf?iva6uqud)CisJ?WZKt-wPEoIP_wGr4chf=FJ1fyEh+@I2bb-wW`D3@!!~T zi=M|`^j*K-GCrO5#Je9%%s-5q=VI~=Z!u{C&*oXF@;G&{YL|Al5<7``#!K_%6e$$jY_6Fr0qS2o%uH1nXRQzHY&pCdiHu4|Ckbc&tGxvcEp*l;)eGOIejH>kv|w<0G=L$*_8S-#J`nTuMcIFMofUSM!$gn3 zx~xkuKCK$PdYLKl)oT3EQQxt09F&Xs$cAPBQhi|LyFoMWbG3U@d6-@*K&AS&Q*rw* zij@W~Ge}$S;Js@6|r-;{|J5Y_g|6l>LgJV9lh5&(Qz7XVFvWyCw zFM#;jKb&qEWqbX_FF+xDEOlX$isC@Hp(M@wcr!EM(#V|d_kz*_>q!h3y&y(+!z31t zPf2FsX^tD>AfHaTH1;Bu3XiyTLko&SYHyp}b&UB1+2P7ElBB!5H@Zk$Z&1#+pz3;` zi`1w5Z#M9<1x_j49!6b}d)ud=umbZ%=~$$!WLt8+0IsReWWEIAT<=wI` z>#f4bo0=Wx>$4oTcRJD`TmwCQr*aEi*?=Ljn+q+WRBG3%oJ=(v#>5GO$e13h-TEGiDbBLv>`r_xyHIRzp6J%!V zNf3+>^)iSe{i1XUJia0}Z1-;46h8&d7JDTIW~%H%n`K1Z^)2rNlvGJ ziiUl=R03*j-g+@)Pf+C@aWIXNAU)=h@99x#Hp4N|Lpyj7U%FUob#=Q3DfjbQkSR&4 zNfc@e?4mriIb>ik`s9)o>xoP~Zc1?YZIZcIWb%h`1Pdy4mt%A=b=N!EHkOYplV?sM z5F6^87oecz?Q(ys4Ks%-k0+Nq)103G0>yUh&M4488fFu0;h!Y9%6J&ORDfd!-G#E;lh3^ zq(}}L@b;}fH(u)GTPWGdUd4~H(uLVP><>Sy6Ah_6CXOkQPu04Jd?a*Al*D_J{ZXOo zv(&wIhxriWgZFrQl2Xk*VSXo@#o3-n)lbEgw$NE*{34zS?;T)lE2a(q5(?h9o-~5A z(ub@%lTsaYEBNW6koSwj@@2Gqq$@&@9{nBm2Z`w(BOUn!W|vJhJx@Ui!uff~J^cVe zjyr9HX}}R1fBr1e(iNRRM!_^M6V~1rhJe*uZP5qPc_<6=97!4;idXrVhx<0aGjQz2 z4VL|E`oKtqKhSol828Z}FMQ>tJ44nP{nH<4NF3>KeEUAjok?&XyV}1RLrSm_=7i+I z;yavPN=L~bsHs5FhKQ1ZzTQ!9I`3?c52&5UYuhi4xM!q^Z%$|<9B|<6G~g?D@xO7% zk~x}M8qIFTwBd(5m&75Sd}iB7ajF%-TkAE7UgSWkRPZ7#A7 zdWQz8-MlLgeEGxc`^9aiILl**6H$7q3X4?bgcA}P=q z{9-$}q4uN|lNG?LNAdMe5(!jWb^bANhezc70QB@M%zj!gf%aa~HzVA&^K@kbM#J*D zJALt2Zd?Y<(P*vOR`MGN(|9d_KHK*i?>R1ROU)`^_tv47{SZ+V-y{;904g z9mVLh-CK=*o?KPSmaa~QzK=t`hmJ0mp@5{aA^c&OFt3qCf{-KgA6-UAv zI&~&trs^uu@bhtA<1m9w(|!{m6x^Nt_=uT zLHN%2eF*eQC&nezPnK%?V2N|4z@^IR<%nmDcPM}}gO6#I!?6oMaPZn>A$IA5yE+=1G()EjuALpfgY?YT*rlSuZ=Z zHdP;X2R%KU4OBuMgdHEbBo8E~KKBiaul=WmMLdiQm=5ge*Qw}GkZx(s>`$a7mS`7jg0u;>!}JG3MjKVtil zMUxras^oUS^7&j})d_aB{d?61b{~JNC5PU*7YE$~(OQtxZ-`L?xXaZPJjYAxv5F+8e%GZVfa}#G(yc(jpn(pn=+E~RC~L`Y z7;_=-;i|c$HF&(Ha)Tu-9)!ha#vTV#U|!@O+R+n^2xUb`)fHwlW#O{Sh4|6YU7L%< zO?Q?To+KRX#XK z+LECVAQ1e|!jswtaLfbvNl=>Wsy?lyJ*>Fh6lHTjzQoi$bP7vkZhM&7C5o$gl&@|& z9Dm>PJ|ql+9Q>S)ETUpr5a3mrXoGx|b_{c$fp_)iFt!#_h$3`72*xgGxff_9r5RS` zP*1V;<`wJvXjj{mlz5MFWf}NmGna-U_(?;9KK32k!5w@c{6&}8Vz2rd#d`V2G+D!QMvS05Bhz_+1TX8dQ zeq~lCN$=C8Bh7Z9Fx;_7d&a$UK@)#ZY26y+v_+2m$u=H7#fyDB>o&qKthHV;aPk88 zKraSjanV*Q_PpW=50?KZp2_}ZN_(PNHz7i_H?}EU&q0Yo-xAT7OpW^On8DoT;<}QjBY>xc$kTKRD6>VU+Hrr$2T`BPxh6Gc2y#II$984e10Ht^b3IQqv zr$UkT{oXu`C5Y!kcJLS`Ep-TQ~hvo0{l(eVAzpp0@8NPKF~M?L_V z(|u-X+Nrrs4Z6#I94!3qKygL4E9^jtTwQMtYA$p=qebxP+lcIijFe{3qFYO7fPFyu zfnL{}UFO#NkFm+OspOt!q?ww=ty~hCzxD#ul>`mN4E#4q1Jk0Y)Amif*ZUOAvB!xe z7gqIPKH^`W93E%iHn6^Eyik;X@mj+NWGz-#75{cr`%OExeDpi!0z7ASA`-Z;<}YDg zpm*fAJNUGHDLjfT`usrcm<^<)4(h!*+Rt28S&%Nu_gs<L$_pv=B2clBU=!p? z=NT=dD@1rkU_UGt$wWg|DOOBh`xtfCIcARVS{jpDmIt0x30+z${dfrI%*isMZRJUq zEX|mcm8C5B=ci<(e318!zREPRt2J71kf;LoD@!9Zs~(5>z3<&^v?U?|sTpwYhU#TP zHl!du<$RH!%#$GKMK3M32a>ABL+(>&^@@j6b4P|{$oS2|AW&f3^y}tw-}i1C7>kjKX7WXu z(`I^FsT?PLfg^ENSk`(Z#MRjtKgmqnrYN6a9T(Lw;;3j*E!_I7qfdE6d$IezP> z4j|+KXPj{#db{*`{gJFEXy;E~SJK#3dHfYmQ6M}-oJkYKRDB59Q&0%LKpI26)tWc( zAb(0@YK|~BDl<@1oaNbn2YL8t34Nc+l(w_uv261S1_}S@$U6BfM;lfg<}xK>gK1m4 z>}KRaks3U!&!cNNf>X?P4AF;IZC94CIG9Rjvi)Umj^jLGCwxQmHkTn8$g6HICKS8| z+(4aU#AnUBs%TH;apcQbH!L&$A@VL>fJgXWBPzsdl_Ta^rY&T?#*mQO%LUTwlN(L9 zmrohOv!B?X*aV-;C#~Ft%aFvp{J7V+?RiLd3|SbD2_a8hC%(7PY52ecvEf1xrKSDJ zTX?+RWsjSf*L>c@Zu{#Z*4}t^g+zH;{fxb3s+xGwM(JTwEr8VQ&U^BKLg|BLoiMc` z4l%03bQbb1dd{MN!ri@F@AR0FQQMZwwc`%WEW#7l*vnW2BLW2Nfy$>pBty`vhpyK- zdP!DdFlHf%0%UpxWBsDuMmZ>i@*Fiakaj$O4sR4;wCV`f#uCiPt`X#0a5Ai>Eo<9( z%OOX1S~iKt-Cpt zy5g)YoN{?1odK+JMF+?Ix8S5q_IDsB?I&NSq%YyfZ5TNNG^^%$1VxUlvHJ9Nqf0uK zaC|Rks^07WM%*0gwGpKRITUcnk_KeW^Xv_!=nAo^A`Qih?`2{*qGj$(LW~2RWH3g0 z(K*S?bv(lBdKcNpN{iF&A0qomKH_H|PjqdV5^|o#itSH9Q!=XlI)+G(8A&0DYBuT! z!X;9o0A6P`SW{-1Aybd6Uv4v2^r08rLt%Xp*yxh7VSNOlrq$|7<>|#Nw#nv8mq|%< zkoJgT#^DT2u{}PldJDQ}5sxVi9ZDl%8E zyN7R~Mx;yn$uH%O-U2e3BfoXE3U@jPeX_`xYT3N#%%S<((BiCx+#p|%0raQ4Qrc8A zYI6=7&Y8}3!BoH4I~>$#R@kW$eh@Rx`q((wJI`Bi%qH3O-(&_nOWQ`wR}!pWS!W;C z9d_ly1~ww0HK2KSw!}u?fxdwv2(@dW0 zpOrkRoI-3M8`*BeT*Ug~$B@#zW7Hf;k&`Oo-FQcay6GO*)ov5=jGCl9Y?0ZEvKF}k z=)+i$N$qBZty*n$fj>A8a%^Sj@i!`yMJ=v%q*ZLkdr0Wg>j?RYgi+Cp=t%gPl>8h7 z0>si`1Pb*9tjdIhj4|sI%GItQ{^E+pmK!Ux;WO-CKF^* z73Ta74Mj~Le6rwRN_&*>I~Q_H4etz5ZO>h`!!Qdf#hOOi!)EWY#89G_&NRWR_z?fg zOPuj;icc!psB7ZwbF^A!E3{}v#3=#@(gt#8N8du&J4gQD^JkMN)E+H2XR}$v$By+_ zH}|Qdu&Aa-H?$F-nvb(y|%4fieL~!V0h*7ILrV_CdMc)2?l~2si~= zyBr$$rdN2nV?ps3h@lwdd-XruboZ6JHT!t_y+dC|{n(E0r|L|7`pwjk=ic!E$92qH zHYAXfeeSx!af$ZcePsSPnl`h=`+;1RHhO;Y#K>RUy%Uclsb^TvYgD`joc0P&nA@Ta z?_X?6<}Ww^&*Q_{xYIJMcN#BNozQ1d*`_E)3r0ffQvf%V@Su83npmL5!_nt}WU-mD zNr$0j)g#ORJD)sg@U`O-bIa?z*70m9U$~K%Szizlr+`@Grv4!GqPJU1pas`SZGJ#^S`vg#oCF31 zJ~uS?cZr|;=y6h^QhZ}?DB?Y#&2|3a!$M|{@_((`79r?mRG$S5p%E@i=BZGPa;y}_ z;L{@Nn-@Hp2DIE8P5rNq<5Yd(?<4Mw<*n5qJTlb($?^?3fFz^qZ3LX-Or6y{WrQrd z@(i0zg2+=~@YrD)m5~etTao*bZH@u4!)iNA4ahQiD}BZ^f+5El3~|VGj)SlawDQ?o zIgI=XI2q*4VM3#5j+rENA4f@a^5~Raf6YP51QcY*^|*OtG-Nz&77Ybm(1Rx_xui=%X-3@d7KE?m?v36RP zAp<#!U3;!M=@ae^0FJnkd!NL;3Zg8lU+JO<)qVN-8yc~|h=cje=cHYSJSx;K%e7HnmMc$#Jfju~pz$>kqBN;DTqJ@VI(?dD}LOP&GHT6qvqF3p-g5M7?{?FmEogd@T(VQhlGio^UjCc6oyNmvv%E;tyuw; zdZlIoov0_-Tw3?w_2Pv2dx8C&@)*oF-b>!NzQ}ewZ@M;>+oVO2`yq~+MwY9_gkJOM0@K$-*JZ|RgoW~~RL6PlVacIABL zb@Y*b5ew%)r*^FR-J_>%JWKMMWz%KArl^sMf|^^8(^l>sk}5}U%I$Q+#m5Xzoe9C~ z+a*49ugf7E3_rEumU=MdSJ}q|{|d2Vfh-f$L)HCsQ6Eq-b=4x4aS$3qGtR9-R>;S!r{Mief;{L{ z0wJ4WHw2VQNN{Y|{A=-otOV07AUC7OP2`UWBjcNM2s8-s!yGg4a(UfAIh<@m<^LrA z$gxe^+BE|Tr9Nf0!ZSO$v`md>z^O3cMn}}W)Z+V=EZQdWwPY4|TEWM6^zf{i*>fumngxVc15f(0 zHp^v@%(J*ldpMO5=j42f9kQvM!oz1fy$el}ht0JN!JlL!@24z796o%k`e|C}?<2%6 z@gHLDk0d}TS@&Ub7x|tb=FSq(f!TaToQH;UT56XfKWaCW2LuDLZF-gA&s?l+Lvq1k zKQ0E?5lAuN^(H-$X;2)}ZT7=r?Y+e>;(H?rC-k0|&P5{5fap5Xy$&=`|3f%tN(GZ4 z);{->SBH}_LKeHAeomlzFFq1EO-c*B+Z*|1SlCsS@6AB5w9NYuh^oKKCIPh_G+&|s z0o%5F!qp6VwTkvl6jH(oKWx+GEG3Li>YA}q;3wfg@aAQOCskN1YB4bX)^{;KhRjD3 z^9{e0`8ffEOrt_83|Byl=TI;U<(v^S{o=%uC5-{$uyVsB4!QE@k1dJb>Ag5y@jgtV zBQyF*8A`G&YbBS1n)~`VmtOIc{o0AgM22-Xiz{X@dvX7ZD?=tGtYJS&X;iL!| z3p(-By4D-+3|){c$m?7_cMyGc_g4rhhL{Fa+4=R*?piD9jfqr6#gbWTk5+mU%t!)m zsGtpJ)j?%z2P*T$x%vDG`@HTK{SHr9dOE3`lkwUD;9DZqFfq}B3Gb9)poAz~>P{Fx zlg$%EYsNO(u)x!gYvJQFo+aNGvg*M0aQX`IE7c#|djCl&Selmp!cTW7FyLMwzRGwq z+BvpMV@9*Y8^e9BYr!fo%v;nYCcm%&CfG-G43}G z39pePZtCsdFA_ME%R-*w963$+L!Mw~h4B!IiOVFEpCLA!K^l&rUn2mgy+M`QVbs~n z!^depRKh3AQYWdjBWB8Q$xXyp`Gv*}lXbx5))Y zKWufJl0lr&%U{oYl@)_9xKGx#V`YA&^JWBKB^$7S=`NCgt0doRtFexg$KbDegjA0I zKbCgW@1^bPR+kT;7?A{Mbt4L=xZukNCNOXoxKns77Y01|30dScoPF?&_zR4K8keaw z*@*mA&2P4`??cw-T!Si-)>qrpCcEK(t!3`tZVb9Wb>c!G7w841Vf(G4rZ7pTAHbVFw?gnZ4ZUBfa-QbGrOV}55@_sq(0Fkj`Xl>@(03b3CrS?SSwH&38K|Gd= zY%EETMTH4LUw%H-pzXHO{bQm72L>)!2vC8agSD9COXKcKoq)Ul6SPow205AC+r+Z# z__`vnJIm7pBZA|Vx)X2JE0D&TV*oFLkZBeRWoGqve+^q5*+1+EW<7@q|2d*!#I&P; zZT=DD1VU82;x8!5-BC7tG1Px6<)U3F;^G`tqF{SocZtIT;KMEU)&zI)VSK(d2A=9D-_^ z{mI*?d;ZT$(->jj{rLN;#oke}16@SZGbMrU;uauU7Wx4r(F4 z8^|_a{^%w5eH>@|gN%jePm**_#XoEc8sz+R*<>Vkfk@Lb91(NjNQPVQH=pkDn! z59aTC_@CQ@MV&CGl(i=kGfIDNsDE;ybH4qiLhFnD&)EGrLVv!7S+~GO>zItv?mw#X ze_q7s7HprJ-4eG(`VTPduR}K{5oS%5NUE|M{nw)YGsX%NI|W8%x=)grp#S?FfGLg* zoQZTN#=f@v_2xfbq0V(8!*M%me|h z4+3*Qx4${_!Dc-rEAtGPMhp*crGI^htvx`~_K6$xkK=P*Q@ESvJeTM;dD{Ozxc?p>F=86O1{dI&cCA4*S@dC@<-kxW! zm?NVaD*mfi%zHkc=A0Z~oulD=G%l2Nd{x}gByS}@@cq@@^yP2{)I5GrfsX5VZ#@?p zoox`0ZJznz`|a_F%1e~_7aJ@G>bNRm@b_{4@g+wIQK79Ofv=ly^>!4;BT-5hWHdHa9;%I`~`QEl7$xEr=#Is2lGsbTjP&mHd1 z3mAIKml{l6ZNzmmBQcvNx#<`B|1+oCV7z#gZ0 zwCUA8J9uUx;h-rstNhU>s49?_Aj+p32B37bBaxG2Tig+Hn#wk;rP^EgpDi>(O=M=Ub zfI+6mpEWrrQ)-v$rz_X9`R8?yPT#jq@{?y9{ibQ~WKi_(+9=Jc2qi&*6M6yIWE~)U z#s*R8X9ti{ga83aOWb+ggc9-VcutcxKlml^!0qcIv2iwR5nx7)=*5J6 z!Yt=IU(AB{`HKrMM^;R>!e8w5@PXV-%%L=%auIZ#3uZqa7MO25R_@3 z5UDtsS^W99|GhL-B*AjkMQYMQ*ds8Bgur^iGz8-7yHg5-rk@qHTp#nqqF~z$#D%NL zY8w$|K_zMnT00#whycN-n8a>X>n)MN0w5;_qc$ikiYS-GHVyI429*}2F|jTrp)>$h zjQy1RO4Q0>tmzZ5hAx`%?o#}8(ag?s|9uf)oKG245)kJ^yni8j`b2|P&9o0gFv!V( z2X$kLY5EIGpgcwe-U#{`bpR|11=CVkXm%adMa~~vS(t-vu?1L&(s;umW2RD#$fF3@=2||Lcl6r87jZ z-)^Sv#&XcL7_fipe}=t*jM6a2l>+2(_o^>|7fiv;`x%E3yG*|(@E3xB&5^K1(hsoG z`js4>fJJPQ`?kK)&j!Js*r#Ex?yorBecliPyp^@96j=dt-t_^ zUif5GatXLlv3VNe7;6ERp$XD4Sj|r&L5mJY!~zVF3ktUu^S4FDcEC3a1r&oV4N0-O zeJ)p%37YbVm^fC9sRdO961}Tgx5vUnTY)*q5_l@;({H@GNij7 z$D#BOBbA;K^E()NktgYUZn7_U9e0};7YIbJ1Lun^cE(M^mT4E|tw7m!Aq%K3s0nGPv!0W)Bh}gfY8&CJKNGnM0SFT$vE_1|+F)5%) zViXy?%5Sqi;_T7VOKXMZ3H;ojHzwXHQ3Zo&9J}8 zqRs*-w~1HjREsQ*2skuM)bNh!v)q>*fW2DxQ@~{BNVz+TlBxxowca3&wK(?1VZrU6 z_z_i5{=6_L+KTa&o|8eYk;oNFq@T0KQBspIP9gGM{Meluc$7CX9=uorfME{o<^fq(F`4bUY_Z_K_h~u z!ff@LdGWR}A^2ruqWo%oroba7=F7#GIFmIZDQ0kNNieU=7NcfOG>|m|EMf_?)Cm95 z;JCbSr@+4yVmlny zwEP*J`Bzen7NIm;Q+fhhEgAV+_63ji%!o6pRg5WcMo^C?WY+`A5$`W*zTpG%`+~V} zDC;m+voWrBbnwahkOiTDZA5=$HDB=V;N66i;q$1)hvJMO(0(LiRJ6OB0(CgyR=wL( zutAW#Ghqk2!7DaxyI1{;#gTmaTG4R0@HJkT;U2tyEunwzq4oF_ms_!tj~I8truKjZ zobeIObH(uW@sc398arzmF4b5{+EA=?W5RfzQmwT3@Dv}=%)ZC2X3sp`4iDR5!mZ>? z=4e`hYX~QdxKP?`<}LEm<;NVAZ}p&5IK8qY5tMz;j9H-1VGbQD9H!t6yrrJt0h0Ha zaxrZb5%3%{B0GXva=)y={`2XiV_hkC_D}9*O%O!KY)gF>~J;o-9*^)t2Fdk`CXUC4#+v*Z037x95m6-0;bUkJ}ZAo>*@d>Xwb6eSEb)@5l>%jZXZO zI;?(>bD*oB8MwvKJ&?OP*?d+LAfdOp?~D9%98Gk#1g`%Kxn?XqfwD{=n0AU8a?a-S zH${)&TFU!RDz%yQJ&B*P0?XGj-w$=-9PpS&-)XXC^(>ABVu%oAC;uw`gs2zqS^J!2 z0~hpMH3)UWc9>>+QuRzF+w^8FK0u!I-Nb)OfpqA^WVQfdNWebmO*PE6!YGi&ApUC? zb!JoPtsEzQ)VlguejJU1Cpd}D*G+K-6SsE!9JjE@=m#7Sm|3wM(RFAGNM<@;I7bY$Y@j+IRQ&Q#8 z7hF-hM(yv{xXTco9@W(iUE~G$mI>^6A+SEj1pWpm9hgfreB`g@91j`Xm1P@gQ6i5k zdU&ryj1?saz6^0=g%KJtcOP*W2MJ*XK$!|q?d0m{vS2;gdrmJx-z3o379-jfJ&*U3 zZ_e&o1P4{*^Ob59$9P1m8PLO?1eVEMWJomtOG_b<^_y<`ZsOt;<2NaA(Kgm}i*zXD z?{FFD;gd(MKx{?$>JWqp-6PD|HcWnOHGNdUFV?YT!qY*6gP*~dn_qzLhrsh>17dLW zIj7ORyn`z)f-mq^50`yE47R=M}quJ=6lD~LMS2CBv zsd8HO=Yx5hw4!wxRgfl-`LH4B_!BgI4D3e+u;xY+_9ilcCzp$jy?uFgF!$8MENh+E z1`0#VP? z3TsiGC0#4Ms8HtCj~26y{Db-p4e0*;;7WFpHZFNSW7<$^M*22x>)thG7SWS^LY)xP z4lfhmYseS!AH4TLdB`FgPOkpuy!9@MkF>1RZ@?nU5E7MJ2J)heA1+=>M=a0=?lO0k zq5RTGb%Z@&5k_!Np!DdzKRtrBIy@sEe6k$QNeVsx9f&UPBdq_|E%pq;zw^Gc1H=h< z*ve2-h3Xj>Zgp*iZ)dW7RB?$$?-)bGu!y2mQ^Ck3)X90#q=vdfzC2aupiNEW*Lbr8 z`}Puu%Iw}hVw0UwfF{wXt9I{~y{mk4$W?ZXwN3@kJu4fVhrk5=fMZAjVa)cEN8bah z=u0JDw#7({(h7jQl{4nY5%q?f(pHA)7ULJC)?W|SM!A!8Ken`@DYLdMY7Fnh! z3^inXHubUXzQYx$p342&eujS)@KsJ)y|c1bDepP zH$RX2J|pPxI&v#99ASL7jHka88DH`*TQ02?i|byYVe+T~>Tomq#Wn0Fn__Wg`J$l5 zP861k{O(7dVhd6z(GL)0qCt!1dUHC(8a3lw^}vB3xzOUhk~;@}vIJ~+EgiJL!SW*4 z1iqCNX#dp$FE{VE(mD_EXu2N{@tGh+bASHS3w9>x3Co$tk#3J`@*_G}U>CS;>9+9i zbdRQNw&)S(w3++xddLXLxnt?tu=n>JS1if0!Q<(lS;|cHZx@u#KA<#cKg|9M=g`E0 z%}|O>$Mk>)&2)a}u(NC9Zs1;Twr9qiln0xr|cCB5jyBb&1&Xp|2a=P-SI= zYW0pni0oR7Gs0)y`!mn{(s2=W%Ajqus@}o48eyTzuCd&8I%i%4N->MrQCWwGYH8Mm zSPmAX-V;G>`f?O&<`2Xv5P0jHupCeb4u2?oQ!dF$$ME5CwH^K&j`{~yWcdOf%Xx!A zvH9&*WHTK!REr7s7(_(eI&y8a00W_p>MBtCA_yQ8MTHHJ9vL9qjX~187>Fcmu`vet zuo$07;NB;y2_^^USEd879sa2qUiB=GfUN{ozB%em3%9hieB#}5(4eUN<7%n$oPh%I zzz4w~;bIuJV*;Uck_#u>B!Nei($P0EK6CZC!Z@!%%+1TB-M88C+3%`pHI#BuiJreK zbcTzdvOhVPh6e0MxrHv11 zf}QMaE1#$N$*Y;W{gkb|(u*jm*RfYKckWf+yj~;uG+71efR44=GCuD#NS=67MD>4g znU`%fQ7~x&9)>8&COo^gr+fT|`@S=R@O--w3(S=XTE%6{ID~1yTXXq!*5`%P$)#$r zP1e7j)VAn;4&5k}m7feG1gVJG;(6#D21ROe|CS;TwnO^3W7eO^wu`wxuX0-^NFGc^ zzNlYPL|lSH12JSem!9!%Y@2W44ejDuGPH`&hn5yZ>V7{vLAj0UQMlx2EF8>x$hgLc zh&5Q00i@SA5{dVdloQ1#+`QFuCJ|VmZD#II5Ei*Ze5u{zU!o)RV*1vBfV2|xVt_4K zA78$WmVZb^!K#lxB-eODsKunz3@jUW`jSK<@q8$KCby61GsDm&1e9M6$K6GL!Xf{R zNk)AT(8U#1?}Z#^0VM`1uIjPOTPz2{>PS9V6YGa1nDS6y6?~$`^8BjetN~ghkJ3;m zSe4M%?{HH0)^Ao#x6D8v$TOcRGp8$LU{%&uw-wC~s2im|)C9-xk>S=w_Mb)wz|WkA+$*WP_l*XJD%_4WOAOmT&D;twthfb2k0+ z3t!keZLl38O8FK41ZNuq#M0>(17!V@=9{kDQ?PcA8cuRxcxET;c&|xx98hd4%%ktL z<+w)glp_Uo;_80ZtK43C|NApw1>m1U3`}p0Qiu>pxNrT6;)@mi&#)|^2=-^SdSGF8 zeFq2tO%~ageXmz()|ZH|JdN*xg_=qn6F%a@%RMt)-i@qG{Vf5>364m;Ga?mh?czVT zzkf}Fx~R5g(7rA7qtf&9aeV^3bk*J(r~)H=GE=>}ULnjsJgQ8n$nGb3XLB6(7V_0)7?Po)syl}u1)c|ZUl?a7n+11dA85o!^SS9ZwIHx5$CL9(oNsJ z^^^aw0Fqul!5w3ihHq1>%?BO9Tixor06KOi-i<)PR^(P~0PxI;Fq4s5W|B#UQ3&C3 z9e7N2s>}Kr9KW({3+Lpp97=7!cn8_SQbz>+GJ6Hw0a$e0B)8<@M#BM+tH&axY82i5 z@)g58HGqia$Q%6l2wJ-eunONO#H*+HFSza3fyXwpXY0GK9O___angQ2_X5*W!QwTI z#bgb~FLF{Q%gDAb$iCA_ilakxi{s*^K*tdAy$~d`qD^x7>n_a<&$U0Z08H#c!ilF< zoV)TB4lm_&>O#*~!jMfG8GM^njkX~lGRCA6fKcGg>KPHW(!)^HCD&V&>_A|0y-I(j z^XDwV*}y{Jfwn2eqhr^P0@f(tFR?$cBBY~VfVrEeOeNn=F@unDmvaYCvS<|fnj=Wr zdOB+dQRy;}XQ5L>(dG!IhMOc-AV=UA3l@98Ad|gqHR}a0hb=6BM=H%^UPy~2v@V84JweF9f=D!Bx zdgc9Vru>1Kj9Mdf*~qFXQl~+LhyXxF06=%6uT=5bYL9tn7vrZ~K)0pdm7LKIaOP;9 zq-aIQ^@qVjG^tX18$;o(L6h>zq3W8Z5A^G-4#i&I%V5CXj?^(TT!YxQ)E&% zgHzY}n-2hXsvUEpX@FQ1LlP|NJ{j)gl4H_d7kcXOCg`3eg>I?Tn$OLz?tP(iKDFSv9nV#fmjj4%fSAgUhqJ6j1{|!k zRKiviqm=I92wF!W&h4znU8w>aYq_S!TZQ0gxt#IIqDHd-_x=mP0KytE!%VP>+{|t)MY> zLS7B)zh4~1zo)aqjZdH*PeABiyikuCn~n(JD`c%`}A4;F#{)jIDK z3e>R6PR$7yMT9Xt@uv>fW2vd^Sy0nQL zhq1A4SA!+2UuXF^vBC9?a+UsPV_Xe=m>9&dX}U|HNEDzt9IVpi%sg0J(6@PVY*FsV zK-U4XF>D9N_Tq7kAxF)0KhWOrn@K5^e7+jndRj#Qv>GjmGA_*xG$-hq5>AYSD&@zC zNe56sJxi;v0Y#4h0QsqO!S%9jW-HT7O*nHD@Ith;V9mQ99<%Q^ALB)LBF-TME0^I| zvce}52z^=C(``VK=CqTGrV+^Q(x`k@XS>W$Rh(|$s1+76vIiR4t+;N2ehwxYPb;7WXXcnucoG7qiy z$IBOa#sVlEb>%D8?<(;4eP;mNrds19*vbO~@CIyF|^6uWJpD=2{lPms4Bn-4ALh}Y@%dG%|o`M#(Blxzqy;+!qRbzdMrrfHB z@qRSDifKKcUie}>yV9)C#zMEk{pWtMfr?<4=9m7E4ofS)*rv3HeBh*9*+c_K{{mGn!?Hiq$K0cSD1TK>%PyR)`qN4gT?mgXMFGo zPOlgj{P`Wp^+!+tH7p0fVX12<8k1Ej&PVvJT~u7~Y7Io$6E@51bs!gK6$Z!op~o3{ zJZEhteo8M7MU-H#&0g`?mjD9at&27n9XaIHruREF13gxH%504rc^6OUQQW~v!$R}X z>0vR0Qp*Qd1lFtk8D46e@rE;J-$3Wp>ynmqoyj;GysFace%HmU*L&gLKY)@3C0%a3 z-XQPhRQnASD>LE)s8N0gQjrdC;>xF>e4)AH(%~8}WlWUV4S-YZY*BzRa;T;((8a$j z{Rt~Y^?G=jZ;Yyyv($}BR5J-5mtc;(ByY_u@4U)VtXcJ{F5rl#I=2LddTen0!gw<$tY2>%^%tzQukw#j5w6x zX@7x`jVKiHVTbqPUb>iV%Md8n2QPVFbP7G^>0?{Bxo=H58jm<*CbMw}BzJfp5ZSK%**u)DMM3P&pjU3lJQl zMT|l+O?i#G3W-qiGB7ySz3$JcZL??Is60)K%T}YN3}N?!c#TW%0P} zbq13e*bz*<$~AH_0HV%t=`$!OINe&Bq)1}+3Dj5`OS@Nu;~xVod*i_X#}N(6@hbOT z{#Xr2CMRF7CTbdU+EiaMS2h^KjMGtTb(#=ZiL*ecxbn$7Ap%WkQ22|`xIiF{G1H@B z>3GL2g7_k%SVeWk36FAc-kxU-)2U|+@t%#`UPP_DN^-lJz`E|V98Qn- z`GF|F^B+n}{`|NDs(`38yG1=8;*xGM^Am<}HT59He5WIzW98Ua+x5RkAX6g;#B8;= z7_!6EquP%zaepZCPSf1>I(v3K8%j!2%u?)(P<`mXNDu$;mf&a1Bv6nuu^Q}gYe$`<30 zh=PgT31D^u{tVl~M=hc!4#Pg=sA%R{Z{^y|;%qtDI`ti?gXYiv4{hHa&-MPk&pRqA zWtYr|B19RH(K51m8)aqhJ(F1>yU57MUT>T5Mn-0mQFb|5-BP?N<3ikb7A7ISGkyyzAO{6C>?0RYME~* zcI@=Ud!pao%cRevgF^U;7Gt|4YR(KxrM@vIIUc?}^;yQC`uXRuo)_kY&9!ZU3)G}u zg?{E2#J{Zp4r=<;HKvEL=N9UMvaQn9!24*s&uXVkuQp&{6_1N-TW3==ODG(jeMu!L zZ*Q7DqlY>C;_D(p;_(6Y0E>$Kmq7{s+kR&Ku-CMjZS-!CYqy19879N6m=9z7~0mu#C>mu~dMRV!0;wYl z?igb69wa(tw2_@39WI<5wXKBY;g(93(cr{Hv^WoXUL@s(ojg87c$epo7#{Jt7?c9{p!{=|t*!dkz;R|)p-fAV&s;|0$2mK? z$AD)E0RxC2Ot*!3TQTGDEJysY_hs|Tf^|FrdP$-4z>A_xd%#7igaGq%33nP>Zm`>mL+qRlQU4 zhW0S+qGsCU{TT<@r#=(yoCEi6Y5)99e_fcb%4$-=WU2J{>V2l1Wv;83O6$F~_qnY);q|LJ*QCRi&%9tiR;M+8zbqDY-wO_J(d=T}I4vd3F$sGEv?Nu7 zF;=ykmL2xg&JaW@x`Qtl2u4olK5IRDXQ^^rP%gQTh#fc)-v?DDc-x|{G*=Namu@FrCCOAIFL|vvmZ_}I4zWt z>3^NE=2orXNHQd;jvI>Y1Rb$JAM#o*`1Q8^3&xB_AtWlhEz2x-JjH87to(}4!k7lk zTY`tzfGN|cuw1z8mMfZyg7UJWLNxJQVUR2#WXsahOf&-a#@p{BVkDx6WX-O&vy7?~ z%8*zUtr1~G)*zTxDI|!Ts{!hu<0xPI4b`8Bon90O(}hD^T<)W?%AK=BLD#Libc1xH zPu}%suqez)CBU-8*(*AKePJ}toavqqp|mf;-(o&!v{v_BnCT?<_H#h?DCh~oS?Thi z*_$*Wr{h($DUep3V#DQX49?s*sZi#{T87S)2uiplIgLEwcbZGHBV}q@iEtx9k#!iO zsni_N_*Aip#!2H20rp24JOv?jV8MO@iG};J1T1?0jh=;ze^;soZ?Gpcdv8vEflXd$ z3CflPOrI3<6M&VeQ`E}KRxhnY9I`3G7LxxGKciea_H~&fv;uFsJrH ztxt6_r=apKjF%D$W%WOc^5VD-=T9`fb_JBC%6pGxL$7&vs$6J|6Mdt2`20maZm+E9OZJNVQp+)LN?O&|&%F`&}6d4t7^* zpZvU{=j5e;?P-=dSM~FJ{B&skoHBtp(K2Og!6Cl~*ER0~r&@D!Zte5WBfIws#_N9c z|Nbdt#$KYppsfWu{;5VFmr|HFD3ed|qnJj3aTGrW#L*f4Lvzo51srlNz*b~?RDR{> z{Pbr!_5KjOa#`+?SoS_>ub4R%<2s-`a=%^u=jC348{z9M*POo|kbiqr&Lsfn97&AB zttH=nDa+9#>{2&S6_%Up|9+Ck6ivSoA^saLRP-?O=;rx<4Cd!w{O>c8fP!gftFcx1 zKR@cFZ~t#{aZlzAOB4hAK_Dbeo6X||Nno@OqA!W(LXoEpPxog7ziZB+y6eJ zo+z5TqY%ktH!lKELu%;sZzwhLHC@%PfmZft??3+c@pT{@OlFH;_1`CjOgGx9aq$Xy)8A??PMprc(cKOY>1cmrsTamxTP;e@M(YBWq*Co zt1QtRRPmfk=!>`*&zLvgGk-bBe{TtU39q{i4l*@!Pw9>y|Fwki_fGw3R6xBB4sAhJ zPv(gIY!AEso|((^p2!-U#=V5_Z0*^`-J-p2m%Vwp-)7#cha^K;qZi&Km!`qs{}_va z*Ij(*!`+5A=mK0~B7Cx`C#dt&CD2)yX7T%J>Sd7d5;kLx8Lt0+IeQV&%V%M8LFO@9 zH_GtqmGIZ9h;AM3VwE2dHtKcGV;<$@weg2h$miA|ymhvbhzi$APH~>W=TFoZnr0S% zeH94LJthBR^x5U@KVM`&ZF8?cWWHZ$mdq-FfF*n+jx9m-5Jkd_g;9oUjK>i`gGK(a zQZ!&VYNH8qhL7pBddffju>Vb-S2Q7vTD>#ORM!Yb?KN=$E`fx{$jylAwh}ZueZ^Kk zfhzZx5+!UJuV4`A?@!1V(lML=n2=W3MHx98e&KzI1lY6pVzXiITxRIFlQoj2R6=MO z`d#6F<3RqDkPI`J3w(6(jhoJ~f6eP(8}5%IEpLacp_#kxSx2A`5u9W}s*^H@<#!@j zv2n!;$iM!9%nA65=8Ts^5cXW|aZ~26Kku)N%0rog#m(&)D(v&Vr}qckwsL+clvpZxn^JB0U-aC<+`u$ijF88g?OFETHntFImnGtzKEoyrNqv1-WU(rv;t#y$+X$zx-%X7Rrq7kplRZ zrQ@OL|8co=A3>IMhqz=udM^BCPr1z8%S(S!XF=0eo~p^HLKJr zo}6^~`3L{87NY9NjvR=Xly5E>VDrlk)icjCkg2iYyuX$ux^v`JQ?VT;GmQ|C57VjP z$_LKVsgbqk(_=rT$!pH7<;!plVSgEIf@sWAu=`Iva z#y7VXsy)`Bx|Q`Qg6(_;B0O8GW3ziS24380I#V#MP#l0aN=0yxFb)dW77)%*w`7n^&X(;aKKf|`! zcgj%iODv8*n8{491%5rLe-H@|#8mYlwAf01_IMTPQjVa^plG7=cC*Mb|4 zs22z8(@Mt(Da1SKu{Xuta~i;E9RpbBf>SYe5ZYT)BZWF=p^0du^2Adn+}i~$BAK)% zfIy$k9Ldg8l(j<}VQXFf+m2~EfgCbe1*wZ+qJ~v8j(s{hZ}2Gt!?sptDIXPY7>ET? zk!LWA=7&2%pYldgL-#A%Y#pcY4!fkb3*c@6u6V{3#;F6?fz1ia(@_sNem-v=9a1ec9i^&7N(>JEngPqFC)^LLQ6iN3eCR#- z#VB&69GPnG@!!ANflWTET5sS2E@eMib}K8QJSY(7?98y2I?=?y^j~=477R`S3>?+J`v^DIu#sw0uy&=beoPPZu`$zQSmCavknfn)4R}u1fuIP$N zJAn;DKr&aV-X;Y3dJNvsH{KU%{;g9>z!F%LVfH9eK731SP{_0`*eG*_2ru-`C-T^jU%CMr`S z)gC7SHdTWXLM=#HPKoEA@exaQ;plddtPui>=p(c1H*xXH(?kolbnf^8YvGa>V_9&j z$_0S(va#d-W1!*-R<7wR>=zg76a>B9f!HlH zte!>HU+*G!eOukeZ<}(39a)}PUR);x5!9bJPu#rc{0iQLt3-Axtc#KKcw2EGfJWU|a)onm zgDnIR(OH$VISU~{;=Ld6*0)2^QgAag)xvjmPLUy)iMn>Z zYSz^^Gq!f^#XF`!&-+mN)&2hTH1&o`bN-`#hpbcM7b~j%8B8VE?Z-QuE=(RsD|;^7 zKt0`8X=K7*+#<^nG8)mZ;e7n~5T#;g@rA5V`OqjhDfLig4_1EmkF`6=@z z21zS`T~UXy_w%5~I2Oo7?%4vvpoi@v9s_=TU~kXC)9)&$Osjp%I@eNvy9Ez{{o_c1 z14Q@7dbssZ`4wG8kubeg{4fgC|Kn2Z#pvYHi2JblZTo<)bAX_@f$GN6=LbJ>f#B;k zR)5mv!pLFfc!1rw%J@XudkN8O`LM^mS9;VrbKN1u<~!-aX%U8p4z)Hx4<;;$DQV3r zuKeW%kn_o+$E7l@&Fix|N!XP&#h}xS=-Vc+vgR>v%9rYOGAC?*yR_lCv**~4?{5V$ zakG{q*BaNNJ=}DGekJ`A$w|-##>8!FHK_ZZOv3;TKDY&E$( zLvJDP8zO}XBC!U!bl-P?{tQ(E+KVP5Wkh*#ONqlFY#6ojN+W@;Qa76p<}zF+Y#W4G z(*cpyaYXcoohupGSDR4)>dzl9dkm2jBI4BX*>57>e7Dr?A0}Kv^@_9JH&TZP2GwgB zk?01bK%^1*VR$!{sC5>g1aTaBSYI=9$3NtOD9&;n&`ak#g$@wGhMs8qJKy>vgqb=4 zPSS!Ub+yta zbNBalTNd@q8iZWgqb3dZZCslNET2%@eZzoS(HhZ}@tH*N8FwT&>-kQ09~FG-(tXtC zt;;{6d9o=gB&zNSU~bm5SNr-5dvvmW7SZ!k2ts#tqwGIP&OR>iM%J6u+inKQeq`@S zgX$jE!vEa&5vT{B%IJ|0M3iq?M#@0hag1TP#&L!PF@)o z<7$tfT~qEd1?dcAhN@>j!;Ac?;Jt*X&H_MLE!|>5c+L|!q>@k6L}-Yxk;>UNBD28X z32rWh+L~U$t?`VfWbuibKu{lpUwyuudT|M<^dr7Qi=R}@8Fc3ZFK>W;k1S@$c>fm0 z8^!xADkOqVnbmrN_NaL%8WKM$KiYpI#xU9h340YY?zNoI#j{XK^@h0Ts!F2)_7c@K z!iTS!zI@b+Vwj)4FZ~^{(G6-jA)y6TZaw+O1b9rV5CrhIdypAt;&^R$Wx2tlL`|aFpSViM^qH-nLfvoXHF`QHMIq2@jr;7VFc4V0qtx96 z`$6wO!!H(1fW3cS8;$j|e0SSS_nuETgr*6+o~;amhORw;QpE7`YK?1aKpVEB7T7 zPT@K&ND$0F2WailM~9AEUKCOP5|()XuxU1kesq91AK3C?ATrTy$s*oub^ig4eQ#}W zRNYW1Ar;l%t_=JYglP?;VtgA_&gyMjen8=41}%yQvR7PW;_9^?%#lX`T^f7%hRvd8 z`uJ^Ah|Juwv9(Xt5a)yJTE*7Cf~`;fo`@ z(g8FHMNKOZ$v5d9LWm?fwxA0r3otFY%=(eL{{EF9OqmH@A$YHy38dn|^mjk$Bx%8< z5cBQA&L)HxDaenG(hS7yHacaXc;|FHXmkar%qCX=Cpd8q)HAGDfeG0Xfl3Iq^2&5|YCQOTNKTlx*J06?YOXMa~GHBafF{b-D0ZN?e6Hv6EzOf5oL6DQRh zmld2Na{+*9G93@O@%ett_HxQH`dx517edOtmruR5K3}m=3poDCVE*wZ`M{`YJS#EE zP7!mK!4V;6w#Ywogg}YYUeNr5w^jst$JEarzIVfy=ooV2y^?BR$*`9C_5d+Ru?hP!L?LlZ)rVu7H4XdXXfa#=!4zE2lYZ~sn+$%dS(Vb=}& z=f!NM!(rhy7`yuGiTB#vPi2wAqGPut-|(=yzchxGa5lS0HWowz1g}2MVh2~Dyg`8~ z{*pUkOGrlgmbp?HrBHq`wtIO}Pn@Dyj3py#?7*^>l$CzTRJnNcUFOkv*61`j) zbZzFSb=rl;D7a1!v2!ipwk)nAvOShz|D^6)!Met;8w!?7pjuRTx39lFO1}?mcj0wB zD0V=qS!a5%NV4cWE}x)36oPlivc{lG+nF2^IwYpIM${bd$}iNF_d9dkNJ2jSSkG7k zOu5>*U~9;J4u)FhLD0?5N+S8%m&Q>oS&!*ikiv6*4w7~ba20!+W8SiL`_Ok+uS2Vm zK@7)1lJH|;B)*w>R1AMlLDWkZB?AoG1w*FNf<`K~+)b##HO;;Qg*9b{X^TV98M4=D z2XN*fKi8VN-iV<>fHtbOw-d=vODBzs5kFPqnR^rTue?KM!dkfFd}-+{?|3a#h(G6Fa#ll@H`e==n71)m&K!e5=4Nq+@d-7h3wq< zr#V%wAk4&Lb^d(Xs~Dus99tZv`6(3fm9GoffQ1;i4tdGp4Ntp{ zvZQhg{!#+CP|Z14`)s7R^XcY?rrgyBYGiNxe#^&b#1d3InxZ`g1#s68Vo?BliQ2X` zHBiIHAcHqsdc^Iz{$14jU<`_#{NCHlW4wLzmS2JUeH0N3Cd(kPdBNR7m!SDb2|qPD z*_)dbGh~?4+d^$yBzJ1*sm{T&%)*bi*~oobb#GzPvFWJ?G`FCnj9FC%MARAF?h>%DX|b zRtrl>i?~>90@*HWggZp74VI3Yv`rSI1c*t@r7O()4k-mxOp-cC4@3I%5fLON_hqx6 zeD3R_b^GxF%Ncwhi5u(k+9OS-74Nqp??%Oa$*kFeXg#fU1Gc0ldS6W@+fhi~uuOpUvjjS(aR=8u zz|H&!ir=TZaM0jHn{8N-N!7>9Ojmq<}`5r?1`z(X$>pQU5tjysVB$3^Tur@QiGshE zM&R0evHwcdiisPO=qi0QE0{;gVha{3#8-&;w4OBuqaGNI8fmPGp zC_cD7d;tb5db10K=etu^wbeRUV5@w=iirZib~ zfFQaxcY6(mz_=Qek-wIp$ui{rz}x4iK!a{}sxT0VY8x0D5#+I!f+hiSO6bQRI)MZt-M)65vILt)YkKoCEO4RxSy6Ngmd6Dp@=m{yjCEf3(3oTy98=A z6`pVOe3BRdhMcff=-;bhUa}YVf$|fEVQ(e;&z_1jslN>ZD+@4{BSh!!AVHd{wp;f- zwUt*#{c_Nn-=Oxe4h*k45K%`NhPyIp-Kae!X;N>}&l~qD#*F&-mTo-L42jaaChH>L zD?-OpEMS8NlGzJryPfXjb(LK%^_YZorI+HZmY}+lE`$z;A(pcp{0#UR4Pp(|Gw6D2 zU|HCJZnV4t;!M= zFgA7*5NBChhcN<{BiEL*A4zp>Ad%H#1#bhH((}7avL1kB>!K{m36x-Co2PtYAd0zq z(dZwQJ<)XH{jFW|iPbNgwk=N$Whq1**#xB<8tnx3+&KFa{7_Cn-4fUr4g>05%!Wpw zMOAaB++R0Ghc0rU)t7<1oGR@Tk-l`TpadL!9wX$9$cE8lZyDad`mqm) zrP(+Ac(x8!6y6?knAI$6#sdrDJBRa8LVH=<@#*?V zL9;D*iKBy>oqFunUjLrqr~O%Gec$dY{@a82*EjK;Kz7f;bj2PH0Xx(@0<*V)oO9|T zsCW2h(09&pq`=o-W<)H;YCPhKp<^*?@P+<6?d{8ozn<7BSA_Nu?K!X^Eco&lV%Yym zm9IjPcQ3~t%RVe^BwJ&_6?_^2!EXOmF6KHzqip>T9UDX+G>_`Nfwt*;l`ezpmMnaz;;<7eiyl#V?5?Mwdrp&T&@9EH#tx~p#F{zr8^PHR6=@q!Hd&qexaAb$@I^CzLhbcCVzw@Bu%Pm|@` zKWd`U;O)Hdxak+a!T&8|H`FT-PDJs-)7jrB2!C#L(e!=pLg$U&xB`ED6EMkGNMTEb zRVKdt4`$4(m%wMrF`AC}w@-op2J}hbz5Q{0{p(*bRz`dp!#jiieTbf@I59-nAQ%+N z@?V^*{~g%dr%v%$?7`P+^C>tBLPjs7B#2_9Pxm6L02-Si4({SGLy zH%Rjxy6V=jzg%rz(O|o)ZfEQCPn6@2@pt8kLJ_AHnEof-p@N6_t8^`Rx#%O`6bIj= z&SLf0_k%8Sq@YrJssfQBBh7|UG0iW(s?yp{6@%2%CAQO+EE8_E^P>s<%|Kk$2z;NqUq3`|MpD4 zW2Azs0ZE5Jj?%0*^9%jZj#8@;>;S;LS6)MMS}gq7e{G*D0ztU&(~KR+sC>bwHF4ac z%hnqzA;}RHcG|x#pDzW>{Ecci+Sfntx}UaMM>&e-EUwevzPcNbRPOug<{TO8fobvL z#-Cxw-KQLbtH0ybO{8`(2>LXGxMl=FLvUAw`s@h7Xkkz0%5^0f9%X(ohMlm?g9Hgl zlzKK#unvzPVs*q`0y_*IU8jM?kjq?(Sm}BLmz0qMkrrPBfiiQX)!Yl_(UOvkfuFB* zueJ*vUkR|l#Bh#+d@_kaNMR#6ju)(aeW1p}CfvSvKCFerP&)qS)1Cy_Slk=37i!KMfB*{I+w%C)XS~EBl1qhWQ^n-$g1wj1 z5m(m5YS_4`x9n=4C;1439VSIsK$L408vd1W}RIs*0595PfLK>C6!zl?x@xb}6A z%XLpHBovRC1%^U4b0^h6TIt?xwj45kUyv7^*IHm3vOL?=6KjyVl^gkGBQ~$7O~A{P z0=Szl&hY6+e0>%n;6poW&Ja}Rp$RYs6K?SkT-Sl9mZ}kg;PuMw&tY=4Pz6!^agjOR zbt$PcK<4w-7F0ZVqLrt4m@E=fNAyaR+9u;>7XYe$;i&$jpsTFFy0{6dR!WU#^gme- z+x31H7kK&OtzWP?FK%ybsZX(hLPWBNCr0K8H$SZU?@%3h4c&2bB?aCIBJs zL-tw+wj=1JEtv$9x?_BSelGnhNMbL#unI&4LzbpVXc(`o2AX>+>4n}rZD%BP`^eI~ zhtyo8wOTnY0|8fM)H6 zQQ)d&_iLvOz4`2sz3uy(KkAerB?)LX%!o77)nzKUY$>s3p8A~ zkx6*lh6*PiKZvt`Y=wE@Iq*Dq`RBV;)N7Mu>I4DysY1{k!puY@GezCpvjKg{3JELt zcfmXge+CL9Y0Zy0SrtQ>g8jgoP2 zrvd$q$i9(&RKg5gt!*z%c-hx@MBlDqk9e&0jVw!Ml(Qt~Yg!H@zg)!CnO_cHrL*t2 z=FQNJZLRJ4_vhp=p%@=Tm$2>*i0SeHd~!wK+q(QvE3tP5 zzLfvG3jn2up$qEuD6qXi)ftnfGxYobYxVjS=a*L7njn}y(%|;N=IJxdC(|8^+aPNv zK)mM?JHH}KSaIQKz%Wp8LM8|~>Kpp4F(*QQa9`dZ!Y8!nNjba4xE=u|)ozuAw1i^i@1!(NAq%gS&Y4noy{&Hc%vMR&>IZRn8A;==8aSnkqdFn4`V_`I${6 z(0eb$g8tDY%q#IN%=G$o`sa^%casZbfOk={yOQVO&2Yn~zW2+n(p(uy4HJE%gB4`j_lKk34YK(vs}2^Wokh`r-HE3#QBH0B281RqvWS}UH|jriNsN7t^|p3 z(~+x~;8jX2NwLq0c~khfwe$MuYlE@V1mY#wAgb%=uGFq>o7+F}ZXx*+_(RFd=xUY} zJEAOkBKjw$CE8Y^27ODT$gDree4l**9p@chZ_*8=}ej5e>0vzuBEnvIR}1Az@}5y|*mQCt8lJ zfYn}7{ty~32P{&G)$4G}4(C?WlTz9WMxKM?Dm7XB4xA~U`qzea$i=Jum!>V7JHvjS+N2RsrWGrfSqo5X$)&G$=DMBT)z z-e*=OhU$O#)*;)^#E5dt=7Y|(^`g{#Ob9LQq&Z`m5Z$?E<`<#gBaXcrlk^P4p~Dqe zm0Q>_pHMsH_%w$3$0+mth?iSvhD2L}*_6ib578qjPFGu=O2=CP)l(%wfH~+iOYGWn zCD~IFC|sCPBpXIlS~ zg}TUj_WLuusL=6o!q#oLTMPTYZM_(g0SBf&r7MwZ-b?~*L5Y-^5>G83ekpC81y(En zZj%chN7Zba&Sm z+{0Of2V6Na^Q9XMAHGA!NDz$oJG{HEHSKiOA`FAyr*KE7=R-K-y6jVaxzVW62bk~D z4)w9bX=7Ea*~d2(O$4E7U@bztSA`q8 z>7`YFiWDo=;uUj@&4JaCy(P?G1DY6Em&_u0MjuB>aQ1sRK;bt&&TUe>P z%IRBr>WD{)&M)cJ9uPxDmz)SCivus;WTbcnNfBP?J(GSlc8}g{=%->MP(UtLE6I4Y zfR^U+^b?5*t5yie7>Pz&8Et5(ly?fdZ88Pkj=0u8@pDS4HdAv%@=1HclxKl(toDzR z5FUh91}Ut%x|c}|kjh%9uY|AhJoyQh$tywFJD9+CPeSd*=)bl)I3qU6i(Hu3q2DCO z&x2Wk6pMLo9wglAfvss~9E(Pw)=I!5D9k}PPLBTchVNEln9w9L)0eMZcp=_T%f&S@wXOUY27eI2{IZ-}-Wo#OI2zd#ME4BB*hPge2Ld zzSWPklfj#x0-xoaOKsJ1fnN5Lf{*$1SE5wl!PvS!Eqsn(ATz4LV|O|J`e4|KWZ+jm zUY_yNZdb97n+>$fYvKOvzq%?x`n*Sq#Jkipcg=Fh*X!aN=}Hkguj{-Bp}jrs!^=mP z<1l3uf^;a`qY5uWt&1eZn1oa7OSa4Lh7P-?-c&+y9ksf-MB!J+w2;;)6Zn;+Is+?7 ze;z;RWBK&66;ABv!>E+Vw8dyUrgJw@9Jp0o%+S>!%2>)eR8OW`vT$V0d__ID8q{|| zE}=!0Nb8n^%O6XMLIykfA$Gd~y@>R%cq+RTJ;Txq!2yC7=5+1)ozk%#p4PHYd4WQkpScUOTOs>NVB_&OFhd4uhXKsB#L+Bj)vrL>p`Q5>n}Vq8Vi8^Xq0x@^_^4f+7cKjn(-&U zP(^E=os#Q#>Gt{2hwQdib)WHMgDm<6f-;Z8T9@k2D~mXbs6{3?nZg13Sl;a((zzM# zKD?1@cj-ln{=@G~?nTGieAfaNbPB*%%vNS1?Pw6|IKnV|HEq819W?V4f}APd5@d!M zb-Llw&T{wVumsO7!rjyEE)6Sh?X=V8{X%|+*B#;$gtUk_cAxIh7guvJwl|U?iCeE& z2D25`dX}lc7joPaR+c1{Q-{I!);ylL9ias92jQ`ogzXGGYYVVHoDz1yqdjGHudjlX zpM7#?Zz6tGlQ5iVE=e)sI@y_|2AA)pXWeb|M{Z0`oe1Wk79a1EEu>2~pH^cuA@n3Bg^tedaOdhEJt&GpW%_`Kc3<}j8Dot4N9cZ43%zOrC;WyF5y{YYPe zdj;Sv?^+2DU}wTQ*Id-DP5@To=%#oV3*xSMX0v0y14<&fmfzQ8c6eb=q@7kCwVUb6 zv{buxPuukx-}Q{8Y%5y@V%$^bx-!l1c9}P&djk$xcUxt77OkdQ%=#Q%7!6_MtR|1{ zDJEX&oECU={K}+ZOgZy}W+=XO-CP{VD-=X>c8^K-h-Wx(yM;X73OG($6BKYU7zcHj z-t(`2Fj1c4#??^1qm+MR4$6AAarn)00ap%_(ct2L{^3l|<1qNX*(El9c*3Qf51PUW z5IZcb17jN}SNmRnp+PQpDk|*LOAeQeX%2hkg9;Mt*@2lDpK{Tp+mThFWB%l98fVq< zGtfeO=66*omAVvC(&%4ehA}s!nlf?n;&U-fRppY-*@Q>3l5!tD!raS}Wpd!gcy}w- zI=J$P3Asp88j)=)o|73s`cueE=A77|N z>8&p-FFxS6IV;mE2d^rhe3h-m>DJa&mStM+9=|!r_9Q+gP1ccaP?aa;CJlDCD0i;$ zQT(FYdAv`_;>Y>!j2tMpCNBA2G*ueY>GVWfM1!w1enNV9q5OR9z)ZzEkER)C`-1!5 z`U-MG*FUC8?J$&&yr536>O^0=B}1uV_&#dPr;cuOzQdcAN4a!KIU+=Gp5Mvq4QJL` za_wL_POe8@m;Gyum^lj*fsDVeddZSal@z{MD2gHa8_Y?_FBO z>LR!+$?dTZV%YjJX4YLIc7CdbfjlaAW!Jz8idfT5L& zHe7J+?5z87la*&h6QdUV36vBwg!;y6eA@N#<>@RVnO2fABYRctLY*t{Gd36OgRZ?S zS4mucKizzHUetE+#$>jgYFPNL8=)16q><9bshZXwLZLHQ(v^|4yD#tmbb{zTMI|Ya zL$tG)%^GYp{}k|lXd8F=fqKT(ruqlX|%+tf(<)EgJX<%YgINg2clw(G* zNoS*wsZ@(F(7M~i>W)!ewr(+18`Gtm>UHj`6{p2qvhK;gGxqIy-HTYW!ed{_sFXT3 z5#@=BYKCH>ipMqS@P{kHSAgt3#El6ZfUd@r5-NvM+eQwB`vS7<@ZMBMS-!iW_~PLx z6CA#5>2GxtDON?v%RPbsn^FlJR5znv^uhv{)YrK!>Jpb+$6mZ4RhKQ?5#$VLI>8?JU4 zX*n~@5F5Gh^tEfS-Ilv*$0yY)3Hx04XiKK2iI~Loh-RA^K^e;O?jM?zmhD!m@z*NF zOfmM^0XNEVZ{&qMtlfMWQC(e=bVOLdO1W3LbK)p@ta8g|q43Dwwz^DDv($ zF4kNro)shANoSGvmfMjk8QDHM>VrF`zqQs`xAr7HV#;ZoNBY&#&Yvy{WHL$!$zI^( z5~-3?yRrD7uw?{$-ybsOGZJPjeFJ3Ggiwl=DMkF7H@oX|*OL-Q4q&TDIW*+z#>!`E zW`T!WV&Z-u-@RKQ?Fb8I^_0rc3a(0P=i;=DnrZprWY$U@CEvJt4%L%V%4%+ti4WB= zD))i#;~ijhx<_-5@XVHHq6gCh>{Ed+WE7nRQ{(=q2d3o*v~jiRhGi&fj@300Qf8g8 zOrkhQr<{2!&Um^+x{as5#Evki;Q*F|K7i7nds%|QsWF_b+?(Eg*_t$Za@yA?S zm^`y?zsFEyC8S*MA+*m*B^>?QRPaUrK44Hc$F6A?H^rCk(HxZMmb~h|?JUCfWkFha zqI~RT)3*a3F*qrhcAd^i>2NXF^6oA2Rix>#Y@8r@U*z)6+n&a(ZRC8S*& z>RZ)jkwC@+Fln88m=y;5*Z_hfTROc_PWvm=Du-xYVplvI9{ z=GK~&kJ$rjDrlFMur%>Be)nEVI!-2W%`u0QVmP1*_}-fvN^?WaMr9|rd}CPEyIa5o z6Ki>Wy)%CIbJYY?*XH~`K-WC>To2We2Uo}gPy_mCk zTsh_1+;(=y9MWlfGu{5Ej#8?fwxnve^N!LF`_46p$R~GmIyM*6s;+8((cnwoVzGl$ zP>7Senq@{qc*v?fZMO{6JI$3^<>!UXXPT544rt%!kEJAUpI(`8SZ51P)(NgrvfuqIFt)9iL}s`w6K7j~ zX@Jk+$NACJnIu!4;x>KUK|uRjXaXXR*CkLLS6354k8ufc%p&+JEioNO))}%h+Mv?wyp}gW+!_b_aT}jnBf+xUT;Bd0(xZ}KF+3yZ&9{|=*hgij;HT$#7jBc&Ds!8 zNnNh$SSs5IWJAv$7{|G>cQPyLY(Vk?r>B>sIa3GuT0V`8Z zT6=Y{^2m)%;g47hNv2{6250t6Mq1_uj%E46Ava;Nnx56JS8kdoI;tl@qY!fBP{pb0 zy^bx3JoVSp6`w&< zb-NG8hzW&f9h5rPN->c}F0ndKHNqdck=s>^>=0!wA!OkBk;C{YA^hWqOQRlc?;849 zS3+S^1oJ8Th>(lvHRgp(Y3+*dYPDmr!h2(dM;ZSP#_2t4=uj{4tXKGwED4pBaNha8 z+U=X^NwkmXlo{QOk>F#QSx^{4&&2E+_8e<4g)D{ek1WNOaMUMJVWmG)PQpzSIyDBd z#V!KTyZrQ|L)~iNsY}v3&rLUT7G#FH=C@Eofz+%#!Fehq|Fq8~`)MJ2=Olx@&&Zqhji#(N)^Gj&nJ<-7Cbc1tFAwm@kY+Uq6*hePA~ z*8^$p8r8*t4{e3lv*y^oV=6Mb?fFVkdDE}j;sh(2BuwrMapfH03}cq@S5Y=fs`Vty z%RFzKwxNT6lMW|9Wz$WR+@U)&jpCqbC&6eitpN>Q23zFO=d;s1a|Gzac=p_$ zPnR(dSjr|AT=YEs(!=$^OyG}@ur>0_b)GSp(9L`1uA17Fik)r{?#Q3k)|#ji$QHOO z$3KQw`b5CC8c4Rri)h@z*VO8-9`%XJMi3^j#^y%6V;syzy_v6q6gH8p?l8T46zU4TwSzh4J*Mi)5Lw>B<)M4IRqA_8 zyelmkBYfEs4h83@ds#kf4(d(S+op|Ec-G3rs&ev* zon_lV^l;~!Ujx&Sv6t^tiC3;{rO)n%Fx{QeU2+*o2WM122F((NU^1?4XX@@IbotU~ z`6}?=|NQouyW`#JZYvicB&fsjA6zPD7*eH^edYBU&zf9Ih2fDtJKXJsbHp_=%ysrZ z=Rl(L1)|RgMlnm#tofN6q7vgux~!;M;-Yca2peY#Ty&Hi)RZ49ABx4?z??lYin05G z*qo?__|(07NqEil%Fwagw3!mZptGhk9UmC;yU=Afoou(nZ|+Qr?Adf@kmgXvQ)p#> z(43s%4=l~qthfs$qd|S`74D;cXCKtNu1DDmxkyp!LZ?Y<@=Yc2q`Mf_Dwq0-T3^$0 z`IKIUxCDc=$r;DzqvHE}cnd15;tT`pX&BYK`T6x;du3T@lAAYcJ_K^0!K9gOY4kPp zr8?7BWdx(FQ&|(e-j|A@m0iuws))VxZqf_Zt8dk=)c|4R%XHuN>O{3&VOJ}SuywAi zlH0DW>GVPCOB;b%cOMVSnPRrH2|N6vSp}|wON3=a^}QFyWj8y~w^Yb^yI#0}s-)cW zwZPCybQYmyALPEWi}|_^%(xmBN+V~%Y0gPUR=HpP>>z@wc=WCuR@@Ux4j^7M&36%* zzLZVm$Ws})@=~^odxxm`v)wk3s`aB1Tb64|XKoNmr)q1Q54X*3>gIiAAogqAi0ApO$WQC7?c^+=OLT|k zcM-GK^GzYXGCjdHbC-3O(+DtxY#?j5{K+hp}n;>ftQN z15|jUIm{5MyIUN5b2onoNDGaYa>l4f4QE;~sZw|>(|kGAZsk}sW5y0X5qnrF|A^@m zKOd)+!Q=XkhGleVV#(|${Y34aAG;OXkFB>jr%9*5<(kK-7j8UkHXW{^brgva!SF zmsJC|!bgfqRUNk^KB6ubLb@_VtTI&CGU_S+#Ek8M(v7cNWxwC>SLK>9D8J*{QjV23 zC-ump5To^skAIN1BLtNYcP|m^%lC(?{kiYON2$0=GXDR`}{hr~e1*Eyh#C_}6~BQ&ynlse8K zWiChMTuxLN=Ke}*pm0ffUL(fS9=;$*+y1gbYK$GCozqEPXPj=TbIjIH?*8FNW4{Pyt2n?6+#t{W9I*}%GQkU>8*jt(7F_Z0wS zefxK%j+Q^juMQHl;qsUeublbB>rH8TA7jiOrwUaY{#=~~N5rISHeIrlLMBw!W4Fg^ zId-8-$E8c58PzR}rU9P{m$`vl5XJ)YO1VawQ0=$0d}xbQKl=~nmPYHifxRSu?z&@V zcW$t59wI;|r-!M1fZ#I(!r+(0q|~iDHY6cBMOD1@5f1}{#RQx0?I=NHZCEL?6R33K z`#vO4YY~JI7|d#!=%YE!d-T3v-zmQ(xM6_5Q|={%QrXshVG`qKf6Mn{xB8hG2u~;1 z8+J4;ofX}_q@1Q7!QyQ@v(S<|bSIAOF0FU(i7m)7)`72SiH;@UhH-RCq~6!kceOB}A^M_jn@D(Yo*3}SVwss4G`UM^0x%L? z3PBPws!{TNZrVLojP7=2mzT`lLXv_(H&YXzd!<+}l0_hI9HH##9)@O%@yvFFLH7*_nrEm$5bgB4Rn;xXIUAAwrC zKi#~l;E>?sDWYpz-#Nroka$cvzEUvJn`7(Dt;2RQ-WT#uF2&^MaeQr-^-H^B5>+23 z)}Lob?w@@>3iiug1&zkeHn9zM!wYeWhZ##kMNq@MiM}!WU&JUL^PXnkuYPhAa^27N zd&V*f>wY-V*_ql8+ZscMc6{b!Tj=Gx++iHYd~W&H^ryrhXcv??qIw#V31{N#Z|yE= z7O8l)r&p}=qCR3*P%36Sc42F#SnqV#P1VYHyqu4l|pW7Of|$f8m1cv+lG zoRrYAm&wv1zm7IR5d7B1(Epdp7(v&;L)^EV4gGt~B6 z8&ik9PtbAovU}_@7h5z%dHK@b4c#5immyg$AIv#%<;L4{r|xXALe?)Tcse2)%^IR) z)ixWCb>xnvI~WAAa7AF=HYIDfpstVWyUHzmE_|ZbX!O&`9Xxzz-Feq#!>4C_f7N=$ z%-@fAU=;?fv}Vc%$M>#?`o&@IL{t1|KxuC?VO@m-hp4Wt07OKR&E8#9$h+`^#=R9M zABo-otq^>UeZ&@PbL{=<_YeZ4GLbxtUpH%EluV!iK8|4nQz6FciUK&p#bogtfrhRt zdX7tZJusS`mo9B+%kkq=r1Db9!nz%$LHpM9KlNJ(82{FDes*d32(Aq36*k2Pi;HAR! ztYIr3D+5yy9z`UC`|jVh0(Nl@CIGO3@rie0*v^=HYT`_CSx+$@0V}dU@$@v*&y8Ko z87^q__61(9TR0t6B!ROPCDR2uyo$T;GF@*eWm%dDx-ClP-nxq&{9IL)%0LP%9H~AW!Hg( zV;?cyQC{dFe8F4GDA(`odm;Ck#Ue>i37EL~JRhu`+A0SoOy0efB>w86W0}b<+6t-f z^@2mrnC2-;N@jfVk)}>Ea+EYL%(L{IrZ}fAnwE*`;rtl_sl&r zHtcy>zzyl%;j6_!fEAVOPK)1MjOPC|eD__?A@Qu|Y~2>;wu}#xuG@9LOWO#6f#N6(~&q$8*a17L{x!yE=PQv>!)$q!U z9>PqAK>0$c%OkK9rlke!uxnKn&R_|(W@U2{%C2}9yn#?KczOj~_E z;InUgVd_~WM3~(R$;LS~-qdjE+WBz^I8F5Jk^5I7PlnEOR3K7nH&r3I_!{uG9NiUD zW6tJu&mY4%Y;}|BhbiKo+2t+DHIT{DPmAF&3U)7APdXR#Z;vFKVs@t#NprAY-HG($ zeoA=_`5`tMEU;5y(ST352&q?Lhop>YJk}slhabqtgFh}ttR;vks1SUXC&e?MT}bz8~I_N6Po43B>Oy`8NG7;?^Y#fj0=k{AxV5{sQhz?BZQA28clUBq^51hjPk!N%UE#fzluyyxLb zLlu0TTi-wJD2koRZF0VR!5v{#SMAK-*iy@FIWHbw=bj@0(VAbpfuA~DhL-_OR?YX; zTN^z)n6{SGB=4^NHNFSg(AEQx)My)K)$uN$j^JN$g#x1^3wGZ+o~C>7-{d#i^{9{i zq-typ9OE`xI3DvA{6+yulg}HiXEarWiNJhP23PjpW5?<-pC3ZG|4Zzh-8Gvwu!;@x z%$<|cZ&Sr(zT2;nTInrNA3x!!`dBgG{mmo5<*b2mO2C9*o^k4@UVFuR!ia!DVBrcm zk1#>V$kXE-Rpa~MA{oR8Q$B}iU2&)t=#EX(TWU}QC&r#v3+jtcHet;zF#SZFsglN# z+vL~D(uiX+l~HmBOxkJdzC7)CvQvdSRfinhxF{R&QbX1F2G=63IU+)YV9R&~@#I#Z z+zte5OE?8;U+fQAXj6Afj7v;QPTQSIugwf++V{9s8wJB5r zgWM9T=Bp6%1S`>3bvgPP%anp4n_+3IFi|V}h+v=y&1b@EiO?LM9{J>Wiwc8!J^E`1 zF@mXS?c4KOXDytHb)mK=4wGb{x=Hndo8+)+fWnk0<6r}lhW5-c^DlE!BLe7s4}z1x zG>Nc7cA72?Q>{$+Um(HCp2oZ3fONDKPR`lXr?me^APYy+(XnMn$IC2_Iht+LqdCo z)%~AV@q+6jX~=DvI{CM={WDW|dRslq_o9^(uVyqidUDCiP_u@l7LamKDYEWY6M8G( z4Nn)g?w$Qr=~nA~eLWbIF_$rPkr(ky?|cjd5c}1-CxNiXx~K8-K&(e7{=Kyxrmxhl z`9K0sGNCz*;XYG#TqFBOjDB&E^YOxq`PYL_)Z7(D)T!}YUsTPxeqqrTqh+bDrS$%k zl67)Vr-nTv)J^L|vlyL>#Jq4)nATGXR4+U+BsNqSJ?Diq zE;y9sB_-mzbNZY};lOlJkfGRkqngQm$Y4AJDz0^Ng9?nwWuKc8XR&h+Yh?qhRNtt2 z+PTWbSTKzbcW^kh3>@5Y$0C#=mdRzHpUOdskg66t!KIHba`;BP<{$xQ+G}~Az^2r3 z=iI>wmnU9RPioJ@O1Mog+-x>l34OFOd$>I~BF@e>39Fu*;my1&kL0GeL6)-_>x zd*5%8{F$Quq(sk!)SdPv`$Q|Gye?o7jCl&^j$O zwP)A+=6Rs&>ntz$Mxje4e$@wb47uBQJi<5ygJgeG?0-uWZS^a@RVu!aQb+%DyA`>Fn#hgJ^LyL@{u{URn$ zE_DyOG1kSe*0=xjKxGwVz)vnkY|<#pyJhGWh}HWM?M6NFZf?>GK>>fbhl=jbsR zItOQq48dO&!@oz*r%qE5&DB*^YpDpL0_sRrtY2D8n34={B4*0 z^-p{N?9*I;Gd>dHkAJwvW<+j;NfPN_D6Bq2nF-J(+7kL2wM>7T{9ph4 zgoP0lFl9)sAw+GO5NBiiKCV7OI!}qsU?J_`7WiEw{v{BZyRb;wZ}oL+S^j$CzXkmB zr%yVdo%`*Ro~%B@a@xnI+&#}ruD(bzJeOdEdrH&yt}y|3+5}dF`?sv2uRL$SBAG;0 zh;16V%@8XeoPLi3+fHe&c1SzaYyIb>J);C==(5d<-S$bY{z9JT`mk-Lt_O`CRF+{d zuGYw4-Nlv>>Wohd*tpW>7IHq_WA+Prl?Q(lf(OqRSG+y2)`OqGQr&0*tCZ_W-wwbN9Si*5GOqE&v_i&7Ne^O{r3O~7Fi(8!$&RA{ zsc5|U?$Xnpf=RSkaR@Mnmx~}$sbLqavazZ5H1*u#LVkm})xlSG0*F-O8vk=GqlUed z=GoCtAMnQ2{V|KjRa3l#r70IxBNu{0B8W@JqFh;(BYIUPBhph zu^WX3$jV4#e9E(mGkaPS?JtQh_ltI7NQHAkVM`SynWC?%@|^MKYuo3Xc-kw2Q#*|t zmLnLCIu`$Vz?GxkLYOORw0N)1qB1rK`<|FqrIzWB6m` z*x$e|V2C!6`|F7P?cB(`*v?l^mtw^3K`TOkb+Av4ZVs5K@X)ZeeRK+#%hMa88&iuy zre#7rWe#jkkhk=Cy0Pt%umJK)V@u4I9=GYim9|tH-oD|h;>!!Ch^`T=4Ps$9uOh1s zr`O54xral8h@LktX1Z~QB;}iiK^1V0`$HGe6n(?-4!fvitq_PmMv`o$5aABt0>56t$tue`(S_3gDwA|{V(CEkq1C7&6ZSwsyl zDjA0bg}tB+dod)56Cxhk%yE9{Dgm>zVKuX1bZ8OQKgY+jmB)kg_Q}V86Y#(OxpgzU zBb|oxJ8AbKTBQ#jB0qyNOLX))LLaOkApWr|+YOzubu4Pnj*(X%g5O|@V~rX&B(; z5C3}azqbBI&|DQVyVtZjtr-~imgweKzi&>_C5}4I|LCC+^dYBCe`hZdRE>LPH(38+ zuP7SH!YPu;kHEA7;t4f7(k23cKG6};c05YkvLVX5IZ?F|v_da$U8rz18Qj!|vVk_CGe;@9=&AgRfqMy&CV3k}P5ixvYpqNs-f2QD>^}NlVvQO)S#X}^g-P& zn}T1I;_CPwE3J@Tjw{X(7H~@+?{X~_8g|nl6(%-Ich4Ws4$>r#7z^zDlBu6j+Q=N| zW>MX*REIx)=#QhR`~wshbJttRwe*(kg~$?yoG=ko2EZ8`DKq5Yz$pRay~TY&+DPz! z+uq(}w6<)<7;EU<^%ge&dYCBYcA3L2!g=gGkgLUn*+PB)t!(rYtbK}hb}cY|4rydT zDQ0b^OYXhCD08>lux7Ks#n}$EN8gT4aC$QA_;k#L%=Xd88at*%Vi&7X=+j0;&vyB= zzi?gd6Ny(IidWJ#R}f#47Vpk2jn>r&VM&P$IS7YDQ=i#_ItvqTs~uT2oGnC+ zvazgj>HA$ZXxDFcnt>0Tk7zX&@~-4AN1Ew1vYKj!y-LacYFsVXxKt7(7=|mb!qXIq z&TQktpLRK;0Niu83)?u?QX^{Xna+ARL;Og4ysK~&4o8X;6Smxbkeb!DxHdnNi%Ga^D80>R_R4NMwaF z7~V_<>ktJyDQT7y=%gjiV{zcK3R7~Gx4i2vhk6f$dZs;D`;mN zDtodvYxJSpp`;tROlV4+9~oH{po(myWN9LFwQnta)|SS(#h&yC3$~-qZl~7nEO90G zB=HwmX$N!qXogI3bbgDFvO}o%wL3&3UNKr%Ba$qRE*U>WBxyAY<{MA(F3$K=;oX|= zVPDvx1MS-d+be=oshx2Fd+*F>Ip7JBDOnjD{MzLf`8f-g?%H=aTT(us)-fDkq8f=N zSM`{-d%9DuXD+`?;?KnfFCm-BB3Txif!59JrPPrS@<@CtzDsSZ;FmwO*#G20`$3oD zl{rUpFK$lC6|SEJqRDpX&vY;oaGk(^QVsR~Jwc05+@`yG4p{pfdLf^e4aC+YF}vaK z6&#-np&SdDFUQniO(8sZHfh>mo%Q(QHT3P z3P{`V_Zv4j_;Q_aY8u9$D`in~>9Y?zMUfR9ao=ZKkxa;;4xw%PqR!7YM6V3+|FH17 zm^)fqbckuzLTh(PO?6(odd5z+Znwqa5BND1NgSpI&C;2wyMXn}!G$I37nY(9YSK9r zkF~T)RU%miQuXj96%wv~R58<++VMAXeeFvUMKz-@DcSVib0*K8Xb|D}h(O%Vg6sGT z#%M8)lfZW0VRR19d=%?7W?H9bWplK6pg$O_xKPtkn_{$fv8DR*sN6#>FD=wUL^=CL$mv;tM~8Xg zE!J(=k_P426)cO~Ox0a?E4fnkro^%AIsFlMngvD05p>y5x0oWIveN}p7QnxvXG4A{ z&E8Mr9PW&P+W8t96N*bp^lmidNW;}8Ph|;d z)<|mSjJgPq><^pm!J*H=92OdOJ7U;Y?iL-p zt?-8=J-xs-(*Dsuvh7#F{<0K&Obp)JOE|s&r6w9^P2Ze>cyj15$9&`Jopbs3r~1)O zfTu&&gbJ(2D{^T}sP90`%nG?L^rnxZKakurA(IBIo?4=_fD3N#{_voamK%=3Nh{1FYy$wXA*P4PFd5pb%y1sI$3qMX(IHB zeHBx0Ecn`g^-xMedVfZ@uZ&671l3Y&Sp?LGeQ`bLd74$fk!|i7fqA3a5&UR~yUi;X z07sJ_5f#tA1^j#;#x%S)y`}gi$LYT!Z2enVmsKNpydn2-*A%~_VK(~;9Z0M+5+z;}n7l(Fi zum+vvu5mMaDy$F`-i%axM=5HgQ?mo238B`HP^}o>= znF^-l_sj?I=QrWd?p#B>E2qiKi{0;SXxn5gy`>N-?3NnJNz#j&(2OXw*r8V-+;IFi zSk+H8FC)bau>sTDA_0GD{hz{SSMj{a-2E8~EraaKIW*4Y!Ijz$JQ1eBy7nRE0^Ky| z8T85CncV^0bjbS13$Xf^-s^GKhMzI@pBW{#2)^X^ScEnOw>tCedc)+!qzp>e@7kxB zM|WoHr|1&Bjdr+8wT0r9$;f~o$@Wp7Gcn3xXkwauEX%k<45~eiFwAXTz|}q_m2H7} zKCEcBmpCC>I2JTTvu45?)8m-B`EMlg=QKN5^@~hVD=(qCTE?w*lqBOEoW-N+2x%)L z6J^My`ef3D3D@A%3|zR&KsX8KZCu89I({0ZFGAxALK`!W7*u2+_-d7{M*aqz-WMv3z2y{uAMxEpwoET zoJ9KSpG#pBK;&hLla3u)HWyUN=|FwZC}9b(`llzoW%ftq?TkLKcEzWcGBww+osX zY$y|?^#lt8d=wEP4VJojws&7aX44!3=ud=7&oSKJZgzhumJoHFo0IyTL&R!w5v{#3 z!X{YQEPtkO58B1q+bXN((Z&emXl?smwEjHAM+RRom+BgGqMK)!h;yw^J2Ul7+|b+- z9gUwgx~de5$Y(@RW$mO9zvarqTV6dH`1bRTB6=R#U3JU@i_nU23>U6r5~~bj>*i;7 z{Y{k~iT9>J}d#rgD62bei+Sw`E{m>aC4Jhy0S*F&}_A^Ee!Zs_q*a90a z4L`dNO{Zoj40nYwL}rC?-SN@=aXT|?bWc(9m4G6PgwggiKjJdDGPM{Bafx;fm)r5} zETi*bq-VUvu5udMxn}-$dG{WV5(eo+T|pHhzg1@&W23M!XgVtsj)W^9oXFrq9p5qp4*;z*^ ztpNwAH=_6mHg6)$tWZmnS59{svbpKjL9RdQ@8i+UU(Jf}@rGD+?E5g0;f5~!?e;fj zQtN(E1piPS%GR_?t2R{fS1jwV%OZR2w$G++rX+w{5J1RCdjq;`KquP+VnA++BLyMc zPV;fVCE_M`73Ra97w(Sd$74*$+7JL6jSEBHi|it(R;o@A)Xi&B zlhiQi0Pc^n_AJYRi+Q2hMpfHs`T7h-cj53yGJ5OnN!btV3g*+zc7zd9vo;IOh$2FC z3y~KH1sCwzy$`@^A+;WTAe8P$Zn<3DZw&JH%V5Z|%27b-WVb(>Bfsgzi|fy2G< zwM`*%(-zY*F3N}$r|#!%eil)@_|fgnk4K3XNu`CD)7p8+``P|b>_c-uQ736DqLYlb zTTwD)3G~Hp-(c-vUf&KnXThus;tK808ib4xrMl>R5vlswD$E^n9ct`5MDUKLJ`3t} zjVMd*jo6q!VLTPr#1dJ<$_jC<7o@aAfbCF1a{TpEi^BfdOL7@?G&>zP&e+W zZSyKdZ;<}QEc-_gHd2Nb_}_F!e66^@@7oeqxJSTL=!8-f_4gOOH=Tmi_c!RTU<4!u zd4VT%#k6>(B0ZA%2{f|XhU2Kk_dfeGZpJQeYD?C{Rb;wm(IPIC*-=ZlGyMrq*pRJh z9~eCVsk%R+oTh@#jgYh*5!C43y$V-6yCKw3W1vFtYIhbQRk8J^C_u}P)}Yl zm61YFYWm93ZPEEaaRnYF@ktNY{PayD{vm8B9>D{0qHu%J2ysBNW z6JaKG+{kZbq7ZktlYg;}WwhgTvxTtr2QnsqY)|qI?$*q^;jZ%m%p)!5^~+69{SFbM zZG6VV;OTorwy~_`>3>{L3uyl{&I>nQ$BCkIb~58Y#gqn}4ulRq+h{F(iTL&O47nv6 zj>VN~%)QY!rwm8Rcj=$?Httm*juDc>niEvY81J?uYnw_s&pz9URZDzH9D`oKMsS-x zjkCNx`Mqiu_~P2Wgq*=Cn6CFMU{dQa*C0=1vt^Wq8{^wvmxewyaY08&beV?TCmNQ5 zU27pcJlZQV(<5D5*WU7QpQv`amIC=MQLbUazhFR3Tf_>Zt@OTSXQjRcCP^F9M0maw zR1=2lcQ+*&BdSsE%LGi*1YL@HZFSxgRk*UVu?SlPrfyv58u-ku-)$b38 zp(XZ%2I|bfbmGr1Tp{M7&j{$6SjM6h^l{&I(PC#J3x1yX`@K#Ip9!H{_uny}*T58o zol%+jwxZ*%ocV0lwd9y}D=CN8cSP?hSbdMFya4r=~U*(ltB z^%p%|{d4PQ$!-egjuC%EjaL$V0+7i~Z&4Mj5;yS|jfwXI`ZWXZindNJkce?Cu(#X6 zUYk{%Op7ed7H>93_6dv|L0B#=OCb2Wfn}RUa)Awsrc#sjabMlEm!eUhD660{-HbfZfyE4AE|bX+&+%N=2=paSk$e>FzUGp!LalF2yL-B zB64c?YGLM_LmA;% zoTzqyh|e^(;tJ>OzjBDbKI(QpSlpdsG03fJxs5yF-gsJG{plLJtfyEF8*bf7Q(eFN zna?IBj8+=Q-)OQcJuqA)C+nL9S%*8+$$R(yE|h;RwoC;)_)MIk>>o*+pDK}WjP~9| zfiDYdU05NsJmMF>tY4?|?uX(ao2sd{-fItLJ>|8~9FelHKG}U#uu>GyTS_3Eo z5l<)I+P?bI$>4{@EyW8AGzK)^x;1N{ZI2+bw@^~(X7@WiN!S&imUz_O-b{;guVFmdv(QywUo=+Q)dqXL6yEV zEnfR8tF9|7?*AUzHCN>K5B~Si{@axJPe1+dph(zpSjMO9QIG!qZ#u;fsiu% zU9R5PICuL>#}u0#5p;B1=dNA8pazURtKRJ|Hv_gxBk&e<5V$~#@nslZ6{CXVoC!Vq zgoj*A4)1)t(Hb}*Ef$C3Ben~@y)&#J+G!-HmH9Ym_c`lG1*oxu`19(6@s0R77~vxx z`Gy6o`&I|6RneS#tXyDXnFtafTUkJ_-U_4>sHeMx@D+ZXL@2c|l-qara?YD%KtZJB zd}E^Jguz8WoK-T*)E(Ewyt-&*BF`~>T$jTsiq7=e@5I-ah+%Vpj;Y6j4NS)1O6;$F z8oTE ziuuGVgqavtJUhh|ZAJ5?A&^Q4$gBSWS*B4)JY@m_56Pk_!2oAkhll*1Z1vz9Tcm+y zYubA*yC#)}m0FHv`Q}&_J1hgM2v=YL!NyHFH+FVP*Rx z7S1I3Kz@5@0-RGb=!m%Anhh0Or8|$`>h_-kvh&Qb&U{F~j`o`pp?9Dqh#TJxzj#th14+cSB&a8AK>%K!#@I1hyh2T_2^SY_ zf5gp)z`UL^YsLf$quQH*fxr-cJpry{idsRoHM~miMwrCr z1Dmt4Qr%G1*bdD0`YqB)N4?DRC`YrnXD(o@G%HoJED(#3Y}JvDp-9;2B!*IQmBdFg zkxvUC2c1$U{FX0!vadS-8vlt2s7`z{(!{TxWahK)-s43jYJ7V?R5I%7XHt}AzNafm zEqoL~wSNz42>|%QXoFJ9avQXK{yH;&iO`{zIH8RC7tNyAZQd{##&UA^Fmg+nd$`+(^pzYNj%WMiXJPf0}kjfh!MomL@TCj z0)W5*@>st^Wu?^K77u@I;-rE>BQ#U}5Wu@ctI*1ev?>iLR#K~+1lrvGi?sogv$rs^ zx`Vku20IHKXYx*_>d(Vz%!bzS^D1+TU(VEJLSu$%MtAD7zcgm;As4Q=8zJAkz6|I1 zi`tzf0cB81BcV>!bc&Yd28>Qx#Ou*)F(;1ovkDh_Y2>I49d909uqYN>Yhze=0*5vt zQ-x`)NKXhZ^PNbjVk zk6%%n@&kia^=`)LM4+^6eW||{!xYIAZdraNtP!Zkp{VTW02yCAgl*&7UCvq!*1u1&Nag)5N2j55 zvvJ43bY|Auc1X^lK0QBxK1DIH`Fdxy+@H)f_D?NzW6;CIoJ-91u?~$Vj-Tc7j_q4A zA-RlLx5+e7%mb5DA%5G&#E3GIzaK7PRk;EyCb43G9;7gMd*x2ZZ~eJIdWBB1 z0gh9P-_{7U#L(_(RUy>a6Vv5HQy?YE2F868ZCL{~YP?%9txJkFck;VOvGGq5-A{!V zwYpQu`go(Nm&hKC(HpKQ!*XhUnlsme1+_(prxLg|nyqJ=l0c#59Q9pbq{VWfpU^vQ zg?VlAH1stHjUP>HedsT%;2IA_i2nTowpb~pnLvJ{eIhel0bJq)u4u19g)k966Y}nr z+3e!n34jh`#&1!pi)eWcKx7Yf&=97yl2Ph$Q>UsU!EY!}hr;}B?f%`x`DfIMXFF}V zfp^xy@@vesOd4j`gdFqCWc@n0G;Qc>ZokBiP@wY=s*ye{j4nm&lDNazK9v2JH>X6b zyJhCi2`Z=!e4{letC@RGpwUED#>$?_*ld1`>1(cNo(V#jxry+PUUahOsm% z>d9ze@?|=R4fWJl9C(f1m@1NFfx~>nf(d*kAGUpG+RA%HL%aRhj1tb?3%?L=L&fUCp!0mqa34sNuH6Px`x!npY!(6Kj)<%vLG3(- zElw71tNZ8N)i(f1!TJ8Ep>Vjj8A85PrEA?6+42CjFXw9@wRB3n&I^E?3h<2HnAlw z-^{XIG;JI`s`29KhX)%?#T94HkT;tYB|esgl6q=_!0GqPCKQuH+y6wI(eA*<>nDYO zW_DWM56_7JQ+IyPBhDqWQWE$E89)Valqi9?!6oTr`;H-J_#=zJ=7hao{*|H#BNp+C z!miM4PaPO|P*9=zz=E1~%q*$oZY$V$h8POARv%RchpCLtXgtg{rn0}xyXkwf-BFbrP&6pErP>XK)7r#(@y{jI3#K>Ed+ zqC!a7&5paT5Cif^7#%3DXfX>-^$6Sb%FJ-f>Y)r7c~9Wv=HLBe1XpK5oP{oCCd@mJ ze-cqpl4yKY@1p7+_l_56M#+mD)C>w2TX%|naTg=nb;Tq~%49~LVv)tcua@@ylFy^C z%*&M_D9@yAs9xyq=*R00*HjAIdAE)y4C!v*+VJNV-9dyR+_NwZ>M1&Hb$Fn-#q7Sg zd%0N=@%jKVT+A+m*4H8Q`{Fd5v8$_ZtDk2KUBI6tHqV~2@dsos@Ac@~9e#u`h-SRGWvQ2Y#hN@l)+w$fUzsp98J^ql zEQy}c8}Xq5>2+8`{>8qJ)$xuQK?WVGg$u53ELUM>kgop6R$66HK98le4 zZaZ}4qiBS^H>0;_FIV!~~6vdgVjyy^M1j?q`A;)=kl`!W+txw|61{5lED@DR$KUTSd_{7J9FvAbTi z#D#`Lz~+%Fn*(U%dZy%Hx4uA)DETz46XA4dsGqr^M441@Ef^kGzxx2x<9t;L&x6zo z>=UQqU?$dm&`K<>8-)pqfTG4>KaCBFcE>~GmyzLzTyf{Go#I}_ONu;9tD6(;GT`rq zF7;)06;4QCxIEiw9v}YG;Q%+a!5=6&rHPEXh!A^l35m8PyqLpV*&O-j7vMOy0i){k z;r6`_Eh8!oqIe3?u#_}l?4Y7uB zou7(W*{3i`ucPp(>2Uk*Kd}ryO^4?Hr^G>L79n?8JUh8r#}f6I&* z2$Jnre{Us{eed{c2sY&oMo>wdsg68fj;Y4w@Y7>LBQe=bq%mkURI3^OswnzvSzWkz zOG@4_zv$j^4+XXRb~FkX@dekZKn+@VNh|;WB9OGmM9tgtXZ9+cJqe{3s=@?7aq}UE zTi^nWgVSiJo@Z+=T?JQbc(U)PpJ>;cUbpB#ckX(_DmC2;ADY1dsoN`p+^zp{bPir^ z(`dt4f^c*I5+5S+j5I?It!BV(r1eyL@UwB~n_;H24zv3%w3NHj2#NfNH`+Uj_XEl5 zDt?9xPnJv^a55>Bbz{BqAs232&QU5k-0$4^aGVIHmx9ucr*kX3_(-x=NhJMPm;d5hV!3&?^{e`ZLNurCIOG2VT+E9pMD4M`^6E#{c&XU zb5(UOc&8kMNp$)#*bt2qyhuW>R+0mQluwf>+F;$cH`Nhi4wfyBhTNB&ygmiB6PT*a zVPeSU1Br3naSU&1;9#q8lB7JG8x=%zdV!JL!YaF_Aq|;n#tAoVx`RMwLnk`q@FA}1^sHt2~2t)zxzZ)^r z{1sozJa5#I(aq!h@KqFC8XkhtY3BbSHx?c2@5>obl_MunK&SlWiKE zFOI_fnoaAXD(0Y|N~baYpx}*}IF0CMl_mJ3$r(nAja9HJK4Q zp8AN28C1D1hdt2Kgz|GN#z_qT`$k(pyB8xf66ivwYpxtz{gV3OBU7}|nqw#82!fk; zCSQHv-Pjk6je5{XnED-AJoESzqn2H8p%&EVj{vK84gMKF)RE`ek7{Wo;z5D7^)J$) zfq_)#R!d#eBxonxDZ`3Z2z2P=%)zZUi_`7XLfio>|57`Lb=IVT50?qVmFzOZtz{7V z)M^fd$MtqW%p*z`dX`?}Jay+Gr^ZISnEcGAlO*VPv$P(|@>be*j&edP^An;}`LS4+3u8yYn~&L5=T^_J9NziqtOqu^ocRD>N1|IH zL?d&L1}D~KfydS0kJzRz`W@?@#-~<-ke-9!TZXp z3`tW=qJ!6n+8v|;DlC)+BHW9hK2xyWJa64fM4|ZRtcjIgx@DG-fi1eK$1aQT87EwS zneqH`l97S$7URb+*`FxC4o9}W*>&qhpn<1%pg|As>n$2B$9ZZoehuh?=n_KoL ztvI?~A2?4kl}N1|Zd`OL(n!&{)^LD_8_xb0rq|^?H7@#w5h}Y2U{!sMOAcm=K@-#p zR>6>fv7Ck@*|D;urSFdTXt{tOc7&n6t&?u!uDaA1sW<{}MOu~vGRhmpl$8T)-S~=_j#38($o=!kc+k=R z@`Kxn`cS$t5Bhaq!IZ=)W=j`44UItMSz{Z{6jv2QxN}tT?HfF@?Pg9r4CgC^Uzkaj z78V&CS(8t0T}XCBq`Z-~8&f8unX!mp90R3qWpB|a& z6Zlw;yS)2+=<$!At;59~PMj|)Dw2}Ddj5(beFveb>Zn5KYq;Ol?$uQkUEcB3G3eG4 zMHK$jk*Pn2?T(X?WyqnIu<8GD+)-0c8ObUwR&hnHrGR-1;@^$HV;w4iIU2)mA z7a+}+*W^4)J!Lsn=={>gpb4$}c@NHD&Gb96z=NsYz@eIFGobkD`m4ZwOC%vlr|C~Y zGjI>wT1eq8($BFzz$NWgaFScL;l~N?ZQXt(?oM0hnPEgLmrZ)A%UShq)U^W=VxpIo z&vBQJAIQ6WTq~0g)bol@?WYF6b9pnh?gg|%M_2ANp;UF*3c^5-IK+G=()QkcFY z=LZxGXPB(tb{b{zao9xV!82uxXF*L|y*bicC4Q{weHVz1DVKa$W63S4l_mxc%K*V84W#HI?uG^VQ4tp1zFM z>zp7$(6(KDBA8lEU%zyuz;#iixw$!Z^A6tcs%-;6myJ7;_U*du8$SjkcEdxTLIUP3 zm(G~h#z0QN|#^datI2>4mHNd1LVv2x`!;! zEL2mJ$D0sg&sWz5l%Ux8qj?sni*wxmmdJw)5%7}|?4O$2r%_hxwWiR_e&H>xF19w0 z>(T4=qSv9;yG%{MCzY+FNMGyt;#l{cJL#?Y@X;Z@j{5rgk(Xw)_+Jlu9ec_+ zsd#17)NBYR;?VK@0Q3d3Bj1$JewGHBLy5AqVAInCOBx5R1$z+`#E6v_f1AkXEpJH! zu7_VW(9bpRYvg>E%y0YMrIH<;GFGZyZ0 z>B}a$eg{ED+wa4s%?vdjb$$msEpF3sQQ;<&u&?RB2JwrIqrIWqYWqB>nf~XM0o)N_ z(Y>;}XolLBk?WjQzC0*@()?@8@h{q44e!&p-qr4yTN&Nkuy8Q%5_`ZP#u*B(ra|AT zX={gp`qLQ9+dA&n7^g5a(OX#_-^%+ph5F|a`}hAWo$_=Ck1OEGQor9J4{_tqueode z{qq_ogvB=QUfIzK`~f>t&`&DC+iP5=E^6OPF#Pu~|Dn47_=scM))LgA{s>0vLoUux zq-+ajok-qwJDoT<6Xrxb${g$xh2=V1tq}WvbVv+lA!&f^SYrM=;o@rAz{>ALa zCp&O5=8#xv1hL6^SDBmqjM@P4^ zQj%h1@fV-^NayBOwceG=2`eRWR21Un&UU_7<%^%T%eJ}miAy^($0l61KWL=l=>Gj)li#Ui8Hw=?frLi%#Y$Tmes^`TZ7ujva0 zOU=zpQ~R~RP%(P9A)uKkef6<_$s6=z#SOY^pdw?|e-%y%3!p>a9Psg5)d7*|c9-GN zU6*C>{;&sU(3uN=g57_5Ej*f@wtnR8K&rMSoojWgdSrrOXm0`w^I>3ve!H3C`5T_# zUtzH-rWi||U^;Y$>EqGW2Xs4N*VPso)GeOP-H-Zi)U&PbI9*le(e2GU#?rl$PnoB! zrOtj0bdk(EW@J~v`MSmDwX7%O!W2>d-m4ERaqHEVoD=jNYHuZH*WjvgekhZ-!J^Dd zmQM7zsh8Wo=JeaEWimhvEk``UM)uqhUw!PYy^LRll{t4_Wymo&zhnA$mN=Qeyu_Cr z{}_-_-B#l2pxToCLeEn?+tXyWm#|caL~{Q&j(^Sad6G zp0+Gaj~HLlp>Nf$r~YdQzb%lb5sW(JI-O|SnU8BoJFSvA#xhdm`1AGFW%qaoH?F=> z4huX_xvL8#vF{Gx|0s<8IY3WIy`Ov1^Z&&?x!(Gf)iie0Hfh;$I?5Kww=VgUse=^_FO zl0ZOOAaoQ^I$~%^DAHT#Eg^8{Z>ld7@diO6u|PwS1qsTSJTV@a z$IZ?GCkZiTfi8W6;pU-jzus1Ng^If1(b}O6@{5lZ)qIj864b+qznuUy8xZdGk8KqRw)In<6!C4pXitl1ox2NkF({`j(jWSr#UJo7=}*9Np4EY#CC<9V%oe|_Y*>_gT?fwT3Kpky}e zl`9YVs*i#>P~ptCo09i?zg)sUbkxJHwq@n!;JSth*nBm%Gg*fo2L1BNdHx4d1|-UO zyFu1S8zPy4wk?}|)tb)}+@dAf!nHhv7Zk~nq)0a z*As*Z8VF6p8d{^qB8xllxwF-j&Xm_O;a`q{ZtQsBL!ffq2AKCwbKkmOZ>+gNrExR6EA`Q0Z<9Y1fqnv41f?5KN2Bt%(R3k^3(59&GU&SCiK zRuIsF9o3+izn&*{dS)Wg%FoIH`)ERJXaJtb1oWKb0{K@?Js;a6%d#p3y$aTsY~quw zd3MOMK+~vXxAL1ac8Qc?#tS(wPiY_(&~8twUghs&AV_T9WG5n*MSye>)%XX1x7Q+$ z1!nrJ2NgIG^$!L>p_5EqfjO)eIGv;_$I40?SM4a?9sJ8RzIV(0pRXB)q8-4fE)uDsaj7t-yXr=bDBgZ9($R=e2m%gok|Zp6N?EPZ+p? z5S3=-MM+<9ShC|Z|LJ6O(&XvSbf`GZ;u<;Jk5V}9jDWOZn~BIALEAnbNb3QDmocx7 zt))KC?fr;jPt(d4Ht~4#o3iWl&x1G&lg38p#^`jTwtwc`(LgKO zVS8@6=A>d%Y2yC1FlKNv!bf!i_T+Rm3(of3Nfl0QKgDux*~Q-N@M)%X%4uCEp2dQj z9GP2FCGLT!E-AMh&IO0^&AuZRTAbSj+(yMI>4tugHfT8+NV9t;4lA||n=)4~w_=aB z_7f2{!+$j7Hd=tNqA`RHg&gUCi1aWPr>e!cFs8qu?aLoU4&7I4Y$d+9PS;9Fo17EX z$!$=yJU)#C`^9N(48WZw;7}iajDD;X7-?3$%dZ?4bu~|dpkPL7tHE~+a=JC(LU@Fv`7V~4NVa^x{OAm!Y-+t_IxKB%+?kzTUG^$ z8Bu|ez~W{-)cV?y6pCX&g3aLvvKF>6+NQ`H?4>^%uwmBkcmpheEPR+-?~AJ>absW~ zt>(UoP##B^PY%8L&_<^t9)EV|``KR4xbfy2oFV1`tF@xW-2rpEyaAZ-I#xDXk}bj4 z%`FX{XF)FJ3{Y4=kUnxK(6I)?;LfvK?HHUC zA~J8|*X^E1H%TJyOQFE|+5KSS8e0fB!NL)R)5fgi1I&iNT=d10^)g2Ko{R6Jn^)Ao zYFy?mTKKWv$tfG&N%@Xb0x9?IWIuhnAs@Vebj@0LH6p-xI)6mV*4+uhHn1!JE-Cgh z+>a;FSeoyb74`G--JM9-K@~Bo6L_4MNla6?zNgDfkhQDQZ@$aK&nx{syAG!TusxmC z7G|N+2)1CdYoKKs-dd+0Nx_w>qQhBGPgr%8_%yO%-JQ{^-lKAG@XOukv7O<teMxB)dsbj!!vGXe(9{=ZUeV=&-AY)o; z%}mrRgS9tH@HC>?c5iMHcT?S; zrHbB8FHvN!nD4pGxl6rqE~8rXBsz&4kzR2%tzfFkjD1z=cp%>(-Aovp?#Xg3<{N4K zm>_|rp4Qs3Z)b@IaLRSwQwOVFJVEf`XM&Ku(#iQq;-SvRJLM4^>mj#v?E4{qKS0BI z#$wCL5gOb2T_dVaN^0wQTmXji!#^0|nUMQ+hq^W!=^52_mypj}`K{S#1^$4|0J0v= zg#+q%lx|b%XLSb+@{S$O9*A=j0E`g6GBE;~&3$i}7MY%g0ANy&RnEx3r_c@`;xU;_ z1)2@vj&Hv{l(pdcB22W7hcur&@UTn4(n$0$3)~xg;zaryXFu}3Q~Er}O|KYpR^qHm z>f&#Z)3s?oJE{G)6Qx}F@@s~Jakb0HeQ$|s)q|V)%y43b(dIL~TX=1C3th95CoZHf z?q5?1A1+V0-Qsm~@J22mXDwiXzX(fU6tV=(Wt?FS`IB0(s|28V6Z@3*V%cXaxO&v; z5eWt=KVy)VPm~U-AWGEzPj&G>`ql~Pe|4l_iK0o?t?AuL>{MQT_eT5tw9zQxUL5WM zmdmyW-^cJ=y($;EP4Pe8`n1$QvbInpRxwd{)9#(3(1YZYquyA0rf0|lnyTPe4)rd6 zF`XbM`n$_o>1XraM{WUGyZY#PMQ$6k%K0&1t!E-9a^T>0+_x3JmZScV3yRcKH=g8C0*f z|FtJKrX%$#t!{)gLI}KJ*`+}|%0%u!pffzKQ$7{PPNr_^3fgtD0!j9a+0+8w<4)11 z_|?ce09$)y%t__oMCGCig>JmE4Rz)ghDDK$N5u`VEB)CXz=pIgd$6vN63ZEW@;6e- z?(P5BaxTz2&?1r;3AV%V@ViMGP;ZU^2R|B-izJ}$(4OY5n7d~qkurMhn9G}aM0IxB zWx>(aQV+K#1ptpR;0e@aa1bQtdP)!-0!T%28?aC~>2|N?R+&fz=j!HsWM1NzW!_{GB3hpxa8@33;(My%|EF7x|tjlduCCP8`HoFg_IXV0_ z^_{ypnNTR$fewQ5ie~$1BeGe%ZUa3n7C)*2$TKESBh-eRfdfk|r18iGk}E8GWf7g+ z^w1+}75;AAQyX&-Q{R9cNA5sv66vN}YOL@ZTK#Fxr3jPLZsE*AVXdqWNJQJ%0tD)$ zBh2d|3aguC!&{$xz9@PCkCo}`dk@W@YAYJGL(9EZ)Z}+H zk?=ojBA|W@bL5<8?&<>)!yckMf%Gg=J}u;sat@rNbzx%q*xjGW)}NzJng3`W`Sv~# zQ5>|a%57hKeZ~$kNSTAXEk8YtqYTA~H{XKs%d&=DnstR8iCN3qyUII<7_SdIRH?-PfHvH-hPm*ex}K@^<|7*fTIU>3@iA(gUyx+cX1n1 z6c-1qpD3Db)~4ri6e2Pgdrdgnb&hMCR4;dgd)*Oob`f92gb_1&A5FBw1iKdP%4m=@ z{qY(z?zOOq9qgmw{1VYpEv+v=D6e}n-yFi(;*Ew|EpQcYJsN&fdM*7NB?ms>$Q`2) ziAaWxNlko9+zOyI= zijfU^KiZCU?DJC9?(<05vD)%{D-oK8%G5Lm@OeTiD(QZpQ10e`K)wes;JADd?qydr zZfZEp9M#wl*Sbk&wTA`*O`W%~L7LU#-M*(UI+LZ4b&sHAo{aybG z!*Uq$vo}N_uY|VqgI>OHG0xf7W_LBY0Kod_%^enPz!cPV(0Ue&ldx)Sz!uJGV0K>U z$t-mQ@kcwfm`D$|kW zZ(wya3=N-ct>m?BIExyy*rVxWjtJpSiL}qOzj5i`g*o*D{CT(q9FW)k(Nu<~u@N}%o6pTW z2Q#s+v~I+~O4>oW9A(d&A@7gMdp;(&vXt3$Hc$M(&Jy$sKe$ZPg!ItGEYMQWTCpc8 z8)+1{_l{uf;{X{aoW5u)2km}N5Wh5OViNJgD~;b+Iveq8{kqS@y8jwweIB0E!5ze3 zpuk|IFVYbl7NyQ+K#S3i+~3ITgD3&asY^9jOAv}7dEEIzQ5;{O#R$->+y0v;{LeD# zEgPYI-J#b8z{$$4eRGZiQ_nC{S|FIaXk3LAVtPzFCksNVNM&0m2Nn_i8%#dhfR~_a z+(|tv%KQ!f^nLy3;Q9B%kAV?^>jAS$JHH5s39%!tznSxGwD|IuS~fa3mIDvdF$zDW zl5zLCn*Sq2bkv-tFW6KIQlebSM`SB?0O3BuSmrms#r7Yl0}86mGL-O#w-%XCgg+c7O@N=Q@tQIUmd#ST)gc=OgL>fWgSsNnCV51{)J@8OmA#JuP zoDEywVR02qj30Lt9?k7RsKUd|sg^L?AT}mk<>D3pO?i~>gKVXyZ6%>jV4R);Hm%rn zM;MB^+(&IX-Nhzy>)kcNOdL!R9DzC-rSu)q`d z`Az4q{qvMjT??72NyxAh*3I@Huf&9U&!-M>Nr3LG%**4;ABs`8Cx4V%e|;%52kSN> zK;$#ADpW)pOtP%tiA@GN|86k-k8i+JLfcp~wEx$iVegg$$h0%UufID4M)CrV{uh6} zwb?kXBijm)$XiLjhej{_{6-)D14I1>$^6yW@h6grOzlEECVz+c{d&Us4^Z&@ZLltR zI9t>3-=eF2jTHUqgz`}VXPJ$C=5cEO+b{SvB4Ef3!3WJJFBbp0`uYtp@$)asgT;%~ zt0aG=U%~_b4eI>!=X!t!rpN924J`Tp?HBy=BdvW8>T#7fJZFD{-T(Pl{mZ-Bq5%#s zhUZSs{GUff?R^ENCl_6`26z1Ctoi-d{m+a2({laIkN(4`UuLsBCiT#xUt8EShFH&a+ zg07UDR1=2ji@gLl>a0gP_-s7Dz_I(dP7y%-R4BlZq@*pg0fc`!BLCMVII6?}o*5rv zoPRt)gbx&G_x5*}>f$@{tSOS=`Y^%|f?n2C(qZ^JZK$savwHb`$y|f=q{Xq6neMR^ zv&g70)pWC4Oe)DdCRJOeHcZiztg$lJ7IYkY%gyM(>Md)d&sVL=MLGa}+Yb(%R7mW01eupMK;2{D2cCuCTXN1jN{|jKrX1967EsDkLUuV@ z|1WpJfBT|&2OEHZkAQwDpnc8(gh1PegOmEkZg<(N3xA(8qDuqRGs~&NrSjp1T`zB* zyDPQ!fzDoiPm!jq%(0fPZ#rXxFUR#img~#yS=WvIN@iu8T)pLsw9hLpTOgI%0BeEP zhdFK6uiysyL5QDn$uAhAr(Xa)>nNf=0+VWE3JrnCLFO(utg$6xX%>i?2e80%=~{gloWsAhN?NRGb?^cB zrwN9N+i14b+)zTE^J~d+v43)WR zvEqfKWUrcV!*YDg9VXHJlu#wZ@^IWwC=2ONVV_PU-tDMi+%6aKI~si8J+aAo*W%-q9)LKY~wa|1t`mP0T1QIf^ojv z1f6=5B@RoE4?*M=y?T4IfW=V7@&VRI{A=vX=2_^YO`wB)ETHDo4E>@wL+0pq#=Shi z;fzSrFd%;TlY@f4u^JzFEz+hfVFKz8<205Xp#hJ&!%vhC6F!AN8o@GtGAs zsmsYIdZJ>q6$~?h+Hnm*NbfYSm(8P-Mn-|^u(q!1zLwLYqM^j-3>{Nt9cS>Yp_Fn7 z?(r~x!ttun|IBM=aH~)-yxfVv+FW8HboUNm*hHT+RjLt%`GA}o$mQ(@Rj<#5*p`=k;`}^aX>sA{3Hejw4K$D%49hTC!ohN~pl^5_ey1YjoQAH_V$qZN~g$ zZ}}kMw}}A>sIE04UG(9bQ{1-PCAvgx3=%Ny2vS@V5+6ry`)%eQJf5w3VE^j4>OU(U_&Yp2Dw49-+b@9wI~!m0@hQh;BStn}d04&9RHDkbwRM-a_17xv##$m) z&$Rel=jR-2O~IIfL+a|sZ|6CJpmh$wPG-4i^!uk8>E&FnX7jw@lTrsd(~{0-Hdo`r ztk^ItnwsL&u+C>e;{~ zYZ!+m_$-NCxNR%Qg%NsKb$@O^w30o!Oi84#bOT4Oi+A0=Uk}3n+;jMNd*gIK1J;7# z7T&QD!lEJ-V1^S7I#@_p!KB^?&IS_rWJ1F0ZtSXy5dY+V?j)^SfV4M;oeC#k1V>S6 zVog}tcbo-eFL;Q+L2COE3FMmtj?I+;7Q$Tt!L~SqsytZN4`)O07`WhgYRssHhDbY( zn&IxJgh9j)jM@pPDNuXGQ~Ishq+W)liG#j+J4fH>+)D{rEO?xh}zw2T9#xH4Pde z&;od8Hx78pd+k+E)grUDEPPvJ$m@X&dwnM8mfF*X1<((*y$lu-#oTn`wp#&E8_rzy zxxV`AS%%+5xx;M|oKKX`&3=y{XN?7l6`4V3{Nu%X2E(~F)c#>rxWPy?ykzgjuMY;RyvKEFG7;)c65TY0*oM1au8;wgt^1d>y z;V5YaP0PHFT<(*qhrEYXN{|Vd)22Xx%mAuF7!Njyv)d(Nr1|TmVx(gPb@No=N}N|t zyV^H8dsS;#nX zhORfBj3F>53cs8mx8cS8ld{nXR;o!#F{8ltd8&D}TT`MhLjVYN5%UdTb~Of6qfEpe zWBv-v>U7Q2D^WQ}xdJEhfQd0r3AqeBj*j+wyi9__WtXV8_89I-X?VuXCO2;J5wGip zl;ZC}UCc=36k@iIFA+`_GdDITO3;R|P4N@%P~yZ_s!4Q1IeNMC)6|Ck%%uQQeu1ow z1!_V#YdHFZQ&mSfQQf0r$uqQ5Z2>dZ3FgP!783g5tu&rp~>!ARZ881*g826EP&JuOs%2`5hSPcN*C=-2OQSdwq!? zV>bRW$GB$=CF96MqH=CaYa}O%JKKG6eS~jGzS??7G!Z<{nyAp{Z?Tkud6#;Kn+~+%J&cNJ))-QE$ zbA} z=OQk`bxA9l>EX44Z1`LU;2WL2sZT|?1I+Nj_1f&3F`GH9xXsX)JKvhyU-fwcU_f2y zs`<xQz{G#3B<5{Fjqe zqNny$b+5+TRg9mM^=85qo~Ltt$u;sEM$Vyf5D78lwx7N{_o`Yo?9!PnefqwN-74JP zYE5T>?5jmo(hNa9`jlw$UwlThN10{)(b^jJC`N)#>UPlcRJP>HOb6gpN!pUu3qu zGE`oqA99zWM0k?%j_77wHC94you|m`PDJ(NN&+jeG zM_JX@Nw)N_F`YSI#G)X0_DhlTaMj?l@AgVP?yo1p&NPIrocmPd0{=-;srB-h{Ri+K z5x+qdy#HmNc$&y%xxh#;r8!!<}uU$A)d>;<8TB!$Rry06;Mdm6d7DQp0eh3wMAMT93u6Es_=@g(xS z-3}R(8u`E41MK5z)dlKOE)9(6q>Uf?en&kJR>6-3+Mo)LyiH0BAfZZymj!H_`isZl*6v^AiRi(ExCso^OEWp3fVY4*r3f`^VK~#n>W>U9FJY#Z^ z(?)xK_fUUr%&D4+qVM}mTySq5fD_?yGibQ)bXY;3d=F#f?2Xv@_{*{ERz3)7vYz9X z+}#n%G=jC+ajW#)?8f`z3Aw6)V)^uWJLnGb-;v&kjJi&i^D-~fRZCSu73* ze^8as^SF>Kw7o<($12jTcrbXU?!Hjn1K!awTdMz31-&+xEfrBG@dBOTf-*B@b|5bEov=+QtHw*XGB>oaI3(!l|XHgok8NR4(=sfr^7IE%ER zL707=$G|##=~-f&nCOWs{te~y&m&j^r|oOi*rY_HX+gTX_GBZB2qXzoS66e3u}?)1 zza`2?&#qQN++~~bCG}&NW2#>i<1UWrFGe61u~3EuMfiGxFvIZz#yEV3t6GU@tNM4P zAcTCp7zJ*3L^OnnOzeS+ zM5GvAHl51wIC2T+oknH3U99#U=Pt%p2}~Q`F5+-&^9dhwWh2(AzQ`v@iR~O~|DrEN z@$i$n8q0O4zv#6K8xiPhiSv;*BxZicNz^UN$SB{as+&V~!$m_Lgg&hnshFE+jAEau zLTt;)y3?Nozx5I9YaoWV6dj^upELvgbz<2i{yIAI;;u?r560~Y>GPdv(#{LVWY;I% zXu&zBLgC^eh@&OvOk$F$yz(A^z}%_{X~AB^b}^P;7UTSMlW8V)(4x`Ox`$R=LQ3kLb}FUA;` ziOgjd*Jz8XnC2B9`R1H@GZ7stLEb^uTjoc??zGN{8M>f$a)5+YZAS6fCxJs(aGS>W zc+QXX_anWq9fM=0Cmw|_O(zfp)`8hu=;Lh+#!HOaghpSrUtf)%P92b}5g%!RmHs90n>^>vHon zqc4|=IL~lBA&@2plqeOCtY#BfV4vB7mI~5OoYe=bTRz~c{h@nvMk_leO$DS(T$IB3 zio{MoZXcGX^RIco{{zl*l=7j8d%FR*wKdZ8&93*$;DKcgri_gPsN!U2{o|{Xp2r1z zEPq6OwUq?zE%ik4D&xDpPTx0mtjk9vRdDq&(YnsZZ$DOMr$K{tgtP<>PdQL&k@h>3 z?zgKN0RxlDRHs(MM|tP(rJqPd%^&ej3Zt_T7>xxkcKKbt>rb@+~*kytr9f6Q2NkhFg}IrV9Z(}k6dIiexEy{GlQ%6Q|a-`g#vM|b^~7r>U3 z!+2_B%97g+^u1YG^$RyTcf8RsEHe-?_cC2T$`|5fm+6IAG&gnTn5srKmo2(YGBZ)Q z1*MOFUv`OE3k$!J?AAU&gK-pZZE|wg2jM$h>z@cujN`9Vj!Q2TCK%y&&jFG8!}XRQ zna-Y`IDvWT4@EKGT->^x<`y``Np@U)aSOt~Y&3tQW8u;E^z+ zlv~8)p1Zre7=meC(yt^f3W#XXAz>~?o3s8?vmX`pe6 zIpr&6=5ma(7+DY38qw{XW*+FyOZz$ikg3;g+(v!ko596zuVNo2PhuSSL>z^~P4e~+ z9m-I(hQ2^);)xmDojq2|H)0pq2sd0t+!=4va~i|7j_*uyxiD}^fB4L~NA0{D|Hpd$ zWlOsjf4-F?x61LL1wzV1bYoamDB4oMqq31~pJZh0;hPG2N2KEzJc_=CJ}oBO>j>IH z3dm7*>MC!CiH%?fIRnT{mA(G2@>J(4hzmD}vag53@h6fvh$vayeS83FjbDgoVV8Il zf61(e%43ztJ;s*b2MpLR6$b(HOQtZ$KuF`5n&%BN=A`9h-ce2q^o3KZZ1ZqTI4)Oh zU-V9x!{-|l%^LT+J}&s+t(~lE#Wp`Ag3^pZ?II|nTJ#uYYqUxG+b_Fy$#ISi;uaH5 zV)?TgD^{XhW^3j2t6}-Z*wd+PK}eahtnRF0c&}6a{4GOe?&PB?Z@r^g0Fa!izx95< zdDn`~361P-Sin1SH?GM?b4%W~QqyfT)g5OC^#BH~@A08kY&sZpd%<6$`f{EGg+Tq2 znk4_0LH(eWBP&&PWO$k&A6J@)`;5Di%0ngk<}<85XrQf#RWy`bQp1Sj)yQgBU^;0m5UZ^H|-nhW*RKGXWb?nvAtSphK-C{B6A?(j|2BSkqpyDhm*on0#1+#KTnO0RUx~#(bpoVE2S=+{UzthRx$?d{oJ+7CrV35P25tZo zD6C{ykT|Z&&B6CCCdv0Af+5U@1!LiIg4l;kW8iKIN%X}EURHy_L6_L61_tCbLH$yU zejVfG+_8L+E?IQigoj*>iABr{lZHVe;*INB6J0cDUw%G)(V{r&Dx1{dPk$|FY4kdW zA*#z01m%gS*Dzu|<%d>5Z&C8^_Ejv+>awOga3S!c%pbjp9Bx&$hga>enq;TK;Hh&d z=Di4@e8J%LZtoDAG?2*&T>jLK7`t8^T5x`k3~S(#ARZy`ADm7@n^8vdP|LWA#*BLDJIB{-MKZ z^GB19^(85Vev+2i3Y6GY8JoJ^xKDi*&r4b0Uwlr=(67oAvVb`5M@MoAVO8}J88)(y zqck0l$r8g9*rbSxz1uc?HJM|@?ai7pZ|mEhT@1w&upVv6_p_DK%O8b{kp|-<mb=lt;6J<}Rx z9(&woLmbZ+j>R#Y&Vu>qjZorA`kww~eyO+k+T}UF2b5&_G~rocb!PuYAdu&bvG<2y zcNz3qoXSxgX=!~+P_(K}oNXozTocioJFk7*`I*hykd0E zYiK3e+oF2aVBlpMEHm|3pUjDM;I0N86n9KG?b2~?ydY<&>!#(wL5slI7dDsleYFJ- z7nY)jsZ!{t=Vlr%(zr!D^_GGOv9W_6VZ_@nGREjTbA&%tWdbVYVX1q;jK=R=bC4RF z0TzvtBn6f%GzAIue%l%B>bKj7z{2ffryB)sJHKaaSQS6>UvrcHRByMcaXMDAVPSH7haQ$96L|kv zY2w5Xke0W^ygwf|C0oj-Yr=g{ZEFd-@hm5YpW>M?q}t!%uI_p0_PY7bHMS7wa4P>t zfJpo!zEft;-Nrm0`j%GdFCxwWn0>bw4+#c+KQVUk@;X07!#n7>jT)cPxft|KZu{mK zq6gymIphuq!@7}z)zS5%cezW*liWLlHnTHXy_eaDKgvI{Q{yk!F!Xf33~NwTCugGD zv{MG$qDKp(QQBYkFX}j<`R8AI@>%H~jEF`vnX@UYa&hUn28;Ma`4=dV+zxZ{>KF{X zwo{5HTY)|=ao%4`Sk4GW>?sIz)F^vcC3TNpI&?$m66s9Hu;?s*&|^orREg9N<6i;l zi^P!`iQ|!Qca?)aQ4B*pxls5@lT9jf{10@=tmbuY4s?#vVzGU8Q~t(}x0r%IHq9@p zA=i6_iOXOGZ-=o&P+m#uNu0i0wZ55{^m2O=Z5oiB%0#a-d%NcjIk{FNbUk#BB%nw|M?BK-`tG-MnJSkt9_tnb;~)ZL)Lw^m?b)6)-^Tx_ zu3UII>uHQ$h>^68^4dN9p#{JB;|&e2Xy1Ziqa{IJ*Nr6ky^UO|AlPOug1}lwsOu89 zElco7snIfp4I7Wjy)iGQlY87b;z1M2aOBZBS#N3JaUj5uQO-VZzE7pUVblL<#V!bL zYvqR)%U>8IxANxD^Tc^sRMj8eK!VqLX<$(}uau1j&X8lE{R|}Z9J;l&$gVTn=Z#t%jdb*nH`K0G>uP>vGh5J;&u3L8*2Z;s#q+=j8)1%nrHCve!CCLo zpjmzp{pEz}dL9FXKGUBBd6@TH%HQHO;J}$5>a|`H_W5bCLrr<+@jb@#thllW;pamX z;FS+gIRdN1HWM5hwyJfV%!J_EX4(Km8`GWogn38ohNE$6T)}+HA_WMxAHTg2&ODIPpeJNK5~et(lytyq{2O-)I(REK+9gEpqqx{;`joV= zde(y&hx%}h^e-JbOKIkkqa7zx?=D(2LVSQ+aE=&5zYh~E%hR6!E_>``#T4&RtD6H$ zcMk}(tG+swaDJbq+xSkA0;eNP%VFM}`=vG-%y%NzJtXwp@>u01;U8EzxyG3H&imEj z$V>8ov0UnouC@X7$Jwt}{hSCUQ#!E7q5A}tSh~KZizk>*T^!SEkeM~T7jvKD$ zYMxYLYRm7h;~-HDXq#W=$(UxrRl}we7F$zOP8G;1OunDaPJwGdR0VI|37Ydh!j;BJ zwARYEFWej9ghfZLj`r;Rag)K^HvZ)3{R7^7`e&sZ^VE!u{4IN;eH&yKQf#y3TP>1N zUBfp$VJbIC>pJt-n5^mA>qdjvsX(r1gPe>#4jS2Cz=6LK(IHJ8Ai3G6w_8mRYTGym zvzitfIu-|FkyN02m1;%T&K&6cG5!!lMwTF)jL{zrSWM0sEb&>Td%RUl<=%x@n=?~i z4j{$DY#`MreJbU?3J6P{imre~xk8+g@!b>E`-D*9!lOyP{TQ-hYZ@bM7q~-jc2A?CKi0M7+Xt`dNXL z61)R?K}Ex&4OV4fJFKo48GGyh@!poew3S+% zi~j1YtHA<^zHf>U*B|LV=2C|GIi91pYU)PMRdH%MJngL5n6b`g_VH>fqs~fHfq>m& z0@M{e1QD<0Q&mN-b7!l=Uv#$5?6SUmbC78m{mgx4zxER4ATemAbL&8d;iEmpg$u(^ z8*SP@`#3@2oZXS+A(nAD(^047t;?*oQ`XreorLQcO$;{-=)2p9+O^w7v#no@XB~@{ z((;kNY}Fq@v?`+%`r_XkpM@1)cnAF;6@-;Fff$nZk#Vq}qyS4uFtf(1n;D6l0y+JT z{@G(7#}VV_wDQ9^FF4G8$QXuB!KoYLZ?HWZ3|AtG_By^Fau}K|mtwsg`jq^tK=v-` z?eHop2uC7X;>PUD13g>6!`V1M5V)tjyy7K1OJGaIb>qHXc)tjOP{xX-mDdD@?;VWR z1xJxt*h+2#Z_)DYiBhdud9T$Ud{die5v49Yc#GHa%vpPhryNzV75*TdsGNUv^)D41 z=M9_?%NJPoqI+<6?p5h8Y^WdJ#uBu}?^W*MR^Un@Y5wh?+KU3Pt{>a+ z_|>ywZD)6b#_@x)oxKAis#j3kD!B~(Mi$Kbg4G)}#?n7+vI~`()%#-J#Qz2IN$wO? znZ`>;nG(hPH*(9*T^M-1+SY++XB;2Eb8L4evtTWF&>p|XhT)e^HhegX)jgaTjC(*y z9DjJNOTxr4QJJS86j|cKA3GzbEUHo$6YGOyr%MyPvpC)qy`MUn*p=N8eK}yq)8m2 zSZ&^Iqmy6taPG{fWcix^ccc` zxmD<$ATPU-n}y&}Do#1_3b)zVy@^k&Z_q(P5e~dm(1cFna|cTC>$VY|e^p?eA9^I2 z*)MnZNf=()m7nzTs-oZ}&gZivKSjBBsNvy@c)<()R(^^;`h}kSB1CX^58?OP<8fp# zcvWa?3;FD1Uk&-tF+rHkt?wQV;Pbe5BxQsGzVrfx!iS~6GGO>TbDgB_Se&4MzdQG~ zIm|)M7Q^2NssB9eSesu0wApc-qOe;0esjjW4g7LkqF2@1!@(~#_etk{Hudy1%o?Tc zFhQHC@2g2^H;wf$sh62nun#r(atsJi3sn(~n~YDywfrHMeTj4alpLOo5C%%hQi89E zd$!uKX~jt1zB{k{Cdb}|C}jX;xLK}t>K@pJ5hC8u2l=gl5NUgPJ0M}ZUdPx%XI%VK z$PL~0cHK82Pz`7p8=%*2%0bp}{@!Vr@iq5HY{(z$<=;n*=EA10le1axC|y0|Z(}N0 z5;_ab#g^$s^kFUTNRM<)uDpq|^}2DDUF=|wWeps?dEZPO0~vJeM+9*LL#?QYvZvx) z!EP_DkOmN0$GNu+nYZ5GC@=K=tPEi758Mw6bKA}*Egj;RP6|Lt{Xx6T-<<7TaD1@^ z%Uk_Le{b~xn~wD6BG31k$s7cm#VO<4nIeM3Rl~8Preia?4%L6~t(a_%W{okin*W3i z$FlU6p2A(;KJQ1*G}kZSDgJWKrm=gYh&F4jRN;@bA<3DJ=5;MMK?}rbE0){EAq}0t z9ld!-YZ~TwJ*i*eDHK|(Q9a6arL<~6@a4h-QYYs4MDpsQDtr80d-rLyOV5Dme1TP4 z27U7ynCanbG5s5{7;qbz|CA@{O>JlLJG;Q60cOOZwq3+zBVL859;%nMMNK zo(157mI}&zY0medGzOTdA+1;4A(KyEw)Z(e4qUle&dtj#b(!Nu$u?D7plUuwI-JU` z6R7@FG(9}@jMLM;>f(fW%dC?=PeOve*d#UA+Keku_pIxr=Q5oPLLY=sa#D!pfb8dQ`v2ea0Du z3f?VI#cd=5U{U;Z2zU!|<(VTeqw(}B>8E1PlR2V*F=AisjRk)y{F3b23+nm!`t)!x7mj5tx{8gUw196NfI_Zzk{op6^%n2;yvuaRFK zs$tc6!!zuNbG}uFdi3cDtoB@GP*qkb8y!d?BqNO!JDpy)lb@{dJ*ip~(TV|kY?jlE z5}1LpDz*!c?eg>6LOMo5%vGd3^QoQ!BsS!B)VxWg+ ztYI^GYpICH%taQ2xs1&mSOBUE1yVD%(UM-Skfsi{o?vcWO9&xn2^ZkxZmbNv8yen* zHerDk#dZQg1U<8gcct>`QWfK)8{+N;KJks0^PndW&r4b2y)nb~Vx3?KSteAbWwV*m zjUOWtBadiA>e?;~eMA<`^!q$6wQ!A1>O<*YoS-RjAn_>ZOE+H~-nzX+@zubWVTU6P z$zDYq1o=qRcVWzn;lv_bi7wFeb(^o2)}?pU-Y)$H$W5pE5jCFzU=>nm<KbIQwQWUGh2uxj=xkJe@zIb;<6B6LQ`Fv4Q zTin&;mH_}AJrl2zTgJ?H3kjFCVd&l3q#Aq1!*PZ`QWIz_!tQw1AX7f}0%squnBMEQ zeiupoeXZO2Hf6w=VB5L|Yz{fllZX!NZ*Q(W!^8^T&dLqNsn8@Zxbxq`Rj3U6vX$>7REW0uZ+Qn!}McbpNL*CZ#jlirRqiGoIa%_(27A+L@qL)NSy z4v9(mV*{VNF~g^UyuUP2N(bD#X-9SA5=72Gd-ze!Swp`UWi`AL_m0mBINtbV5*R&= zb*3AB>N=VaEP7-%5?Chs59Qk88`;($Hh*l5Y$3`VBc2>lbd))lAq69(Y`h; z%nPGCwB{L~p~t;rS1gD%(x>Ld&k^V++h)}SN)&U9+N_hhOu)WGM+mLUiW6i&N&!6# zqc*sCj}XPd_%X-38^n>S=b$&p717DaTq}y1P~X?TYSZZwR`)yBVTZIC4^ouy(s0$A zrN!y|={Wq`nabPoi8peELkVmd#kecMLuMJ@nAa+s1)m8grM>K2-u%^m#>eO{YLN1d z*a8z<3ql60!O-Sj-r7E??~aiDlJbNunM!$A3{=a!TPdPa`iGeEdxbb&px!Q`MvcpN zQ?ZwEIbX>aQ({>@tZ9}8@%$_>3QhH|A+ASruuNQ$=y7>(K4(1qfsyj&& z5MJs9S(iZ>?Gpo)HJc-T2;~DWyIgcs;IBbQV2MP#)%Ey5P&zd1nqW_%Ynbu+p$#h# zJ(+=IO#{$i1%a08MYG3cmTlJdiWn@SxC7P8tb62hIRgaEc#@w+t5Zl{CDYzm@|hId zjCcxKQ6!hQuj|ArSzxTPn8=4-(qToj(KVgC3<2}oNn4~|zi2&rlRu$;`i?IfR_}wI z!e47|{=td&*@Ef~eDzdAh#GIg8KAi=?{<|V*PmLTJDh@}VrgdDk_2(elJY^PeENz= zCzkS_mOe#4)R1m?v5D432G=JmaN_A;>0JtN)p@I;YH5E#Sp-HeiYrqR+Q(UxJ%Y)L z4)xkYt47)r9q?mm$@*=<7X`9w`3}V5Wvo`)PH&0*twjG%d*mNKK2X`at?b!Bn**g3 z_ZKTuhsdQ+S6z}PqNrEOa#6kCwwyNz-nQ2}|7whq-iE(6@A!u>>^yxP40e|BSIQ|~ z<&_m{AKXTibIO}^e{Bf!zb%NhNZj>A%y!oY|3kv_%kT6rJ7LQm5G?PLJ!AhDOR#?# zIqr1_!SW|3rNOQLCtv&LCj_S|P@*sNHc0%RES;Z^0Q^ZniQai8?&|;7SNm`JyPtn~ z9}g(ee}r6Y`E!f%FW>6V%8h?M_p(EvM0X!}An-So=>K`Kzj>(t?KS-8#s0Nz^#9L} zU8(HID;dWqF0R~tJhWf_R$L(cmf3#d4x`TpPpO%i9CW%^A+(FA^KjS8-A4DLA0OHN zQgz?U7ge3=@{0R!T)uxS@6@a0m2r;>R}+u<@z>L@Gh1Zqr`?M^nnz|?Z>C>YcQ6a; zW!=&W=i4m%o6}^+lUoNl_V4|>f6_}< z-}TNf5xaVQJNwoxfA`OA8IBVrx&==0JD&W#NAmN(I(nRaSC{BSwd%LO|5VPe<_RuQ z*k}ux^<%?IP9(K#V5VyT`CKWxKgv>6VC(y9fA@jIn=;t9jtX{yo=1ud2S(Qc?Evne81TMrQV_)2 zucAsc-%eAUz2%GlEE?vW?-kN+T+!ZRwW=CA6YewTnTPccFrbURNWU}odbIisop9FBJ>8w@2 z)qm&t`fs~!nVO9|Mx9gS7rPiGpcpdr9TzwVwkxcJC@@~r%Rbbb;kO7gB3E{eR@b8}-dpU@EI9hRZ zFzm`s9R|m7i4ZP9GTVIIW)Cyg7N9566$Gxl_?t)M&s{^ETDyciAXbAZSluZI=&i{; zS(|5rQ} zpBBa=uZ2fsNJP=F1_~A#E;ObE&7mdLb1EcO9fsY`d(~;e|IVfQ1?sHmMaQ3)YORf@ zox>w(s^Rsua?G}R9BjY3)8I} z6yX>^M5Tj>fF7im&{089LQz4HE}f_ldJj=)BE5HzE+8NZgc68!0qIRc2|WZ1A+$gO z;Xa)Ad)IgGTK6|uDjn1q8y~NN(KUG(k+YrRbOm`p9_eDb9pT}a+cd1YwCVt>>ALF8vjmvk%3JsA zs-FUrzn&!3FBbHFMrV8cM0O{c6@GnWp{QUF4Q3sG#FUB=c7A$}G`Pt8L&&d29B+vI zgO+JatrztGGO)46G2YPA8ojx3_YLaPzAfvcCLl*%)4Q`0$lnf~Nv$xrI1#q(Ov764 z{6Wyr#<#oq3FC>!v?!9Xn8p?1X+6u8pSnlS(loI@`-a*A?y;rwy{(dQ^(k|Xg|h7_ z?y(uyErItS^0?n6g?+O6#(jKQPR$N;0WXw3{N2m`d^upG)N_r7+SyP=Pvn1owzwzr zeITd;5XF4=a0nW1Zx-~y?$LgS0PTwcCW_On z)u&a~ZHMB=(AS=fe*2m?GbAalAQU)XgG0;NRjZ3~pe#m7zJ!QXD**sHpRB{I= z7R1mR=hrE;PHqLpF-c02hqx+w{dH&JQ%(7b(#(j2G733~pmH%+m3 zIqquATNY9ui^3N!SbqW@XRQ8(k`Kx3syo0EQY#+O1cr5)bp?^HL7Sd=8#z^Vp$$|hapK|(|Kh8~~>O-m= zF+38%GSHq?Pq=zrHR_Ow`#FbL7mX53+)v`m4u4WUO9z19+-jyhk63 z*5%~#=JlxLY0;>B+v-QovO;UZz#tQiI8@bYddjx>d}nGD+-%>`g$va7_sZ(YacUM2 zb}vESv0~*y(!8_^r={s~VXF{p%9FiH7`#F@bH)dvg~$~}K3VaU0H4TQr+j1WAtFsDLh8f%+iw5VCPXjrfzlQ;gy6U@g%YKPDNpSa1dis<@7eEAbQTbgUF|lot-O$a z#Yf5Oh`~1Qi6z}qxU;}pHP0Jpb55n{2Zxv|)+V`JVrUiuN1y+`^ynOTD7oCcY0f;) zvzWR=>Evp0%q3hs5#QqdHc4Oj#|t4#cVd;b-nH6%_2d&m;)%+}5%=*k$T(ifJEZ0J zOfaHZV|K8>y0_;Ry@jFOcN#EDZG7rf+~X4fexUL=#L-fvuP@xbHRN4e>=b~Nl?!WH zPg6GEZ*Vih#Il39{X}8@{pQbha(Un;#oHkMaZc42k5b>BO`1Z}_O56MCNq`%E?L*rK8(NX`mviz^USt>U>vrdC}6l?Q3am6E&fcm(TP2y>NLx zdASGJ%yPWGB8l)HE9_5Ai8_tYTS50+4Cq@YG_9LnJo<7BPpWS{{fkt=)(#QER6S(f$m*wBt+ORmjiwCkA58d8%tI z*>8!`7A0u~0po@I6ECGY|M{B;B(surA>lr*ZS^KrF~)S%9|y?_m0<$de79JWMX5*u zwbyg3-rdY8DFB@2si+irq|32Q+MAW+0_Ox624cq6KV_UrF&KZPn*?&p`gBCBk(g~c zt~!!!t-Ly6-859#XS`bRIto>v-2ryt+T)t zkkHkhE!mu3EBshpP`xXTrG`(Dn*<46&a)oK=GjW+KVLcmga>?MOTa&ua!R3Juxq#U zeB|>h^{lm%UMRW)6Jmy{Dy37rfUmw%dlO%$8LTsn*Iazrl-k?4yI|utEux1B>QR}i z$y9IOOZcBSyR_mNn$=SJp7z>C&SkpK7vu%}_N;QVqXB<}1P*_6zCfP^i&ztYVf;$c z@5p=y)GWvEdiQTq{}W`%?)Vls0thmGYg(YC+vop;nS)%#Mo?a$Q6A|zeqPPU!Zb>}_Qox;QVbDAdyk{WZ1GViPCX{qhApDF)Pd^}kL~2}he9 zTulPEtV_ElQk=SfRBKYhJOG0eCSp0`l6QYCdD1&Lnue-6(9H$%aTnr(>e%PK?}@y) zE6)ZMCw@Wqi1L*jJ+Xlg4m77NXpQfkCJ*JeD^ov>d2WO|qY=|8Irq_tapnvSx!rHz zsuQDI_kNWcebQe&PN*w>rNl2e%Qhu{k^fe9!Rtc*@C>?}2|qu+Of==5=MPxRXQ!7( ztp@>&E8DNRBXC}R%HrFNNc@=z!1nu`M?Qew*7aI88HH1WZ0e`@fC3u?!X_{9l~wx> zyedCknjlbh-CdGt?J8{vfIM>NuU#A7c>K((GU zzop*ST#EV<6ylBna_+i8NZ-RV^uD7XvZf;iVCjxuB1lAt)pp`wyi;#X=+AH2?^}`1 zm<;cs?;?D2F89<6f=?H9wPJ+*E(CUoSv?Ni*%ckzUI)m66QOR}T6XC8CeoRUYk^-s zFq9f)eY6;GJ&IKbK9$|Eu_R`F*w-!GC$!R8C~&-n`O&vn%2G%xaP43ZVd&_H{SKtLJQq>&O=cP^R=~0w<5N? z`_sL@I*pApDGoK=sxhp4@TUP>|u5pP*)N&MhL%ipGPUD z%i)Da{wJtvqouW7rfY*@J%n;gGTefcO`)muD0(T6~CYO-IUk1M0#-*;e^)R0VcF2u4bI)F*7$ z83tMUJ4Wv>#s}yB_?8C5oa$JzNz>eYqpZpe3KTbb0NKUqM|~SVTzL&P`q1XVBI=a= z@(+&i&r<}n28TBrP|z<#2hZFiN!dYhNjM(z3g+*E z9HI)nf=qd+#!B-+!p3gJKEC|HP^*cdJ?-1|wGo;7*7L`73*5;KMt}Edf*rAgq5Z8CE!B@i&{*FbzmL z4J@00Nv^u(s-;`oxjmSp=kWmcNWX3_U}FN^vvo0Kq{kdfnT!+^?e6mPuz%>sZNPc= ziopE<7nw(BU_zQ0t{=YU*WZMF4|L$9yD{;ob6B+TdT zmu81IYroUjDSKA-NaL{A(8pl7j9sb&ARi+#GVN-a`;3mS+@yZ5j~f|QrUi8UxI+I+ z<1k61Oca$a4W1j@3i=~#X29#eoWNelhLRvngF2AYuFYnwb&(=tpvr<3khlBGqwa4C z6U52qw%c7ejg!OmeruYCi((Mq@ixv51R_#>q?k#latv3dkru5|YNyH2u>9T;KRTO5 ze9u?R4~~mWrURjtbxkmk)y8~$R5u@2QQKCG%vT6)A9ms+Gg98Gf;)}GB$n8ORsRY7 zWeBRl>^B1FCXQExd<>kUOd_lH+(u*FKz0%$UEaO%+bv?**`5IZ9BJxewU^B}M?7*#6_##%4eMrvT$`ee^~gTggrtjghk2A}10J6qkHVM~ax ztL|QF8NzX+kBMD`B$L6h@H_ADr_cUR>BxHX>y#bU#{2!ao5)YJ*Tia< z>8Gt@N`_fg`UlU_w_>H}Vz1=sX}tFk(u2L}_r=r)TyZg6%q#YC7vf{fxdxBTcd!UJ ziO13!8(>PzvCRI1D{e0M;FH!NRj)gBu!e69u56=LUk8@aJ;o>dxEIaY=m}@+&tcS+ z5qg|ysm{j4Cw@i4Sq+S9-EL6gGhhjI=kMt&lF1t=!x3y&{iL&<}u8Sna#74Y|My;|3YRwEzt>~?X?x_7n~ zfuLj$+P&f)c$(^1=bm=Q@R8%UBI+BDJlTdkULv(Nb|1`MB#;fU_}kQ!pc$F(dvXka z0_iPK+&ias~*LWBlIscJ)E^|S!11KYXKLf`Xnrw4#_ZavZWj6mCmNq%`pueOV zynqVF7-Thl#y6I3lW)^40uzFdS7q>I0A1V`wK{vf{n}0z_GlH8p5R7Xv8EMM+RKY8 zsC`2Gr1!lt!+_G_;^I6-p38240V!?rTaa|}2frXo-&yYsx@V|{K{KhD)me5V^FR8K z4q)S^k=g>2;U`JE!*1XrjSEJ+A}4|`dsK!$}Dtv)7H9jPUKzLA9G?{Dw%<|wqc8x!dizY<6QA9Ygg8A+3pEY|y~D|)J2KF&{(4wpn}92zKe@;B@$ho1 zp!v*Nsey&mBJT%*(q_nmw|`(kOFVT>;%O8RkxJCJ`Bf9bqWlTtkY>4_x9dHO z8!;(y7vhDz(Tg^x{&yEZl)ToQV(P3lx?IEWJ(cHC~fnQ;Li8_zmn^rJsxA~UU%g6b(l6oA1Z40m={0| z^BgA85y`S*c=MRufnO@?)1_T|Y-(;J>t2Du^<(S*8`=M|SpYtYx($OC*}Z3NlNKEv z-eir5ua#kMbT0F8sK8zA2mRP0|Sp#Mn53;3bcR1V}J<1JO`v(FSLfj4fN{n zg_Pj^nOfX3mw-u=My2b+Y9tm;(a|O2OlMwuVgfb2e*w|HWO{h4PPmVNPt!a&VZB_X zF9YY)4_okai4GP<2bMk8my7|L+ut~Qsd7xnTeB9c0Y53Gq>Q?i9;ec6%H0#k;cVVS z-JJ%g*z?l&4wcd+MK!HmSIx)y1%;hKKq4f`FaE{Fl-Sd&t!S1G-!C2?ldrc?-_|gNvv9ul z=DMnV!4dR!?F)6hR9!w2=K>(cHF}H7l7EXh7R)a3lRy1V@QEIP-dK8D-kWskFf1tK zBEt~u-P?jRVDF}v%=~(sFJWoK3m6Trl2pPMB?YX|U5LZIIDE)Hs>{DAHn!-v&rL6F zY!DQ<3=U<%RaCb&?N0`_Gl`B{$uuv9@<4XCvTssv4l??u1r7N|msZp$ee8g|*-gRY9 z=F{r$$@T&}z7X*$O)2#UX;wJim11$)H`*2AK244Nh##hK@`l6iHtNHUt`YY7dScSF#!Z-{YrFVkF(e|eC#UU zKfxD^d)=zEa-uhDfenq5l*`Ju{jtxM?jRyWNA2I37r6EvP9CHJD=&!3mSuttaJLB! z%%9Y>Qf9Z<$F1hdnf0|i3Vby!zn&ts&VPlVc0@!p;|?F*2EEMlo;00E%H1teP6xh` z^|K=J3D}FfS22r>*fW&d1cM|6KCmd-EZuV2S*1nn_4+-E+(i+hOeFZquSW>M&(Uo! zl^#U;K=>Z@pe2qQm+C3w=`$r=vrpA^8t?BpK>~|{kW!=K+L60-&zqZ}9u@&{C$n7x zRbmcr`pL?*Ef2V_&*kOlB zSz!0FFoE}Qic74uWqJiOV9Vn3`K*Hf0%ElgGcBq^ zT>P;1HtI>~4zIpUrB%$@+fFdRc}7n;4^ZcKe@)M(3Y?GxCV-(O>#6gs{y#KD2K`Iw z4729Ev36{(@Wr)41TNKmY>~K*$n}Bc?G*1elr)qStBvYh93blB34yntv{Dqi`()wU z6~%VW=tpHv$b&ILZLQ9>9=8c4&}`Ors*sh1pd$G@Pv=N;I`A={Lcs+t~l-k?m~U&pvvITpm+?p0+!5aZYmYjBMQU9Cj~)=-jWwyr?Au z_p{M+S=%VI zy8Pd}xa-uo;N8MkLZKzZnRfm2_HZSeDK8h2t=@SQ2apwS?_i5GhLF9lq0upVj}lE; zm==z#k0-3f55oz{d7?H$XS|u(8fN;gfy#fry6k_huol{XSG$o-XY7mze(v`l zbPK`j?Sk6J>bZhc;r__hnUZ3d85h^e+~t$uT3oWMKB=VVXj>2Ts362t{e3EXa+jXj z$G1`OCsK?%z>!kgMUb`>ukVB=u4Oi}c&h6bRBC%3$JEN+R!zrJCZclyrw=0tSK9L`XBz`IyEQY_B!;Egk0oT6P z$MR^q%Whh%(d+lvX5rgs-tvjmN7p@e0J|_2YnOghilCJ48H)T>lw>tr=PYDKogz;}Pq^09>W}3Sn(7JZGnmEY zgW7GSKJjvmq;Bax-}bKW@vYB8)t@{w!i$YHm@1ahNx&o4=Eo=;e}d~930*glPyQuh z8i59R&y0b_D(;UCB@eQDfi&>MH*;`eAGCI9?Q+?{3VzIUKPy+^fxB5+A889ZKC%8Fl=SEGrDiYAzy*_gkNy3{s^eCwvVo08YlXzN=sb zU!MTT+*RUduagJO!sU>dkq8w@`k6}vwLFdzJT^gQuxG3mJ8?4Obv$f$iZFhq|1ZLr z@!G9sKo2KIO8$c|MnOKG4QJcy8jppFDLWqjMVF-+ms@I50PBQWx&DhS ze)}^=gDrNc?x9%W1ov|1tP11cl|#o_SG;XsW0f-ePt)6jX3DV=>=NGOtCR;4kOr^e zEZG={f4dph znl^rBD)26eJ_{Nq?;@q%a|UUgOBwVSTm|u)AHqrG!kkirTby6x1sC?M%W3;qjcE3dPXIi9aB%yw~YE?O$PU6Zt z@L#s`R|@(1I2DgnwB`0P0at+^69kKUAqO`;{>&L&3B@HW z2{ysF@Jqc5bD{kaO5%jZ6+I&qURqVu7WM9-jLZ*kh(QOpD$IMdsi3kD-i+ z47vjsLk8qU=ET&L|J9L{U#ko$n2GLG2Jod4lj_ekIlVkk52th8M6OaVK2hsBIG{u@ z-Sd;yb@;rk79)DBrX4u4nEni?Enjcu>oc9-77vhf;Sf1GeAs){XWHkz`smf}_@!Nk zNBcsPC0!~eX>?TrTl zorH`2`ZH8UL66$e0&6u)n00;v3*4;mfmk0m;{6pBF1Wh5dj^>u@~*7*S7-YuKzR#; zL@k~f)&z)8#~OaO%$=Evttpl$ierLhSU+DG$RO4}ygBAUO(~esT|2AfknO(Z zA7KZl))q9ycjYz5>}sT~acnO!N8btp1njB}Eeunw!!RygY8Wta)GK|`F|B*!ak+)t zk^(535H+^e0bmLiD=^mxb+Wy#T_p1Pk}YBMv@u#yd7y2&%m>wK1asN#^T8rZW^=IkxkhDf#fD+MeaiH?|#4kY0L(D<3Ff(S+vKon5u&jZ&m*}QhX$v zy56yO==O}?&EHxW6Bs&XauS>D-Z>_0c|2rE-cvKV7xyaal^I3|es<(&E_GGC2i`j9 z;uM$QL|HY-l}1{B>ihjjgcK}nzbd>yEL!w)~qu? z`fCBLQ)0B*-bSlFS^mfA0z;+}a?7jF+-okIf`9n5s9|2I*UFr-_f@tB{Y;K75Bjm< zFDaKvS!u2wROuJzNrhBD#jw`nZ|%I*ErADSV+Em+k3hIyG?J__IvL~f!k8wSItT7-<_vCnt+9i)`X|5g(k8Lb=0w= z7dp29lr2)KdTU@v$W#4$TC~>#Ip(J zJz-9Dshcky1aXH7rn>ZR#pVXrR2ODW1@?!cO1jsyZLEHPL!GcP-AK-b)Qjj^Ns{fG zhnbm0hA7Pjw)6Bzzk}|Z8`++N8KBGNM^+8MN0%lqPF)V=!l9NYQw5(le zTpmC{>MMXIko8skC6L)Q4Ve0IX_V8>Tuv%M7^sM2A0Y~w)8kL!E!iGw;I?aZ zc6RSnxGr*OL#C|%ZIR}~m>@Z?#W>VH5XEv3 zqnAxC4Am$2G=)ifL8g;6Foui)=dF10 zPG5rgo=M*+-AQ_@TW{{M>vWR6Grzh-rjGUf+bdUncDNan_lcO!`R?&+>x}M!cBt(4 zx}Db#*GF@U(YK>u*{r{ z9D$~!OCY<*-v4jK`T08bWnaJpEC~C!$WRQ>m75I=8xOJYjR_02;hq(;)4ve-*z#qx zx`l>;G=$#bWb-u|76|)9;}c)H4cDKB9s}J*klNLWCCnmErcYmv;C~QsQ@uxMMQkDM zr>y2qg<2jF!Z}J5fcr${xwvA)qx1c{H(z0ZF2?m^A>n@+peoEbLcHvPJc+1$b%I`q za32t}cwsfQ-v+o!KP;0I9kLOztyF*n%EjipR5=Ilk%pRQ7sq5O4J1v$2YxOHGqlFk zeM-vh+_h&64Qh;|y?w7?3@EUrJf|yU#bW~)&9=?~hVa$yMOrU1yWxKZ;33vcol*8i zpjJ)Mmv8#toXvfPnlkw3ws}?DTR4bbI+_O(?$s&%@nv_Y*N+;#>NKxMGVH=Pa!y z*9%j=a`TS`rnTQYHuM!xS6LV*N9 zRyBDnwl?4~2$N0HfUe-{olN&`lkTD#ZUq6JL+dA+z~CeBAjh=rI8 zWvob=pPk^k>Uxs#)(gYg_Z&}TuP$?pN}l97l~;l4=qnbBGnaiPMQu>*oHwpOQ~ zD@ts&b6US;6nBB5KtSu}XCeW>;Q5L~yFZdy@dXFl+cSoR#;ETT( zRnTm1_IsaC&#I^v=hb)M#NR|b56#4!o7=cZVbcSaaZv`Hw=RYWW~Z>}W$z{#G%A^i zF16*Z6kUu4Wt_c3(HnheQOxWngyrUY&lxPZdtun{yS)y(a^@!?mjcP2#_-;|B6can ztHD^8qL%dTky2L@{cqrixVKtHDjnnQ9fnJK^SU{?t12o)Bz%tzVYsQDB+vEw?ol6q z@bKr}GgBKvoL6NtcJ8tUJnGtxcn@#Z`&F;NF*IZ^dn3_$skMwS;m9F&DEgxRhu#yB-QENu@=myZ3ddTvXruW@}Lb{I~x;OqMFzLEQjK>U-4H=VNoNyI`@-A|z_aJHz| zv8P9NNMQ;!E#94Pm}B+T^^2d&bSXCPmC&*7RTD2j2IM~rcdY3r74=lI11p|t#&82^0|ApOZ(Z+>2X@Na0!)^V*k@-;5aNxp}NekBghgsg*A?$(ZA~4)x zS_68+)Xhw@oF-||mNas2*KD>(P^k+(z+t_q?)_w)o8z3fOqK3Dzg-;RWF7kVuqt+rkjppktwMPU&u?#)REW4<8$1z3Px-=UQblZOfnkT*>7aC&xMg82tHg4?NtM><0uTV3_{Mzm{YL8NtR$~MX z^!qUdio-p{XUaIPfN)+2Hda{A8-s zf}+E%ocqD1y0K|DMYj}p6Vp)kk>@Si4bG_%LjVU$k%i&VPN1_=$C=w9YK&MGhrEYX zbP~!!%)Z;_Mps)#7EL#&vtyY7$>=Oy^kIu%VVMZMWz`5+({$6Y=@td1$&+7dYpGb;0M}bcL3=jp8)xV1GifdW%S< zxuQmq!G8E37rsfyv-f}f2T1}n0#N$9w(qVG7)9O<&wUS+ba<0C{!s)OB$)F02l)tdZHy$BQ{V!AIolb92{B zx~eIiVUDd|aZ^E*f7V0=HKaxuh9Z)&jJcCPRRV6Hxkaa0TuOEJoEGnM*b9rj5;8SA zO2y%tWeV+7aC<-U3Hld82#=0`WBX~3T!ptSG8ux&0KQ0^;!8U>W{|%y2-ghkZKtFj zY)UKq5ra3f@gGO*_SRgwa;zZ2IX^nM%f7va*s{7WMCiISb6xpvQFKUx6E9&*X0_iX zYg#fC>@dcMKY7tr11LhZX(2Jd4$qbV!17>KcpY63C0+Bi679`B)_P34YH)*am0|kF zo0lxU8dmH;@l`Y{`~D9ZD$5oe=^>J6fFORJ)>C)gkJueJP)-Sq*oPhl$m_7~l2|9g zf@X@~&N+i>`iuQX_4@_0Iuy^nI~O`&|Dhvnn^2WzV}UBL2VUlT%sP$Dm8SMh!}x^1 zB6?%&#Nr+~Zq5Cc3UyYL;8W=mvCpQp9hjW;zc}QF+rQur+x^YdaRYdThiBI?E@99eyhhU7W?Am%R_>)Q-{8;~dZ zM3L5vO4B~`+_nM^*Buug>#7g_P{(qQ*1rq7@uwtk>bb-FU4*McW)&lE$FINJ0(Bm? zU5sesWDxx+m9Pbs8l{`=5z~#_^`oB=iL26gKVadzd?)&ekw4%UH*o>n{ChSGI0W!L z8%U7INYfMFSq-L7!v@$`MOEx#X!j#+-qWWIQv_&f0@bY?$GPD*h=j|Zd&4He=Xk_`#W1Vv`O zdVKSF)nwWgefJ`MBh+FzhXgkt{9K3_%pOBlXTZ7KiGU$abwHTI88lRBqjiA=uXTeN z^p2GcX3=&=iO5#CrM}3V^8DS0T9zx(L4YBjRT6(&7^GVFJF)+23HizQGfG3#tHMEy z;0p-gzF6hyHdx^hHy?@FeQ7_uk%3lYLe-BzNM3(1`C*{h=FXtA+dMvM>F~G} z8kZ`oZZJ&_m*CaR`?!$%;iO6lT$}xorQ^&F;6n)HlVeC0pbST>H^s5=cv+|Jy^&tr zF^69}#Epf}iL?KW>E!WOVE%iRbH2dog{oEv)7`Z0rJ3|3BqkU3tjAMX0YxVf!dgb& zJ~eXhYyXs$;$jLZGY;~9`#8bBU$1%x4Ox5J*fd-KI1K;*k|)q+V;cb`V+9m5Z} zW)B{%l~r^lSPULAl_hM?ZajcyTcY(52GzGltZthki-sa4+Y>in2}!~-7hZ9jcP%6DhP$tDinSf|PTSR*Q`Ux3=e(xV%;koPN!!h^!=H6* zu2$BA$%v{ZX1jx>Rrob6pf#DNNbVQ3n&`6*1GI0 z(jw=YSvTp=zobU#VXgIhnp?Lz>dQKlc7xucZMJZZ^UEn2)61G}8?Wk?YIhn0=-qc) zq26V)Q%EefDz0d@{Y94SNA3kH50o%KGk!z<1suajj}ol_4ndIhl>SCmH@$(efV`r?mAB!mBqt6URmdc%p}Bg`4&-Dy=+Gf=fWCwUu??MhO$3`%?(Rj8PZV>=RvYpaJQze@c2=4|~Lo=UB|S zpQi>V9s%8--MM^!k(NB5z)HPzER;Y)JqR4iRVnB&e^LrH8Sxue z9PY>gwl;bTWechwc8^c_G2n`#nhS{RXaQI;$KIX67$SOkTYu5X^~V`xd5nd0sC3&k zm&_NR!$-`Lk{<=Z8*aAwk>on+^ZT<7iErJc8Jst?_DNqg#9eb!V9AQ8Vdyd7VJ+By z86|4HbD$$CmoqNnr_}a?p>2a@=(kBaqtw7WnE677d7liATzdzbmH$Q6llgqGNl>A$ zndtSQ*Zc*h%CnMr(^pR(issaII)e=PhJsdQMXk7x;d(c(<)xRJ!%+R#Fn}U_1L0o{ z2T84EZ-uzIgs*{Kj7i(l|6XQ8A63V>F@iTc`ekyfUuQgzwVCOEa}9r2%0mct9S)kL zdR^CUoQO2tlO(tNKE~G@Vj9m&=I-;g_o8$P?<^6{kz^ZDBMP{=y((XQ)Zf&jPNgJK zbrK+HG0J9dVWPFr6AzRgi+)_NwsvJkN+`)yMn`*CU;0f%?CJ+bdUW^1jfe$=!Oimy zIz`J*1wdM2Z*Xn)*4|Zz8+$G2oPlQqJ{& z7g`)<#rqt-`gh2buN2Y1^A%k=wRGZ-Y*#*y=3>!U|voT`p?z1bh{SB35>1 z!rW+Y=vh9Iq31_$F;QW`J6RK_cHpMU52VZ3Ql=GwqzuT4$ps{IV~=l;dI~1{-sMyM z;=oY{>DyoS+x}ad`!vm^x+0&?kL@}&SGi1giD%+67kQtkdmWweBuQ{0HdMX!Get+6Q9 z3LN2?mpH`@%m3g;O@G7g;SsDa?EOG2Yt-?5XU}42<^)mDI{@Eg^WZ{?;D972y1s2w&0Rb=QjdpsGLTx^AIwT7N8&e^6qxRGbnN{=x!E#e2J; z;lVw2ytzvhU6UEkD}IHh3=UhLHFds4lEX8(=L43QL3t#UEQHh_^Wvx5q^XmGjz$st zi0`QF%{PMPvGG&Rs`J7gg)s`{%V76RrRHhFE`^((ARrcYTrSHay!N8nOvwCz}@YDzS;5UuENu{hRPef16Q+hdBR2oZ6P|=PZUruAzj?!}{}fsI7xsi@P^6_x}xQ z;U4qwlGz=JI&A5HmIW|d@dTRdtEfqFRK;eu?!4z;QyM5VAd{wpu>n_(tCRVi-M{68 zZ4#F@FtArhE$>^_dFDr<BVN8ej$+M6Orp7<23aW|x@gFf&ZUwL*zagJ|*ttbyFA zDY4+NC91d9b7Z^DJ{JmkWgZ-}f8l;!i+Bso&m40s|A%!z7Jq!SX^!->lrJUD^yG}? zaX^V|zQz%IcxzXxFeYm5jIUBOvWWNtf&8qXqcki$a-<$5Qzd>3bV5s{IPwQ3lB;Us zm@CKN{NHN#2#ER(7nNEpS__@n#W&kd*T&X)+df7g*k7_&HX3HzEp-*oJuJp{gY5cb#_2&JfG?}g3kSFw*M811aloA?lgPma05fx;9eYsje z2SVl2-XNV{+kmoZ-cB~IHXwVQ-Iu8g91{_1 zp}Hm*{6eawiU7vaW6vdWPu9)a^)xT;L&g>SCj~*UIHfVOxk?= z`uWRQqUF07!I@DRmJou-P*ZSsK%NuP$e;+%zvMd@xRK=pJFA5uJsJok&F%e=t>5*2 z`H7e|;&`bCTAEYO{SMgXT7iL~kJp#Rq9dL)H!gkWJXKZrdNZ@Pv07$;rOfcUy9#{F zsyw^R?~kygfA4bO>TIG()4J!-jneFo5?`3kI%T_Shv8@<`J1cFcb!3*U>xv?R`d*X zW=w#2%s!Fr=QC?}lOOBdySvr-fLr=fR>~G3-AQG!Qw6^M)aP^Ufwt(%xs`6Yj5In| z<5LuXwuCQmP~y`}@tf)f8ozp9XeH5Y78K_czvBW0X%;AgniqHR^wo2HPq2#S zmtI6ZWQpMOc^>AAR)`$$oeO!9Lnyrt9&!*L@=%x_$ zNi1rq(^k9{u7YZMpKGpM+G2^)`7MFp8EoDpdP*SKM^t5OijFg

v6cMYG-RuW9#0CMiLR<<1j$f%>oQ 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

GIAS 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

~8Nr_7GvNe1dT|DHy{7*$s$B{ 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 literal 0 HcmV?d00001 diff --git a/docs/docs/cloud/index.md b/docs/docs/cloud/index.md index 708c23d82..b23908dcb 100644 --- a/docs/docs/cloud/index.md +++ b/docs/docs/cloud/index.md @@ -1,5 +1,11 @@ # LangGraph Cloud (beta) +!!! tip + - LangGraph is an MIT-licensed open-source library, which we are committed to maintaining and growing for the community. + - LangGraph Cloud is an optional managed hosting service for LangGraph, which provides additional features geared towards production deployments. + - We are actively contributing improvements back to LangGraph informed by our work on LangGraph Cloud. + - You can always deploy LangGraph applications on your own infrastructure using the open-source LangGraph project. + !!! danger "Important" LangGraph Cloud is a closed source, paid product in an invite-only stage. We are currently focused on providing high bandwidth support to make our select early customers successful. If you are interested in applying for access, please fill out [this form](https://www.langchain.com/langgraph-cloud-beta). From ed5d114087c5c9817e02310f45262e08723cdc00 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 12:19:43 -0700 Subject: [PATCH 15/28] Fix --- libs/langgraph/langgraph/pregel/loop.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index b8c3fdc4e..c49d20697 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -73,10 +73,10 @@ class PregelLoop: checkpointer: Optional[BaseCheckpointSaver] checkpointer_get_next_version: Callable[[Optional[V]], V] checkpointer_put_writes: Optional[ - Callable[[RunnableConfig, Sequence[tuple[str, Any]], str], ...] + Callable[[RunnableConfig, Sequence[tuple[str, Any]], str], Any] ] checkpointer_put: Optional[ - Callable[[RunnableConfig, Checkpoint, CheckpointMetadata], ...] + Callable[[RunnableConfig, Checkpoint, CheckpointMetadata], Any] ] graph: "Pregel" From afd940d9bd975bf134ee3b16877743b1377ba740 Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 19 Jul 2024 12:38:55 -0700 Subject: [PATCH 16/28] first draft --- docs/_scripts/copy_notebooks.py | 1 + docs/docs/how-tos/index.md | 1 + docs/mkdocs.yml | 1 + examples/node-retries.ipynb | 139 ++++++++++++++++++++++++ libs/langgraph/langgraph/graph/state.py | 1 + 5 files changed, 143 insertions(+) create mode 100644 examples/node-retries.ipynb diff --git a/docs/_scripts/copy_notebooks.py b/docs/_scripts/copy_notebooks.py index 5954e7ef5..703fd5dcd 100644 --- a/docs/_scripts/copy_notebooks.py +++ b/docs/_scripts/copy_notebooks.py @@ -56,6 +56,7 @@ _MANUAL = { "human_in_the_loop/time-travel.ipynb", "human_in_the_loop/edit-graph-state.ipynb", "human_in_the_loop/wait-user-input.ipynb", + "node-retries.ipynb", ], "tutorials": [ "introduction.ipynb", diff --git a/docs/docs/how-tos/index.md b/docs/docs/how-tos/index.md index d98b9e255..b1783e492 100644 --- a/docs/docs/how-tos/index.md +++ b/docs/docs/how-tos/index.md @@ -68,6 +68,7 @@ These guides show how to use different streaming modes. - [How to add runtime configuration to your graph](configuration.ipynb) - [How to use a Pydantic model as your state](state-model.ipynb) - [How to use a context object in state](state-context-key.ipynb) +- [How to add node retries](node-retries.ipynb) ## Prebuilt ReAct Agent diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index daf74c894..e7e1f71e5 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -163,6 +163,7 @@ nav: - Add runtime configuration: how-tos/configuration.ipynb - Use Pydantic model as state: how-tos/state-model.ipynb - Use a context object in state: how-tos/state-context-key.ipynb + - Add node retries: how-tos/node-retries.ipynb - Prebuilt ReAct Agent: - Create a ReAct agent: how-tos/create-react-agent.ipynb - Add memory to a ReAct agent: how-tos/create-react-agent-memory.ipynb diff --git a/examples/node-retries.ipynb b/examples/node-retries.ipynb new file mode 100644 index 000000000..29f59e7dd --- /dev/null +++ b/examples/node-retries.ipynb @@ -0,0 +1,139 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# How to add node retry policies\n", + "\n", + "There are many use cases where you may wish for your node to have a custom retry policy, for example if you are making HTTP requests, calling an API, etc. \n", + "\n", + "## Defining your retry policy\n", + "\n", + "In order to configure the retry policty, you have to pass the `retry` parameter to the `add_node` function. The `retry` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "from langgraph.pregel.types import default_retry_on\n", + "from langgraph.pregel import RetryPolicy\n", + "\n", + "# Note: This is equivalent to writing retry_policy=RetryPolicy() since these are all default values\n", + "retry_policy = RetryPolicy(\n", + " initial_interval = 0.5,\n", + " backoff_factor = 2.0,\n", + " max_interval = 128.0,\n", + " max_attempts = 3,\n", + " jitter = True,\n", + " retry_on = default_retry_on\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can then change the default values to work for your custom use case:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "RetryPolicy(initial_interval=0.5, backoff_factor=2.0, max_interval=128.0, max_attempts=5, jitter=True, retry_on=)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "retry_policy = retry_policy._replace(max_attempts = 5)\n", + "retry_policy" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "If you want more information on what each of the parameters does, be sure to read the source code.\n", + "\n", + "## Passing your retry policy to a node\n", + "\n", + "Lastly, we can pass `retry_policy` to the `add_node` function:" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "import operator\n", + "from typing import Annotated, Sequence, TypedDict\n", + "\n", + "from langchain_anthropic import ChatAnthropic\n", + "from langchain_core.messages import BaseMessage\n", + "\n", + "from langgraph.graph import END, StateGraph, START\n", + "\n", + "\n", + "model = ChatAnthropic(model_name=\"claude-2.1\")\n", + "\n", + "\n", + "class AgentState(TypedDict):\n", + " messages: Annotated[Sequence[BaseMessage], operator.add]\n", + "\n", + "\n", + "def _call_model(state):\n", + " response = model.invoke(state[\"messages\"])\n", + " return {\"messages\": [response]}\n", + "\n", + "# Define a new graph\n", + "workflow = StateGraph(AgentState)\n", + "workflow.add_node(\"model\", _call_model,retry=retry_policy)\n", + "workflow.add_edge(START, \"model\")\n", + "workflow.add_edge(\"model\", END)\n", + "\n", + "app = workflow.compile()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now the `\"model\"` node will be configured to use the custom retry policy that was defined above." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/libs/langgraph/langgraph/graph/state.py b/libs/langgraph/langgraph/graph/state.py index abde9d0e9..96ad8e513 100644 --- a/libs/langgraph/langgraph/graph/state.py +++ b/libs/langgraph/langgraph/graph/state.py @@ -254,6 +254,7 @@ class StateGraph(Graph): action (Optional[RunnableLike]): The action associated with the node. (default: None) metadata (Optional[dict[str, Any]]): The metadata associated with the node. (default: None) input (Optional[Type[Any]]): The input schema for the node. (default: the graph's input schema) + retry (Optional[RetryPolicy]): The policy for retrying the node. (default: None) Raises: ValueError: If the key is already being used as a state key. From d7dc16310fc598c92e16d5b31adebce7a6105920 Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 19 Jul 2024 13:05:28 -0700 Subject: [PATCH 17/28] nuno comments --- examples/node-retries.ipynb | 76 ++++++++++++------------------------- 1 file changed, 25 insertions(+), 51 deletions(-) diff --git a/examples/node-retries.ipynb b/examples/node-retries.ipynb index 29f59e7dd..93749e065 100644 --- a/examples/node-retries.ipynb +++ b/examples/node-retries.ipynb @@ -6,113 +6,87 @@ "source": [ "# How to add node retry policies\n", "\n", - "There are many use cases where you may wish for your node to have a custom retry policy, for example if you are making HTTP requests, calling an API, etc. \n", + "There are many use cases where you may wish for your node to have a custom retry policy, for example if you are calling an API, querying a databse, or calling an LLM, etc. \n", "\n", - "## Defining your retry policy\n", - "\n", - "In order to configure the retry policty, you have to pass the `retry` parameter to the `add_node` function. The `retry` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters." + "In order to configure the retry policty, you have to pass the `retry` parameter to the `add_node` function. The `retry` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters:" ] }, { "cell_type": "code", - "execution_count": 8, - "metadata": {}, - "outputs": [], - "source": [ - "from langgraph.pregel.types import default_retry_on\n", - "from langgraph.pregel import RetryPolicy\n", - "\n", - "# Note: This is equivalent to writing retry_policy=RetryPolicy() since these are all default values\n", - "retry_policy = RetryPolicy(\n", - " initial_interval = 0.5,\n", - " backoff_factor = 2.0,\n", - " max_interval = 128.0,\n", - " max_attempts = 3,\n", - " jitter = True,\n", - " retry_on = default_retry_on\n", - ")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "You can then change the default values to work for your custom use case:" - ] - }, - { - "cell_type": "code", - "execution_count": 9, + "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "RetryPolicy(initial_interval=0.5, backoff_factor=2.0, max_interval=128.0, max_attempts=5, jitter=True, retry_on=)" + "RetryPolicy(initial_interval=0.5, backoff_factor=2.0, max_interval=128.0, max_attempts=3, jitter=True, retry_on=)" ] }, - "execution_count": 9, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "retry_policy = retry_policy._replace(max_attempts = 5)\n", - "retry_policy" + "from langgraph.pregel import RetryPolicy\n", + "\n", + "RetryPolicy()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "If you want more information on what each of the parameters does, be sure to read the source code.\n", + "If you want more information on what each of the parameters does, be sure to read the [reference](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph.add_node).\n", "\n", - "## Passing your retry policy to a node\n", + "## Passing a retry policy to a node\n", "\n", - "Lastly, we can pass `retry_policy` to the `add_node` function:" + "Lastly, we can pass `RetryPolicy` objects when we call the `add_node` function. In the example below we pass two different retry policies to each of our nodes:" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ "import operator\n", + "import requests\n", + "import sqlite3\n", "from typing import Annotated, Sequence, TypedDict\n", "\n", "from langchain_anthropic import ChatAnthropic\n", "from langchain_core.messages import BaseMessage\n", "\n", "from langgraph.graph import END, StateGraph, START\n", + "from langchain_community.utilities import SQLDatabase\n", + "from langchain_core.messages import AIMessage\n", "\n", + "db = SQLDatabase.from_uri(\"sqlite:///:memory:\")\n", "\n", "model = ChatAnthropic(model_name=\"claude-2.1\")\n", "\n", - "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]\n", "\n", + "def query_database(state):\n", + " query_result = db.run(\"SELECT * FROM Artist LIMIT 10;\")\n", + " return {\"messages\": [AIMessage(content=query_result)]}\n", "\n", - "def _call_model(state):\n", + "def call_model(state):\n", " response = model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", - "workflow.add_node(\"model\", _call_model,retry=retry_policy)\n", + "workflow.add_node(\"query_database\",query_database, retry=RetryPolicy(retry_on=sqlite3.OperationalError))\n", + "workflow.add_node(\"model\", call_model, retry=RetryPolicy(max_attempts=5))\n", "workflow.add_edge(START, \"model\")\n", - "workflow.add_edge(\"model\", END)\n", + "workflow.add_edge(\"model\",\"query_database\")\n", + "workflow.add_edge(\"query_database\", END)\n", "\n", "app = workflow.compile()" ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "Now the `\"model\"` node will be configured to use the custom retry policy that was defined above." - ] } ], "metadata": { From 2a7ae83bf83f583c06d6fc73bf31097ec5256d36 Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 19 Jul 2024 13:07:26 -0700 Subject: [PATCH 18/28] spelling --- examples/node-retries.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/node-retries.ipynb b/examples/node-retries.ipynb index 93749e065..6798852f3 100644 --- a/examples/node-retries.ipynb +++ b/examples/node-retries.ipynb @@ -6,7 +6,7 @@ "source": [ "# How to add node retry policies\n", "\n", - "There are many use cases where you may wish for your node to have a custom retry policy, for example if you are calling an API, querying a databse, or calling an LLM, etc. \n", + "There are many use cases where you may wish for your node to have a custom retry policy, for example if you are calling an API, querying a database, or calling an LLM, etc. \n", "\n", "In order to configure the retry policty, you have to pass the `retry` parameter to the `add_node` function. The `retry` parameter takes in a `RetryPolicy` named tuple object. Below we instantiate a `RetryPolicy` object with the default parameters:" ] From 55c78bbf90ad6951faca81490df44269791c52a6 Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 19 Jul 2024 13:13:02 -0700 Subject: [PATCH 19/28] remove unneccesary imports --- examples/node-retries.ipynb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/node-retries.ipynb b/examples/node-retries.ipynb index 6798852f3..498acd1ab 100644 --- a/examples/node-retries.ipynb +++ b/examples/node-retries.ipynb @@ -46,12 +46,11 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 21, "metadata": {}, "outputs": [], "source": [ "import operator\n", - "import requests\n", "import sqlite3\n", "from typing import Annotated, Sequence, TypedDict\n", "\n", From eaa23ef4e51c5730b3761db6894f353725194285 Mon Sep 17 00:00:00 2001 From: isaac hershenson Date: Fri, 19 Jul 2024 14:56:14 -0700 Subject: [PATCH 20/28] added retrypolicy reference --- docs/docs/reference/graphs.md | 6 +++++- examples/node-retries.ipynb | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/docs/reference/graphs.md b/docs/docs/reference/graphs.md index 6a2dfd613..497e16d14 100644 --- a/docs/docs/reference/graphs.md +++ b/docs/docs/reference/graphs.md @@ -65,4 +65,8 @@ builder.add_conditional_edges("my_node", my_condition) ## Send -::: langgraph.constants.Send \ No newline at end of file +::: langgraph.constants.Send + +## RetryPolicy + +::: langgraph.pregel.types.RetryPolicy \ No newline at end of file diff --git a/examples/node-retries.ipynb b/examples/node-retries.ipynb index 498acd1ab..c50230ded 100644 --- a/examples/node-retries.ipynb +++ b/examples/node-retries.ipynb @@ -37,7 +37,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "If you want more information on what each of the parameters does, be sure to read the [reference](https://langchain-ai.github.io/langgraph/reference/graphs/#langgraph.graph.StateGraph.add_node).\n", + "If you want more information on what each of the parameters does, be sure to read the [reference](https://langchain-ai.github.io/langgraph/reference/graphs/#retrypolicy).\n", "\n", "## Passing a retry policy to a node\n", "\n", From dc703857dd5b4ded719a9ca5c9f76789e69b9344 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 15:43:40 -0700 Subject: [PATCH 21/28] Run tests in parallel --- libs/langgraph/poetry.lock | 37 ++++++++++++++++++++++++++++++++++- libs/langgraph/pyproject.toml | 3 ++- 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index f068e1405..b77864484 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -747,6 +747,20 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "execnet" +version = "2.1.1" +description = "execnet: rapid multi-Python deployment" +optional = false +python-versions = ">=3.8" +files = [ + {file = "execnet-2.1.1-py3-none-any.whl", hash = "sha256:26dee51f1b80cebd6d0ca8e74dd8745419761d3bef34163928cbebbdc4749fdc"}, + {file = "execnet-2.1.1.tar.gz", hash = "sha256:5189b52c6121c24feae288166ab41b32549c7e2348652736540b9e6e7d4e72e3"}, +] + +[package.extras] +testing = ["hatch", "pre-commit", "pytest", "tox"] + [[package]] name = "executing" version = "2.0.1" @@ -2784,6 +2798,27 @@ files = [ tomli = {version = ">=2.0.1,<3.0.0", markers = "python_version < \"3.11\""} watchdog = ">=2.0.0" +[[package]] +name = "pytest-xdist" +version = "3.6.1" +description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest_xdist-3.6.1-py3-none-any.whl", hash = "sha256:9ed4adfb68a016610848639bb7e02c9352d5d9f03d04809919e2dafc3be4cca7"}, + {file = "pytest_xdist-3.6.1.tar.gz", hash = "sha256:ead156a4db231eec769737f57668ef58a2084a34b2e55c4a8fa20d861107300d"}, +] + +[package.dependencies] +execnet = ">=2.1" +psutil = {version = ">=3.0", optional = true, markers = "extra == \"psutil\""} +pytest = ">=7.0.0" + +[package.extras] +psutil = ["psutil (>=3.0)"] +setproctitle = ["setproctitle"] +testing = ["filelock"] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -4130,4 +4165,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<4.0" -content-hash = "170eaa0e542a02d5f2fb0d42d1f04c5d010bd3735a44b927b28e6b742c689eb0" +content-hash = "5fb6190a1b01d0cd351ea9a0023c8c8d6acf4fe831101ab87f9fc41308f74b74" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 14c90c401..7b4ab2078 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -31,6 +31,7 @@ langchainhub = "^0.1.14" langchain-openai = ">=0.1.2" langchain-anthropic = ">=0.1.8" dataclasses-json = "^0.6.7" +pytest-xdist = {extras = ["psutil"], version = "^3.6.1"} [tool.poetry.group.dev] optional = true @@ -61,7 +62,7 @@ omit = ["tests/*"] [tool.pytest-watcher] now = true delay = 0.1 -runner_args = ["-x", "--ff", "-vv", "--snapshot-update"] +runner_args = ["-x", "--ff", "-v", "-n", "auto", "--dist", "worksteal", "--snapshot-update", "--tb", "short"] patterns = ["*.py"] [build-system] From 27cd4e222146ff84240f424ed6a0b822c78ddd1e Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 15:44:21 -0700 Subject: [PATCH 22/28] Update core --- libs/langgraph/poetry.lock | 6 +++--- libs/langgraph/tests/__snapshots__/test_pregel.ambr | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index b77864484..3bee7a407 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -1760,13 +1760,13 @@ langchain-core = ">=0.2.2rc1,<0.3" [[package]] name = "langchain-core" -version = "0.2.19" +version = "0.2.22" description = "Building applications with LLMs through composability" optional = false python-versions = "<4.0,>=3.8.1" files = [ - {file = "langchain_core-0.2.19-py3-none-any.whl", hash = "sha256:5b3cd34395be274c89e822c84f0e03c4da14168c177a83921c5b9414ac7a0651"}, - {file = "langchain_core-0.2.19.tar.gz", hash = "sha256:13043a83e5c9ab58b9f5ce2a56896e7e88b752e8891b2958960a98e71801471e"}, + {file = "langchain_core-0.2.22-py3-none-any.whl", hash = "sha256:7731a86440c0958b3186c003fb9b26b2d5a682a6344bda7bfb9174e2898f8b43"}, + {file = "langchain_core-0.2.22.tar.gz", hash = "sha256:582d6f929a43b830139444e4124123cd415331ad62f25757b1406252958cdcac"}, ] [package.dependencies] diff --git a/libs/langgraph/tests/__snapshots__/test_pregel.ambr b/libs/langgraph/tests/__snapshots__/test_pregel.ambr index bbfb425eb..250acc712 100644 --- a/libs/langgraph/tests/__snapshots__/test_pregel.ambr +++ b/libs/langgraph/tests/__snapshots__/test_pregel.ambr @@ -509,10 +509,10 @@ ''' # --- # name: test_conditional_state_graph - '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "The final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' + '{"title": "LangGraphInput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- # name: test_conditional_state_graph.1 - '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "The final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' + '{"title": "LangGraphOutput", "type": "object", "properties": {"input": {"title": "Input", "type": "string"}, "agent_outcome": {"title": "Agent Outcome", "anyOf": [{"$ref": "#/definitions/AgentAction"}, {"$ref": "#/definitions/AgentFinish"}]}, "intermediate_steps": {"title": "Intermediate Steps", "type": "array", "items": {"type": "array", "minItems": 2, "maxItems": 2, "items": [{"$ref": "#/definitions/AgentAction"}, {"type": "string"}]}}}, "definitions": {"AgentAction": {"title": "AgentAction", "description": "Represents a request to execute an action by an agent.\\n\\nThe action consists of the name of the tool to execute and the input to pass\\nto the tool. The log is used to pass along extra information about the action.", "type": "object", "properties": {"tool": {"title": "Tool", "type": "string"}, "tool_input": {"title": "Tool Input", "anyOf": [{"type": "string"}, {"type": "object"}]}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentAction", "enum": ["AgentAction"], "type": "string"}}, "required": ["tool", "tool_input", "log"]}, "AgentFinish": {"title": "AgentFinish", "description": "Final return value of an ActionAgent.\\n\\nAgents return an AgentFinish when they have reached a stopping condition.", "type": "object", "properties": {"return_values": {"title": "Return Values", "type": "object"}, "log": {"title": "Log", "type": "string"}, "type": {"title": "Type", "default": "AgentFinish", "enum": ["AgentFinish"], "type": "string"}}, "required": ["return_values", "log"]}}}' # --- # name: test_conditional_state_graph.2 ''' From 1dbf7a33925e9c26c4d955cd82fa2f07d88eea47 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 16:37:27 -0700 Subject: [PATCH 23/28] Move all checkpoint edits to apply_writes --- libs/langgraph/langgraph/checkpoint/base.py | 10 +- libs/langgraph/langgraph/constants.py | 3 +- libs/langgraph/langgraph/pregel/__init__.py | 17 ++- libs/langgraph/langgraph/pregel/algo.py | 121 +++++++++++--------- libs/langgraph/langgraph/pregel/loop.py | 38 +++--- libs/langgraph/tests/test_prebuilt.py | 27 ++--- 6 files changed, 109 insertions(+), 107 deletions(-) diff --git a/libs/langgraph/langgraph/checkpoint/base.py b/libs/langgraph/langgraph/checkpoint/base.py index 8cb278f32..7f1ffab1f 100644 --- a/libs/langgraph/langgraph/checkpoint/base.py +++ b/libs/langgraph/langgraph/checkpoint/base.py @@ -1,5 +1,4 @@ from abc import ABC -from collections import defaultdict from datetime import datetime, timezone from typing import ( Any, @@ -79,7 +78,7 @@ class Checkpoint(TypedDict): The keys are channel names and the values are the logical time step at which the channel was last updated. """ - versions_seen: defaultdict[str, dict[str, Union[str, int, float]]] + versions_seen: dict[str, dict[str, Union[str, int, float]]] """Map from node ID to map from channel name to version seen. This keeps track of the versions of the channels that each node has seen. @@ -100,7 +99,7 @@ def empty_checkpoint() -> Checkpoint: ts=datetime.now(timezone.utc).isoformat(), channel_values={}, channel_versions={}, - versions_seen=defaultdict(dict), + versions_seen={}, pending_sends=[], current_tasks={}, ) @@ -113,10 +112,7 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint: id=checkpoint["id"], channel_values=checkpoint["channel_values"].copy(), channel_versions=checkpoint["channel_versions"].copy(), - versions_seen=defaultdict( - dict, - {k: v.copy() for k, v in checkpoint["versions_seen"].items()}, - ), + versions_seen={k: v.copy() for k, v in checkpoint["versions_seen"].items()}, pending_sends=checkpoint.get("pending_sends", []).copy(), current_tasks=checkpoint.get("current_tasks", {}).copy(), ) diff --git a/libs/langgraph/langgraph/constants.py b/libs/langgraph/langgraph/constants.py index 41970ae16..4a50e8833 100644 --- a/libs/langgraph/langgraph/constants.py +++ b/libs/langgraph/langgraph/constants.py @@ -1,10 +1,11 @@ from typing import Any +INPUT = "__input__" CONFIG_KEY_SEND = "__pregel_send" CONFIG_KEY_READ = "__pregel_read" INTERRUPT = "__interrupt__" TASKS = "__pregel_tasks" -RESERVED = {INTERRUPT, TASKS, CONFIG_KEY_SEND, CONFIG_KEY_READ} +RESERVED = {INTERRUPT, TASKS, CONFIG_KEY_SEND, CONFIG_KEY_READ, INPUT} TAG_HIDDEN = "langsmith:hidden" START = "__start__" diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 41b517e0a..14edeacbd 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -355,7 +355,7 @@ class Pregel( ) as channels, ManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = prepare_next_tasks( + next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -387,7 +387,7 @@ class Pregel( ) as channels, AsyncManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = prepare_next_tasks( + next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -429,7 +429,7 @@ class Pregel( ) as channels, ManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = prepare_next_tasks( + next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -475,7 +475,7 @@ class Pregel( ) as channels, AsyncManagedValuesManager( self.managed_values_dict, ensure_config(config), self ) as managed: - _, next_tasks = prepare_next_tasks( + next_tasks = prepare_next_tasks( checkpoint, self.nodes, channels, @@ -560,14 +560,14 @@ class Pregel( # deque.extend is thread-safe CONFIG_KEY_SEND: task.writes.extend, CONFIG_KEY_READ: partial( - local_read, checkpoint, channels, task.writes, config + local_read, checkpoint, channels, task, config ), }, ), ) # apply to checkpoint and save apply_writes( - checkpoint, channels, task.writes, self.checkpointer.get_next_version + checkpoint, channels, [task], self.checkpointer.get_next_version ) step = saved.metadata.get("step", -2) + 1 if saved else -1 @@ -652,14 +652,14 @@ class Pregel( # deque.extend is thread-safe CONFIG_KEY_SEND: task.writes.extend, CONFIG_KEY_READ: partial( - local_read, checkpoint, channels, task.writes, config + local_read, checkpoint, channels, task, config ), }, ), ) # apply to checkpoint and save apply_writes( - checkpoint, channels, task.writes, self.checkpointer.get_next_version + checkpoint, channels, [task], self.checkpointer.get_next_version ) step = saved.metadata.get("step", -2) + 1 if saved else -1 @@ -1145,7 +1145,6 @@ class Pregel( # exception will be handled in panic_or_proceed futures.clear() else: - print(loop.step, task.name, stream_modes) # save task writes to checkpointer loop.put_writes(task.id, task.writes) # yield updates output for the finished task diff --git a/libs/langgraph/langgraph/pregel/algo.py b/libs/langgraph/langgraph/pregel/algo.py index 46950b9f8..e93d6e14a 100644 --- a/libs/langgraph/langgraph/pregel/algo.py +++ b/libs/langgraph/langgraph/pregel/algo.py @@ -7,7 +7,9 @@ from typing import ( Iterator, Literal, Mapping, + NamedTuple, Optional, + Protocol, Sequence, Union, overload, @@ -29,6 +31,7 @@ from langgraph.constants import ( CONFIG_KEY_READ, CONFIG_KEY_SEND, INTERRUPT, + RESERVED, TAG_HIDDEN, TASKS, Send, @@ -41,6 +44,18 @@ from langgraph.pregel.read import PregelNode from langgraph.pregel.types import All, PregelExecutableTask, PregelTaskDescription +class WritesProtocol(Protocol): + name: str + writes: Sequence[tuple[str, Any]] + triggers: Sequence[str] + + +class PregelTaskWrites(NamedTuple): + name: str + writes: Sequence[tuple[str, Any]] + triggers: Sequence[str] + + def should_interrupt( checkpoint: Checkpoint, interrupt_nodes: Union[All, Sequence[str]], @@ -48,8 +63,7 @@ def should_interrupt( ) -> bool: version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) null_version = version_type() - # defaultdicts are mutated on access :( so we need to copy - seen = checkpoint["versions_seen"].copy()[INTERRUPT] + seen = checkpoint["versions_seen"].get(INTERRUPT, {}) return ( # interrupt if any channel has been updated since last interrupt any( @@ -72,21 +86,21 @@ def should_interrupt( def local_read( checkpoint: Checkpoint, channels: Mapping[str, BaseChannel], - writes: Sequence[tuple[str, Any]], + task: WritesProtocol, config: RunnableConfig, select: Union[list[str], str], fresh: bool = False, ) -> Union[dict[str, Any], Any]: if fresh: - checkpoint = create_checkpoint(checkpoint, channels, -1) + new_checkpoint = create_checkpoint(copy_checkpoint(checkpoint), channels, -1) context_channels = {k: v for k, v in channels.items() if isinstance(v, Context)} with ChannelsManager( {k: v for k, v in channels.items() if k not in context_channels}, - checkpoint, + new_checkpoint, config, ) as channels: all_channels = {**channels, **context_channels} - apply_writes(copy_checkpoint(checkpoint), all_channels, writes, None) + apply_writes(new_checkpoint, all_channels, [task], None) return read_channels(all_channels, select) else: return read_channels(channels, select) @@ -118,19 +132,46 @@ def increment(current: Optional[int], channel: BaseChannel) -> int: def apply_writes( checkpoint: Checkpoint, channels: Mapping[str, BaseChannel], - pending_writes: Sequence[tuple[str, Any]], + tasks: Sequence[WritesProtocol], get_next_version: Optional[Callable[[int, BaseChannel], int]], ) -> None: + # update seen versions + for task in tasks: + checkpoint["versions_seen"].setdefault(task.name, {}).update( + { + chan: checkpoint["channel_versions"][chan] + for chan in task.triggers + if chan in checkpoint["channel_versions"] + } + ) + + # Find the highest version of all channels + if checkpoint["channel_versions"]: + max_version = max(checkpoint["channel_versions"].values()) + else: + max_version = None + # Consume all channels that were read + for chan in { + chan for task in tasks for chan in task.triggers if chan not in RESERVED + }: + if channels[chan].consume(): + if get_next_version is not None: + checkpoint["channel_versions"][chan] = get_next_version( + max_version, channels[chan] + ) + + # clear pending sends if checkpoint["pending_sends"]: checkpoint["pending_sends"].clear() - pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list) # Group writes by channel - for chan, val in pending_writes: - if chan == TASKS: - checkpoint["pending_sends"].append(val) - else: - pending_writes_by_channel[chan].append(val) + pending_writes_by_channel: dict[str, list[Any]] = defaultdict(list) + for task in tasks: + for chan, val in task.writes: + if chan == TASKS: + checkpoint["pending_sends"].append(val) + else: + pending_writes_by_channel[chan].append(val) # Find the highest version of all channels if checkpoint["channel_versions"]: @@ -138,8 +179,8 @@ def apply_writes( else: max_version = None - updated_channels: set[str] = set() # Apply writes to channels + updated_channels: set[str] = set() for chan, vals in pending_writes_by_channel.items(): if chan in channels: try: @@ -153,6 +194,7 @@ def apply_writes( max_version, channels[chan] ) updated_channels.add(chan) + # Channels that weren't updated in this step are notified of a new step for chan in channels: if chan not in updated_channels: @@ -171,9 +213,8 @@ def prepare_next_tasks( config: RunnableConfig, step: int, for_execution: Literal[False], - get_next_version: Literal[None] = None, manager: Literal[None] = None, -) -> tuple[Checkpoint, list[PregelTaskDescription]]: +) -> list[PregelTaskDescription]: ... @@ -186,9 +227,8 @@ def prepare_next_tasks( config: RunnableConfig, step: int, for_execution: Literal[True], - get_next_version: Callable[[int, BaseChannel], int], manager: Union[None, ParentRunManager, AsyncParentRunManager], -) -> tuple[Checkpoint, list[PregelExecutableTask]]: +) -> list[PregelExecutableTask]: ... @@ -201,10 +241,8 @@ def prepare_next_tasks( step: int, *, for_execution: bool, - get_next_version: Union[None, Callable[[int, BaseChannel], int]] = None, manager: Union[None, ParentRunManager, AsyncParentRunManager] = None, -) -> tuple[Checkpoint, Union[list[PregelTaskDescription], list[PregelExecutableTask]]]: - checkpoint = copy_checkpoint(checkpoint) +) -> Union[list[PregelTaskDescription], list[PregelExecutableTask]]: tasks: Union[list[PregelTaskDescription], list[PregelExecutableTask]] = [] # Consume pending packets for packet in checkpoint["pending_sends"]: @@ -247,7 +285,11 @@ def prepare_next_tasks( local_write, writes.extend, processes, channels ), CONFIG_KEY_READ: partial( - local_read, checkpoint, channels, writes, config + local_read, + checkpoint, + channels, + PregelTaskWrites(packet.node, writes, triggers), + config, ), }, ), @@ -258,18 +300,14 @@ def prepare_next_tasks( ) else: tasks.append(PregelTaskDescription(packet.node, packet.arg)) - if for_execution: - checkpoint["pending_sends"].clear() - # Collect channels to consume - channels_to_consume = set() # Check if any processes should be run in next step # If so, prepare the values to be passed to them version_type = type(next(iter(checkpoint["channel_versions"].values()), None)) null_version = version_type() if null_version is None: - return checkpoint, tasks + return tasks for name, proc in processes.items(): - seen = checkpoint["versions_seen"][name] + seen = checkpoint["versions_seen"].get(name, {}) # If any of the channels read by this process were updated if triggers := sorted( chan @@ -280,22 +318,11 @@ def prepare_next_tasks( and checkpoint["channel_versions"].get(chan, null_version) > seen.get(chan, null_version) ): - channels_to_consume.update(triggers) try: val = next(_proc_input(step, name, proc, managed, channels)) except StopIteration: continue - # update seen versions - if for_execution: - seen.update( - { - chan: checkpoint["channel_versions"][chan] - for chan in proc.triggers - if chan in checkpoint["channel_versions"] - } - ) - if for_execution: if node := proc.get_node(): metadata = { @@ -333,7 +360,7 @@ def prepare_next_tasks( local_read, checkpoint, channels, - writes, + PregelTaskWrites(name, writes, triggers), config, ), }, @@ -345,19 +372,7 @@ def prepare_next_tasks( ) else: tasks.append(PregelTaskDescription(name, val)) - # Find the highest version of all channels - if checkpoint["channel_versions"]: - max_version = max(checkpoint["channel_versions"].values()) - else: - max_version = None - # Consume all channels that were read - if for_execution: - for chan in channels_to_consume: - if channels[chan].consume(): - checkpoint["channel_versions"][chan] = get_next_version( - max_version, channels[chan] - ) - return checkpoint, tasks + return tasks def _proc_input( diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index c49d20697..62bf6c806 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -38,13 +38,14 @@ from langgraph.checkpoint.base import ( copy_checkpoint, empty_checkpoint, ) -from langgraph.constants import INTERRUPT +from langgraph.constants import INPUT, INTERRUPT from langgraph.managed.base import ( AsyncManagedValuesManager, ManagedValueMapping, ManagedValuesManager, ) from langgraph.pregel.algo import ( + PregelTaskWrites, apply_writes, increment, prepare_next_tasks, @@ -88,6 +89,7 @@ class PregelLoop: checkpoint_metadata: CheckpointMetadata checkpoint_pending_writes: Optional[List[PendingWrite]] + step: int status: Literal[ "pending", "done", "interrupt_before", "interrupt_after", "out_of_steps" ] @@ -133,19 +135,13 @@ class PregelLoop: if self.input is not INPUT_DONE: self._first() - elif len({tid for tid, _, _ in self.checkpoint_pending_writes}) == len( - self.tasks - ): - # assign writes to tasks, apply them in order - grouped: dict[str, list[tuple[str, Any]]] = {} - for tid, k, v in self.checkpoint_pending_writes: - grouped.setdefault(tid, []).append((k, v)) - writes = [(k, v) for t in self.tasks for k, v in grouped.get(t.id, [])] + elif all(task.writes for task in self.tasks): + writes = [w for t in self.tasks for w in t.writes] # all tasks have finished apply_writes( self.checkpoint, self.channels, - writes, + self.tasks, self.checkpointer_get_next_version, ) # produce values output @@ -179,8 +175,7 @@ class PregelLoop: return False # prepare next tasks - prev_checkpoint = self.checkpoint - self.checkpoint, self.tasks = prepare_next_tasks( + self.tasks = prepare_next_tasks( self.checkpoint, self.graph.nodes, self.channels, @@ -188,7 +183,6 @@ class PregelLoop: self.config, self.step, for_execution=True, - get_next_version=self.checkpointer_get_next_version, manager=manager, ) @@ -202,9 +196,14 @@ class PregelLoop: for tid, k, v in self.checkpoint_pending_writes: if task := next((t for t in self.tasks if t.id == tid), None): task.writes.append((k, v)) + # TODO clear checkpoint_pending_writes + + # if all tasks have finished, re-tick + if all(task.writes for task in self.tasks): + return self.tick() # before execution, check if we should interrupt - if should_interrupt(prev_checkpoint, interrupt_before, self.tasks): + if should_interrupt(self.checkpoint, interrupt_before, self.tasks): self.status = "interrupt_before" return False @@ -219,7 +218,7 @@ class PregelLoop: # map inputs to channel updates if input_writes := deque(map_input(self.graph.input_channels, self.input)): # discard any unfinished tasks from previous checkpoint - self.checkpoint, _ = prepare_next_tasks( + discard_tasks = prepare_next_tasks( self.checkpoint, self.graph.nodes, self.channels, @@ -227,20 +226,19 @@ class PregelLoop: self.config, self.step, for_execution=True, - get_next_version=self.checkpointer_get_next_version, ) # apply input writes apply_writes( self.checkpoint, self.channels, - input_writes, + discard_tasks + [PregelTaskWrites(INPUT, input_writes, [])], self.checkpointer_get_next_version, ) # save input checkpoint self._put_checkpoint({"source": "input", "writes": self.input}) else: # no input is taken as signal to proceed past previous interrupt - self.checkpoint = copy_checkpoint(self.checkpoint) + self.checkpoint["versions_seen"].setdefault(INTERRUPT, {}) for k in self.channels: if k in self.checkpoint["channel_versions"]: version = self.checkpoint["channel_versions"][k] @@ -327,7 +325,7 @@ class SyncPregelLoop(PregelLoop, ContextManager): **saved.config.get("configurable", {}), }, } - self.checkpoint = saved.checkpoint + self.checkpoint = copy_checkpoint(saved.checkpoint) self.checkpoint_metadata = saved.metadata self.checkpoint_pending_writes = saved.pending_writes @@ -396,7 +394,7 @@ class AsyncPregelLoop(PregelLoop, AsyncContextManager): **saved.config.get("configurable", {}), }, } - self.checkpoint = saved.checkpoint + self.checkpoint = copy_checkpoint(saved.checkpoint) self.checkpoint_metadata = saved.metadata self.checkpoint_pending_writes = saved.pending_writes diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index 2e3c2098b..a0a8436e5 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -1,4 +1,3 @@ -from collections import defaultdict from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union import pytest @@ -100,14 +99,11 @@ def test_no_modifier(checkpointer: Optional[BaseCheckpointSaver]): "start:agent": 3, "agent": 3, }, - "versions_seen": defaultdict( - dict, - { - "__start__": {"__start__": 1}, - "agent": {"start:agent": 2}, - "tools": {}, - }, - ), + "versions_seen": { + "__input__": {}, + "__start__": {"__start__": 1}, + "agent": {"start:agent": 2}, + }, "pending_sends": [], "current_tasks": {}, } @@ -159,14 +155,11 @@ async def test_no_modifier_async(checkpointer: Optional[BaseCheckpointSaver]): "start:agent": 3, "agent": 3, }, - "versions_seen": defaultdict( - dict, - { - "__start__": {"__start__": 1}, - "agent": {"start:agent": 2}, - "tools": {}, - }, - ), + "versions_seen": { + "__input__": {}, + "__start__": {"__start__": 1}, + "agent": {"start:agent": 2}, + }, "pending_sends": [], "current_tasks": {}, } From b0e0d269c530c32b01f31ada366d6a27b41849af Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 16:40:50 -0700 Subject: [PATCH 24/28] Lint --- libs/langgraph/langgraph/pregel/loop.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/loop.py b/libs/langgraph/langgraph/pregel/loop.py index 62bf6c806..5d02080b0 100644 --- a/libs/langgraph/langgraph/pregel/loop.py +++ b/libs/langgraph/langgraph/pregel/loop.py @@ -196,7 +196,6 @@ class PregelLoop: for tid, k, v in self.checkpoint_pending_writes: if task := next((t for t in self.tasks if t.id == tid), None): task.writes.append((k, v)) - # TODO clear checkpoint_pending_writes # if all tasks have finished, re-tick if all(task.writes for task in self.tasks): From b77ef7d162994146299be5e5717dcc52d4bff929 Mon Sep 17 00:00:00 2001 From: Nuno Campos Date: Fri, 19 Jul 2024 17:13:51 -0700 Subject: [PATCH 25/28] Lint --- libs/langgraph/langgraph/pregel/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libs/langgraph/langgraph/pregel/__init__.py b/libs/langgraph/langgraph/pregel/__init__.py index 14edeacbd..7defcdc70 100644 --- a/libs/langgraph/langgraph/pregel/__init__.py +++ b/libs/langgraph/langgraph/pregel/__init__.py @@ -744,7 +744,6 @@ class Pregel( Output is a dict with the node name as key and the updated values as value. debug: Emit debug events for each step. output_keys: The keys to stream, defaults to all non-context channels. - input_keys: The keys to use from the input, defaults to all input channels. interrupt_before: Nodes to interrupt before, defaults to all nodes in the graph. interrupt_after: Nodes to interrupt after, defaults to all nodes in the graph. debug: Whether to print debug information during execution, defaults to False. From 29d7a812ae238bf008064de3ecefd51cb36bf83f Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Fri, 19 Jul 2024 17:52:47 -0700 Subject: [PATCH 26/28] [Docs] Update checkpointer docstrings (#1074) --- docs/docs/reference/checkpoints.md | 4 -- .../langgraph/checkpoint/aiosqlite.py | 49 ++++++++++++------- libs/langgraph/langgraph/checkpoint/base.py | 21 ++++++-- libs/langgraph/langgraph/checkpoint/memory.py | 33 +++++++++++-- libs/langgraph/langgraph/checkpoint/sqlite.py | 21 ++++++++ 5 files changed, 99 insertions(+), 29 deletions(-) diff --git a/docs/docs/reference/checkpoints.md b/docs/docs/reference/checkpoints.md index 34c7dbb58..eb8735dd6 100644 --- a/docs/docs/reference/checkpoints.md +++ b/docs/docs/reference/checkpoints.md @@ -18,12 +18,10 @@ You can [compile][langgraph.graph.MessageGraph.compile] any LangGraph workflow w ### BaseCheckpointSaver ::: langgraph.checkpoint.base.BaseCheckpointSaver -handler: python ### SerializerProtocol ::: langgraph.checkpoint.SerializerProtocol -handler: python ## Implementations @@ -32,12 +30,10 @@ LangGraph also natively provides the following checkpoint implementations. ### MemorySaver ::: langgraph.checkpoint.memory.MemorySaver -handler: python ### AsyncSqliteSaver ::: langgraph.checkpoint.aiosqlite.AsyncSqliteSaver -handler: python ### SqliteSaver diff --git a/libs/langgraph/langgraph/checkpoint/aiosqlite.py b/libs/langgraph/langgraph/checkpoint/aiosqlite.py index 431684d1a..76ab9bde3 100644 --- a/libs/langgraph/langgraph/checkpoint/aiosqlite.py +++ b/libs/langgraph/langgraph/checkpoint/aiosqlite.py @@ -46,22 +46,29 @@ def not_implemented_sync_method(func: T) -> T: class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): """An asynchronous checkpoint saver that stores checkpoints in a SQLite database. + This class provides an asynchronous interface for saving and retrieving checkpoints + using a SQLite database. It's designed for use in asynchronous environments and + offers better performance for I/O-bound operations compared to synchronous alternatives. + + Attributes: + conn (aiosqlite.Connection): The asynchronous SQLite database connection. + serde (SerializerProtocol): The serializer used for encoding/decoding checkpoints. + Tip: Requires the [aiosqlite](https://pypi.org/project/aiosqlite/) package. Install it with `pip install aiosqlite`. - Note: - While this class does support asynchronous checkpointing, it is not recommended - for production workloads, due to limitations in SQLite's write performance. For - production workloads, consider using a more robust database like PostgreSQL. + Warning: + While this class supports asynchronous checkpointing, it is not recommended + for production workloads due to limitations in SQLite's write performance. + For production use, consider a more robust database like PostgreSQL. - !!! Important + Tip: Remember to **close the database connection** after executing your code, otherwise, you may see the graph "hang" after execution (since the program will not exit until the connection is closed). - The easiest way to do this is to use the `async with` statement, as shown in the - examples below. + The easiest way is to use the `async with` statement as shown in the examples. ```python async with AsyncSqliteSaver.from_conn_string("checkpoints.sqlite") as saver: @@ -72,12 +79,9 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): print(event) ``` - Args: - conn (aiosqlite.Connection): The asynchronous SQLite database connection. - serde (Optional[SerializerProtocol]): The serializer to use for serializing and deserializing checkpoints. Defaults to JsonPlusSerializerCompat. - Examples: - Usage within a StateGraph: + Usage within StateGraph: + ```pycon >>> import asyncio >>> import aiosqlite @@ -95,8 +99,8 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): >>> asyncio.run(coro) Output: 2 ``` - Raw usage: + ```pycon >>> import asyncio >>> import aiosqlite @@ -309,12 +313,13 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): on the provided config. The checkpoints are ordered by timestamp in descending order. Args: - config (RunnableConfig): The config to use for listing the checkpoints. - before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None. - limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None. + config (Optional[RunnableConfig]): Base configuration for filtering checkpoints. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. + before (Optional[RunnableConfig]): List checkpoints created before this configuration. + limit (Optional[int]): Maximum number of checkpoints to return. Yields: - AsyncIterator[CheckpointTuple]: An asynchronous iterator of checkpoint tuples. + AsyncIterator[CheckpointTuple]: An asynchronous iterator of matching checkpoint tuples. """ await self.setup() where, param_values = search_where(config, filter, before) @@ -356,6 +361,7 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): Args: config (RunnableConfig): The config to associate with the checkpoint. checkpoint (Checkpoint): The checkpoint to save. + metadata (CheckpointMetadata): Additional metadata to save with the checkpoint. Returns: RunnableConfig: The updated config containing the saved checkpoint's timestamp. @@ -385,6 +391,15 @@ class AsyncSqliteSaver(BaseCheckpointSaver, AbstractAsyncContextManager): writes: Sequence[Tuple[str, Any]], task_id: str, ) -> None: + """Store intermediate writes linked to a checkpoint asynchronously. + + This method saves intermediate writes associated with a checkpoint to the database. + + Args: + config (RunnableConfig): Configuration of the related checkpoint. + writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair. + task_id (str): Identifier for the task creating the writes. + """ await self.setup() async with self.conn.executemany( "INSERT OR REPLACE INTO writes (thread_id, thread_ts, task_id, idx, channel, value) VALUES (?, ?, ?, ?, ?, ?)", diff --git a/libs/langgraph/langgraph/checkpoint/base.py b/libs/langgraph/langgraph/checkpoint/base.py index 7f1ffab1f..b6694db89 100644 --- a/libs/langgraph/langgraph/checkpoint/base.py +++ b/libs/langgraph/langgraph/checkpoint/base.py @@ -29,6 +29,8 @@ PendingWrite = Tuple[str, str, Any] # Marked as total=False to allow for future expansion. class CheckpointMetadata(TypedDict, total=False): + """Metadata associated with a checkpoint.""" + source: Literal["input", "loop", "update"] """The source of the checkpoint. - "input": The checkpoint was created from an input to invoke/stream/batch. @@ -119,6 +121,8 @@ def copy_checkpoint(checkpoint: Checkpoint) -> Checkpoint: class CheckpointTuple(NamedTuple): + """A tuple containing a checkpoint and its associated data.""" + config: RunnableConfig checkpoint: Checkpoint metadata: CheckpointMetadata @@ -269,11 +273,13 @@ class BaseCheckpointSaver(ABC): ) async def aget(self, config: RunnableConfig) -> Optional[Checkpoint]: - """ - Asynchronously fetch a checkpoint using the given configuration. + """Asynchronously fetch a checkpoint using the given configuration. Args: config (RunnableConfig): Configuration specifying which checkpoint to retrieve. + + Returns: + Optional[Checkpoint]: The requested checkpoint, or None if not found. """ if value := await self.aget_tuple(config): return value.checkpoint @@ -286,6 +292,9 @@ class BaseCheckpointSaver(ABC): Returns: Optional[CheckpointTuple]: The requested checkpoint tuple, or None if not found. + + Raises: + NotImplementedError: Implement this method in your custom checkpoint saver. """ raise NotImplementedError @@ -301,12 +310,15 @@ class BaseCheckpointSaver(ABC): Args: config (Optional[RunnableConfig]): Base configuration for filtering checkpoints. - filter (Optional[Dict[str, Any]]): Additional filtering criteria. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. before (Optional[RunnableConfig]): List checkpoints created before this configuration. limit (Optional[int]): Maximum number of checkpoints to return. Returns: AsyncIterator[CheckpointTuple]: Async iterator of matching checkpoint tuples. + + Raises: + NotImplementedError: Implement this method in your custom checkpoint saver. """ raise NotImplementedError yield @@ -326,6 +338,9 @@ class BaseCheckpointSaver(ABC): Returns: RunnableConfig: Updated configuration after storing the checkpoint. + + Raises: + NotImplementedError: Implement this method in your custom checkpoint saver. """ raise NotImplementedError diff --git a/libs/langgraph/langgraph/checkpoint/memory.py b/libs/langgraph/langgraph/checkpoint/memory.py index bd5dc6fd1..6d85fd188 100644 --- a/libs/langgraph/langgraph/checkpoint/memory.py +++ b/libs/langgraph/langgraph/checkpoint/memory.py @@ -107,15 +107,16 @@ class MemorySaver(BaseCheckpointSaver): """List checkpoints from the in-memory storage. This method retrieves a list of checkpoint tuples from the in-memory storage based - on the provided config. The checkpoints are ordered by timestamp in insertion order. + on the provided criteria. Args: - config (RunnableConfig): The config to use for listing the checkpoints. - before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None. - limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None. + config (Optional[RunnableConfig]): Base configuration for filtering checkpoints. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. + before (Optional[RunnableConfig]): List checkpoints created before this configuration. + limit (Optional[int]): Maximum number of checkpoints to return. Yields: - Iterator[CheckpointTuple]: An iterator of checkpoint tuples. + Iterator[CheckpointTuple]: An iterator of matching checkpoint tuples. """ thread_ids = (config["configurable"]["thread_id"],) if config else self.storage for thread_id in thread_ids: @@ -158,6 +159,7 @@ class MemorySaver(BaseCheckpointSaver): Args: config (RunnableConfig): The config to associate with the checkpoint. checkpoint (Checkpoint): The checkpoint to save. + metadata (CheckpointMetadata): Additional metadata to save with the checkpoint. Returns: RunnableConfig: The updated config containing the saved checkpoint's timestamp. @@ -191,6 +193,7 @@ class MemorySaver(BaseCheckpointSaver): Args: config (RunnableConfig): The config to associate with the writes. writes (list[tuple[str, Any]]): The writes to save. + task_id (str): Identifier for the task creating the writes. Returns: RunnableConfig: The updated config containing the saved writes' timestamp. @@ -254,6 +257,16 @@ class MemorySaver(BaseCheckpointSaver): checkpoint: Checkpoint, metadata: CheckpointMetadata, ) -> RunnableConfig: + """Asynchronous version of put. + + Args: + config (RunnableConfig): The config to associate with the checkpoint. + checkpoint (Checkpoint): The checkpoint to save. + metadata (CheckpointMetadata): Additional metadata to save with the checkpoint. + + Returns: + RunnableConfig: The updated config containing the saved checkpoint's timestamp. + """ return await asyncio.get_running_loop().run_in_executor( None, self.put, config, checkpoint, metadata ) @@ -264,6 +277,16 @@ class MemorySaver(BaseCheckpointSaver): writes: List[Tuple[str, Any]], task_id: str, ) -> RunnableConfig: + """Asynchronous version of put_writes. + + This method is an asynchronous wrapper around put_writes that runs the synchronous + method in a separate thread using asyncio. + + Args: + config (RunnableConfig): The config to associate with the writes. + writes (List[Tuple[str, Any]]): The writes to save, each as a (channel, value) pair. + task_id (str): Identifier for the task creating the writes. + """ return await asyncio.get_running_loop().run_in_executor( None, self.put_writes, config, writes, task_id ) diff --git a/libs/langgraph/langgraph/checkpoint/sqlite.py b/libs/langgraph/langgraph/checkpoint/sqlite.py index 6ac9ee593..eee6e05c7 100644 --- a/libs/langgraph/langgraph/checkpoint/sqlite.py +++ b/libs/langgraph/langgraph/checkpoint/sqlite.py @@ -309,6 +309,7 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): Args: config (RunnableConfig): The config to use for listing the checkpoints. + filter (Optional[Dict[str, Any]]): Additional filtering criteria for metadata. Defaults to None. before (Optional[RunnableConfig]): If provided, only checkpoints before the specified timestamp are returned. Defaults to None. limit (Optional[int]): The maximum number of checkpoints to return. Defaults to None. @@ -410,6 +411,15 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): writes: Sequence[Tuple[str, Any]], task_id: str, ) -> None: + """Store intermediate writes linked to a checkpoint. + + This method saves intermediate writes associated with a checkpoint to the SQLite database. + + Args: + config (RunnableConfig): Configuration of the related checkpoint. + writes (Sequence[Tuple[str, Any]]): List of writes to store, each as (channel, value) pair. + task_id (str): Identifier for the task creating the writes. + """ with self.lock, self.cursor() as cur: cur.executemany( "INSERT OR REPLACE INTO writes (thread_id, thread_ts, task_id, idx, channel, value) VALUES (?, ?, ?, ?, ?, ?)", @@ -467,6 +477,17 @@ class SqliteSaver(BaseCheckpointSaver, AbstractContextManager): raise NotImplementedError(_AIO_ERROR_MSG) def get_next_version(self, current: Optional[str], channel: BaseChannel) -> str: + """Generate the next version ID for a channel. + + This method creates a new version identifier for a channel based on its current version. + + Args: + current (Optional[str]): The current version identifier of the channel. + channel (BaseChannel): The channel being versioned. + + Returns: + str: The next version identifier, which is guaranteed to be monotonically increasing. + """ if current is None: current_v = 0 else: From 75f8a33c9e870450365b88a2cce6e4c38d8724ac Mon Sep 17 00:00:00 2001 From: William FH <13333726+hinthornw@users.noreply.github.com> Date: Fri, 19 Jul 2024 18:29:24 -0700 Subject: [PATCH 27/28] [Docs] Format notebooks (#1076) --- examples/chatbots/customer-support.ipynb | 286 ++---------------- examples/human_in_the_loop/breakpoints.ipynb | 10 +- .../human_in_the_loop/edit-graph-state.ipynb | 7 +- .../human_in_the_loop/wait-user-input.ipynb | 9 +- .../add-summary-conversation-history.ipynb | 29 +- examples/memory/delete-messages.ipynb | 15 +- examples/node-retries.ipynb | 12 +- examples/pass-config-to-tools.ipynb | 3 +- examples/persistence_mongodb.ipynb | 35 ++- examples/persistence_postgres.ipynb | 7 +- examples/persistence_redis.ipynb | 206 ++++++++++--- ...-from-within-tools-without-langchain.ipynb | 87 +++--- .../streaming-tokens-without-langchain.ipynb | 78 ++--- examples/subgraph.ipynb | 135 +++------ examples/tool-calling-errors.ipynb | 4 +- examples/tool-calling.ipynb | 29 +- 16 files changed, 425 insertions(+), 527 deletions(-) diff --git a/examples/chatbots/customer-support.ipynb b/examples/chatbots/customer-support.ipynb index 90d55e455..374d3e538 100644 --- a/examples/chatbots/customer-support.ipynb +++ b/examples/chatbots/customer-support.ipynb @@ -32,10 +32,7 @@ "scrolled": true }, "outputs": [], - "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph langchain-community langchain-openai scikit-learn" - ] + "source": ["%%capture --no-stderr\n%pip install -U langgraph langchain-community langchain-openai scikit-learn"] }, { "cell_type": "markdown", @@ -51,15 +48,7 @@ "id": "3d1ef253-6b0c-4481-868c-e1fe84f2c8ff", "metadata": {}, "outputs": [], - "source": [ - "import requests\n", - "\n", - "url = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\n", - "response = requests.get(url)\n", - "\n", - "with open(\"Chinook.db\", \"wb\") as file:\n", - " file.write(response.content)" - ] + "source": ["import requests\n\nurl = \"https://storage.googleapis.com/benchmarks-artifacts/chinook/Chinook.db\"\nresponse = requests.get(url)\n\nwith open(\"Chinook.db\", \"wb\") as file:\n file.write(response.content)"] }, { "cell_type": "code", @@ -88,12 +77,7 @@ "output_type": "execute_result" } ], - "source": [ - "from langchain_community.utilities import SQLDatabase\n", - "\n", - "db = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\n", - "db.get_usable_table_names()" - ] + "source": ["from langchain_community.utilities import SQLDatabase\n\ndb = SQLDatabase.from_uri(\"sqlite:///Chinook.db\")\ndb.get_usable_table_names()"] }, { "cell_type": "markdown", @@ -112,11 +96,7 @@ "id": "d9ea4e80-30e6-4d46-b480-35f0be2fb055", "metadata": {}, "outputs": [], - "source": [ - "from langchain_openai import ChatOpenAI\n", - "\n", - "model = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")" - ] + "source": ["from langchain_openai import ChatOpenAI\n\nmodel = ChatOpenAI(temperature=0, streaming=True, model=\"gpt-4o\")"] }, { "cell_type": "markdown", @@ -138,9 +118,7 @@ "id": "ea958e9f-ab1f-49b5-bd85-16332055297c", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import HumanMessage, SystemMessage" - ] + "source": ["from langchain_core.messages import HumanMessage, SystemMessage"] }, { "cell_type": "markdown", @@ -159,12 +137,7 @@ "id": "975b039a", "metadata": {}, "outputs": [], - "source": [ - "# This tool is given to the agent to look up information about a customer\n", - "def get_customer_info(customer_id: int):\n", - " \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n", - " return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")" - ] + "source": ["# This tool is given to the agent to look up information about a customer\ndef get_customer_info(customer_id: int):\n \"\"\"Look up customer info given their ID. ALWAYS make sure you have the customer ID before invoking this.\"\"\"\n return db.run(f\"SELECT * FROM Customer WHERE CustomerID = {customer_id};\")"] }, { "cell_type": "code", @@ -172,20 +145,7 @@ "id": "1d5fa446", "metadata": {}, "outputs": [], - "source": [ - "customer_prompt = \"\"\"Your job is to help a user update their profile.\n", - "\n", - "You only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n", - "\n", - "If you are unable to help the user, you can \"\"\"\n", - "\n", - "\n", - "def get_customer_messages(messages):\n", - " return [SystemMessage(content=customer_prompt)] + messages\n", - "\n", - "\n", - "customer_chain = get_customer_messages | model.bind_tools([get_customer_info])" - ] + "source": ["customer_prompt = \"\"\"Your job is to help a user update their profile.\n\nYou only have certain tools you can use. These tools require specific input. If you don't know the required input, then ask the user for it.\n\nIf you are unable to help the user, you can \"\"\"\n\n\ndef get_customer_messages(messages):\n return [SystemMessage(content=customer_prompt)] + messages\n\n\ncustomer_chain = get_customer_messages | model.bind_tools([get_customer_info])"] }, { "cell_type": "markdown", @@ -206,19 +166,7 @@ "id": "a8604a3b-b484-4b2b-a914-4236cb98c524", "metadata": {}, "outputs": [], - "source": [ - "from langchain_community.vectorstores import SKLearnVectorStore\n", - "from langchain_openai import OpenAIEmbeddings\n", - "\n", - "artists = db._execute(\"select * from Artist\")\n", - "songs = db._execute(\"select * from Track\")\n", - "artist_retriever = SKLearnVectorStore.from_texts(\n", - " [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n", - ").as_retriever()\n", - "song_retriever = SKLearnVectorStore.from_texts(\n", - " [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n", - ").as_retriever()" - ] + "source": ["from langchain_community.vectorstores import SKLearnVectorStore\nfrom langchain_openai import OpenAIEmbeddings\n\nartists = db._execute(\"select * from Artist\")\nsongs = db._execute(\"select * from Track\")\nartist_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in artists], OpenAIEmbeddings(), metadatas=artists\n).as_retriever()\nsong_retriever = SKLearnVectorStore.from_texts(\n [a[\"Name\"] for a in songs], OpenAIEmbeddings(), metadatas=songs\n).as_retriever()"] }, { "cell_type": "markdown", @@ -234,16 +182,7 @@ "id": "0a2a2b74", "metadata": {}, "outputs": [], - "source": [ - "def get_albums_by_artist(artist):\n", - " \"\"\"Get albums by an artist (or similar artists).\"\"\"\n", - " docs = artist_retriever.get_relevant_documents(artist)\n", - " artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n", - " return db.run(\n", - " f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n", - " include_columns=True,\n", - " )" - ] + "source": ["def get_albums_by_artist(artist):\n \"\"\"Get albums by an artist (or similar artists).\"\"\"\n docs = artist_retriever.get_relevant_documents(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Title, Name FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"] }, { "cell_type": "markdown", @@ -259,16 +198,7 @@ "id": "da533f50", "metadata": {}, "outputs": [], - "source": [ - "def get_tracks_by_artist(artist):\n", - " \"\"\"Get songs by an artist (or similar artists).\"\"\"\n", - " docs = artist_retriever.invoke(artist)\n", - " artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n", - " return db.run(\n", - " f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n", - " include_columns=True,\n", - " )" - ] + "source": ["def get_tracks_by_artist(artist):\n \"\"\"Get songs by an artist (or similar artists).\"\"\"\n docs = artist_retriever.invoke(artist)\n artist_ids = \", \".join([str(d.metadata[\"ArtistId\"]) for d in docs])\n return db.run(\n f\"SELECT Track.Name as SongName, Artist.Name as ArtistName FROM Album LEFT JOIN Artist ON Album.ArtistId = Artist.ArtistId LEFT JOIN Track ON Track.AlbumId = Album.AlbumId WHERE Album.ArtistId in ({artist_ids});\",\n include_columns=True,\n )"] }, { "cell_type": "markdown", @@ -284,11 +214,7 @@ "id": "b3c07010", "metadata": {}, "outputs": [], - "source": [ - "def check_for_songs(song_title):\n", - " \"\"\"Check if a song exists by its name.\"\"\"\n", - " return song_retriever.invoke(song_title)" - ] + "source": ["def check_for_songs(song_title):\n \"\"\"Check if a song exists by its name.\"\"\"\n return song_retriever.invoke(song_title)"] }, { "cell_type": "markdown", @@ -304,23 +230,7 @@ "id": "72a14d5c", "metadata": {}, "outputs": [], - "source": [ - "song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n", - "\n", - "You only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n", - "\n", - "When looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\n", - "on similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n", - "\n", - "\n", - "def get_song_messages(messages):\n", - " return [SystemMessage(content=song_system_message)] + messages\n", - "\n", - "\n", - "song_recc_chain = get_song_messages | model.bind_tools(\n", - " [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n", - ")" - ] + "source": ["song_system_message = \"\"\"Your job is to help a customer find any songs they are looking for. \n\nYou only have certain tools you can use. If a customer asks you to look something up that you don't know how, politely tell them what you can help with.\n\nWhen looking up artists and songs, sometimes the artist/song will not be found. In that case, the tools will return information \\\non similar songs and artists. This is intentional, it is not the tool messing up.\"\"\"\n\n\ndef get_song_messages(messages):\n return [SystemMessage(content=song_system_message)] + messages\n\n\nsong_recc_chain = get_song_messages | model.bind_tools(\n [get_albums_by_artist, get_tracks_by_artist, check_for_songs]\n)"] }, { "cell_type": "code", @@ -339,10 +249,7 @@ "output_type": "execute_result" } ], - "source": [ - "msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\n", - "song_recc_chain.invoke(msgs)" - ] + "source": ["msgs = [HumanMessage(content=\"hi! can you help me find songs by amy whinehouse?\")]\nsong_recc_chain.invoke(msgs)"] }, { "cell_type": "markdown", @@ -360,32 +267,7 @@ "id": "73e74268", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\n", - "from langchain_core.pydantic_v1 import BaseModel, Field\n", - "\n", - "\n", - "class Router(BaseModel):\n", - " \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n", - "\n", - " choice: str = Field(description=\"should be one of: music, customer\")\n", - "\n", - "\n", - "system_message = \"\"\"Your job is to help as a customer service representative for a music store.\n", - "\n", - "You should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n", - "\n", - "- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n", - "- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n", - "\n", - "If the user is asking or wants to ask about updating or accessing their information, send them to that route.\n", - "If the user is asking or wants to ask about music, send them to that route.\n", - "Otherwise, respond.\"\"\"\n", - "\n", - "\n", - "def get_messages(messages):\n", - " return [SystemMessage(content=system_message)] + messages" - ] + "source": ["from langchain_core.messages import AIMessage, HumanMessage, SystemMessage\nfrom langchain_core.pydantic_v1 import BaseModel, Field\n\n\nclass Router(BaseModel):\n \"\"\"Call this if you are able to route the user to the appropriate representative.\"\"\"\n\n choice: str = Field(description=\"should be one of: music, customer\")\n\n\nsystem_message = \"\"\"Your job is to help as a customer service representative for a music store.\n\nYou should interact politely with customers to try to figure out how you can help. You can help in a few ways:\n\n- Updating user information: if a customer wants to update the information in the user database. Call the router with `customer`\n- Recommending music: if a customer wants to find some music or information about music. Call the router with `music`\n\nIf the user is asking or wants to ask about updating or accessing their information, send them to that route.\nIf the user is asking or wants to ask about music, send them to that route.\nOtherwise, respond.\"\"\"\n\n\ndef get_messages(messages):\n return [SystemMessage(content=system_message)] + messages"] }, { "cell_type": "code", @@ -393,9 +275,7 @@ "id": "ddf27314", "metadata": {}, "outputs": [], - "source": [ - "chain = get_messages | model.bind_tools([Router])" - ] + "source": ["chain = get_messages | model.bind_tools([Router])"] }, { "cell_type": "code", @@ -414,10 +294,7 @@ "output_type": "execute_result" } ], - "source": [ - "msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\n", - "chain.invoke(msgs)" - ] + "source": ["msgs = [HumanMessage(content=\"hi! can you help me find a good song?\")]\nchain.invoke(msgs)"] }, { "cell_type": "code", @@ -436,10 +313,7 @@ "output_type": "execute_result" } ], - "source": [ - "msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\n", - "chain.invoke(msgs)" - ] + "source": ["msgs = [HumanMessage(content=\"hi! what's the email you have for me?\")]\nchain.invoke(msgs)"] }, { "cell_type": "code", @@ -447,15 +321,7 @@ "id": "bd6ddd8b-7500-46a7-811d-3bcb937bda51", "metadata": {}, "outputs": [], - "source": [ - "from langchain_core.messages import AIMessage\n", - "\n", - "\n", - "def add_name(message, name):\n", - " _dict = message.dict()\n", - " _dict[\"name\"] = name\n", - " return AIMessage(**_dict)" - ] + "source": ["from langchain_core.messages import AIMessage\n\n\ndef add_name(message, name):\n _dict = message.dict()\n _dict[\"name\"] = name\n return AIMessage(**_dict)"] }, { "cell_type": "code", @@ -463,45 +329,7 @@ "id": "27494de5-8345-4c23-bc0e-81e0dd5d47d8", "metadata": {}, "outputs": [], - "source": [ - "import json\n", - "\n", - "from langgraph.graph import END\n", - "\n", - "\n", - "def _get_last_ai_message(messages):\n", - " for m in messages[::-1]:\n", - " if isinstance(m, AIMessage):\n", - " return m\n", - " return None\n", - "\n", - "\n", - "def _is_tool_call(msg):\n", - " return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n", - "\n", - "\n", - "def _route(messages):\n", - " last_message = messages[-1]\n", - " if isinstance(last_message, AIMessage):\n", - " if not last_message.tool_calls:\n", - " return END\n", - " else:\n", - " if last_message.name == \"general\":\n", - " if len(last_message.tool_calls) > 1:\n", - " raise ValueError(\"Too many tools\")\n", - " return last_message.tool_calls[0][\"args\"][\"choice\"]\n", - " else:\n", - " return \"tools\"\n", - " last_m = _get_last_ai_message(messages)\n", - " if last_m is None:\n", - " return \"general\"\n", - " if last_m.name == \"music\":\n", - " return \"music\"\n", - " elif last_m.name == \"customer\":\n", - " return \"customer\"\n", - " else:\n", - " return \"general\"" - ] + "source": ["import json\n\nfrom langgraph.graph import END, START\n\n\ndef _get_last_ai_message(messages):\n for m in messages[::-1]:\n if isinstance(m, AIMessage):\n return m\n return None\n\n\ndef _is_tool_call(msg):\n return hasattr(msg, \"additional_kwargs\") and \"tool_calls\" in msg.additional_kwargs\n\n\ndef _route(messages):\n last_message = messages[-1]\n if isinstance(last_message, AIMessage):\n if not last_message.tool_calls:\n return END\n else:\n if last_message.name == \"general\":\n if len(last_message.tool_calls) > 1:\n raise ValueError(\"Too many tools\")\n return last_message.tool_calls[0][\"args\"][\"choice\"]\n else:\n return \"tools\"\n last_m = _get_last_ai_message(messages)\n if last_m is None:\n return \"general\"\n if last_m.name == \"music\":\n return \"music\"\n elif last_m.name == \"customer\":\n return \"customer\"\n else:\n return \"general\""] }, { "cell_type": "code", @@ -509,12 +337,7 @@ "id": "8aec704a-46fe-4fb3-bdee-11c3bbffc370", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.prebuilt import ToolNode\n", - "\n", - "tools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\n", - "tool_node = ToolNode(tools)" - ] + "source": ["from langgraph.prebuilt import ToolNode\n\ntools = [get_albums_by_artist, get_tracks_by_artist, check_for_songs, get_customer_info]\ntool_node = ToolNode(tools)"] }, { "cell_type": "code", @@ -522,16 +345,7 @@ "id": "4d5b75c6-73e0-4922-a765-a15be63f869e", "metadata": {}, "outputs": [], - "source": [ - "def _filter_out_routes(messages):\n", - " ms = []\n", - " for m in messages:\n", - " if _is_tool_call(m):\n", - " if m.name == \"general\":\n", - " continue\n", - " ms.append(m)\n", - " return ms" - ] + "source": ["def _filter_out_routes(messages):\n ms = []\n for m in messages:\n if _is_tool_call(m):\n if m.name == \"general\":\n continue\n ms.append(m)\n return ms"] }, { "cell_type": "code", @@ -539,13 +353,7 @@ "id": "fd4dbf98-dbb3-411a-bad6-2bb334072aaf", "metadata": {}, "outputs": [], - "source": [ - "from functools import partial\n", - "\n", - "general_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\n", - "music_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\n", - "customer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")" - ] + "source": ["from functools import partial\n\ngeneral_node = _filter_out_routes | chain | partial(add_name, name=\"general\")\nmusic_node = _filter_out_routes | song_recc_chain | partial(add_name, name=\"music\")\ncustomer_node = _filter_out_routes | customer_chain | partial(add_name, name=\"customer\")"] }, { "cell_type": "code", @@ -553,33 +361,7 @@ "id": "dcade924", "metadata": {}, "outputs": [], - "source": [ - "from langgraph.checkpoint.sqlite import SqliteSaver\n", - "\n", - "from langgraph.graph import MessageGraph\n", - "\n", - "memory = SqliteSaver.from_conn_string(\":memory:\")\n", - "graph = MessageGraph()\n", - "nodes = {\n", - " \"general\": \"general\",\n", - " \"music\": \"music\",\n", - " END: END,\n", - " \"tools\": \"tools\",\n", - " \"customer\": \"customer\",\n", - "}\n", - "# Define a new graph\n", - "workflow = MessageGraph()\n", - "workflow.add_node(\"general\", general_node)\n", - "workflow.add_node(\"music\", music_node)\n", - "workflow.add_node(\"customer\", customer_node)\n", - "workflow.add_node(\"tools\", tool_node)\n", - "workflow.add_conditional_edges(\"general\", _route, nodes)\n", - "workflow.add_conditional_edges(\"tools\", _route, nodes)\n", - "workflow.add_conditional_edges(\"music\", _route, nodes)\n", - "workflow.add_conditional_edges(\"customer\", _route, nodes)\n", - "workflow.set_conditional_entry_point(_route, nodes)\n", - "graph = workflow.compile()" - ] + "source": ["from langgraph.checkpoint.sqlite import SqliteSaver\n\nfrom langgraph.graph import MessageGraph\n\nmemory = SqliteSaver.from_conn_string(\":memory:\")\ngraph = MessageGraph()\nnodes = {\n \"general\": \"general\",\n \"music\": \"music\",\n END: END,\n \"tools\": \"tools\",\n \"customer\": \"customer\",\n}\n# Define a new graph\nworkflow = MessageGraph()\nworkflow.add_node(\"general\", general_node)\nworkflow.add_node(\"music\", music_node)\nworkflow.add_node(\"customer\", customer_node)\nworkflow.add_node(\"tools\", tool_node)\nworkflow.add_conditional_edges(\"general\", _route, nodes)\nworkflow.add_conditional_edges(\"tools\", _route, nodes)\nworkflow.add_conditional_edges(\"music\", _route, nodes)\nworkflow.add_conditional_edges(\"customer\", _route, nodes)\nworkflow.add_conditional_edges(START, _route, nodes)\ngraph = workflow.compile()"] }, { "cell_type": "code", @@ -715,27 +497,7 @@ ] } ], - "source": [ - "import uuid\n", - "\n", - "from langchain_core.messages import HumanMessage\n", - "\n", - "from langgraph.graph.graph import START\n", - "\n", - "history = []\n", - "while True:\n", - " user = input(\"User (q/Q to quit): \")\n", - " if user in {\"q\", \"Q\"}:\n", - " print(\"AI: Byebye\")\n", - " break\n", - " history.append(HumanMessage(content=user))\n", - " async for output in graph.astream(history):\n", - " for key, value in output.items():\n", - " print(f\"Output from node '{key}':\")\n", - " print(\"---\")\n", - " print(value)\n", - " print(\"\\n---\\n\")" - ] + "source": ["import uuid\n\nfrom langchain_core.messages import HumanMessage\n\nfrom langgraph.graph.graph import START\n\nhistory = []\nwhile True:\n user = input(\"User (q/Q to quit): \")\n if user in {\"q\", \"Q\"}:\n print(\"AI: Byebye\")\n break\n history.append(HumanMessage(content=user))\n async for output in graph.astream(history):\n for key, value in output.items():\n print(f\"Output from node '{key}':\")\n print(\"---\")\n print(value)\n print(\"\\n---\\n\")"] } ], "metadata": { diff --git a/examples/human_in_the_loop/breakpoints.ipynb b/examples/human_in_the_loop/breakpoints.ipynb index f102cadb3..32d3732e2 100644 --- a/examples/human_in_the_loop/breakpoints.ipynb +++ b/examples/human_in_the_loop/breakpoints.ipynb @@ -133,21 +133,26 @@ "from langgraph.checkpoint.memory import MemorySaver\n", "from IPython.display import Image, display\n", "\n", + "\n", "class State(TypedDict):\n", " input: str\n", "\n", + "\n", "def step_1(state):\n", " print(\"---Step 1---\")\n", " pass\n", "\n", + "\n", "def step_2(state):\n", " print(\"---Step 2---\")\n", " pass\n", "\n", + "\n", "def step_3(state):\n", " print(\"---Step 3---\")\n", " pass\n", "\n", + "\n", "builder = StateGraph(State)\n", "builder.add_node(\"step_1\", step_1)\n", "builder.add_node(\"step_2\", step_2)\n", @@ -160,7 +165,7 @@ "# Set up memory\n", "memory = MemorySaver()\n", "\n", - "# Add \n", + "# Add\n", "graph = builder.compile(checkpointer=memory, interrupt_before=[\"step_3\"])\n", "\n", "# View\n", @@ -222,8 +227,7 @@ "\n", "user_approval = input(\"Do you want to go to Step 3? (yes/no): \")\n", "\n", - "if user_approval.lower() == 'yes':\n", - " \n", + "if user_approval.lower() == \"yes\":\n", " # If approved, continue the graph execution\n", " for event in graph.stream(None, thread, stream_mode=\"values\"):\n", " print(event)\n", diff --git a/examples/human_in_the_loop/edit-graph-state.ipynb b/examples/human_in_the_loop/edit-graph-state.ipynb index affc6ea88..a5d4ef6db 100644 --- a/examples/human_in_the_loop/edit-graph-state.ipynb +++ b/examples/human_in_the_loop/edit-graph-state.ipynb @@ -135,21 +135,26 @@ "from langgraph.checkpoint.memory import MemorySaver\n", "from IPython.display import Image, display\n", "\n", + "\n", "class State(TypedDict):\n", " input: str\n", "\n", + "\n", "def step_1(state):\n", " print(\"---Step 1---\")\n", " pass\n", "\n", + "\n", "def step_2(state):\n", " print(\"---Step 2---\")\n", " pass\n", "\n", + "\n", "def step_3(state):\n", " print(\"---Step 3---\")\n", " pass\n", "\n", + "\n", "builder = StateGraph(State)\n", "builder.add_node(\"step_1\", step_1)\n", "builder.add_node(\"step_2\", step_2)\n", @@ -162,7 +167,7 @@ "# Set up memory\n", "memory = MemorySaver()\n", "\n", - "# Add \n", + "# Add\n", "graph = builder.compile(checkpointer=memory, interrupt_before=[\"step_2\"])\n", "\n", "# View\n", diff --git a/examples/human_in_the_loop/wait-user-input.ipynb b/examples/human_in_the_loop/wait-user-input.ipynb index a08b0c114..6f41e65e4 100644 --- a/examples/human_in_the_loop/wait-user-input.ipynb +++ b/examples/human_in_the_loop/wait-user-input.ipynb @@ -137,22 +137,27 @@ "from langgraph.checkpoint.memory import MemorySaver\n", "from IPython.display import Image, display\n", "\n", + "\n", "class State(TypedDict):\n", " input: str\n", " user_feedback: str\n", "\n", + "\n", "def step_1(state):\n", " print(\"---Step 1---\")\n", " pass\n", "\n", + "\n", "def human_feedback(state):\n", " print(\"---human_feedback---\")\n", " pass\n", "\n", + "\n", "def step_3(state):\n", " print(\"---Step 3---\")\n", " pass\n", "\n", + "\n", "builder = StateGraph(State)\n", "builder.add_node(\"step_1\", step_1)\n", "builder.add_node(\"human_feedback\", human_feedback)\n", @@ -165,7 +170,7 @@ "# Set up memory\n", "memory = MemorySaver()\n", "\n", - "# Add \n", + "# Add\n", "graph = builder.compile(checkpointer=memory, interrupt_before=[\"human_feedback\"])\n", "\n", "# View\n", @@ -253,7 +258,7 @@ "\n", "# We now update the state as if we are the human_feedback node\n", "graph.update_state(thread, {\"user_feedback\": user_input}, as_node=\"human_feedback\")\n", - " \n", + "\n", "# We can check the state\n", "print(\"--State after update--\")\n", "print(graph.get_state(thread))\n", diff --git a/examples/memory/add-summary-conversation-history.ipynb b/examples/memory/add-summary-conversation-history.ipynb index c905b6ba7..5b5a19156 100644 --- a/examples/memory/add-summary-conversation-history.ipynb +++ b/examples/memory/add-summary-conversation-history.ipynb @@ -110,23 +110,26 @@ "\n", "memory = SqliteSaver.from_conn_string(\":memory:\")\n", "\n", + "\n", "# We will add a `summary` attribute (in addition to `messages` key,\n", "# which MessagesState already has)\n", "class State(MessagesState):\n", " summary: str\n", "\n", + "\n", "# We will use this model for both the conversation and the summarization\n", "model = ChatAnthropic(model_name=\"claude-3-haiku-20240307\")\n", "\n", + "\n", "# Define the logic to call the model\n", "def call_model(state: State):\n", " # If a summary exists, we add this in as a system message\n", - " summary = state.get('summary', '')\n", + " summary = state.get(\"summary\", \"\")\n", " if summary:\n", " system_message = f\"Summary of conversation earlier: {summary}\"\n", - " messages = [SystemMessage(content=system_message)] + state['messages']\n", + " messages = [SystemMessage(content=system_message)] + state[\"messages\"]\n", " else:\n", - " messages = state['messages']\n", + " messages = state[\"messages\"]\n", " response = model.invoke(messages)\n", " # We return a list, because this will get added to the existing list\n", " return {\"messages\": [response]}\n", @@ -145,7 +148,7 @@ "\n", "def summarize_conversation(state: State):\n", " # First, we summarize the conversation\n", - " summary = state.get('summary', '')\n", + " summary = state.get(\"summary\", \"\")\n", " if summary:\n", " # If a summary already exists, we use a different system prompt\n", " # to summarize it than if one didn't\n", @@ -155,17 +158,13 @@ " )\n", " else:\n", " summary_message = \"Create a summary of the conversation above:\"\n", - " \n", - " messages = state['messages'] + [HumanMessage(content=summary_message)]\n", + "\n", + " messages = state[\"messages\"] + [HumanMessage(content=summary_message)]\n", " response = model.invoke(messages)\n", " # We now need to delete messages that we no longer want to show up\n", " # I will delete all but the last two messages, but you can change this\n", - " delete_messages = [RemoveMessage(id=m.id) for m in state['messages'][:-2]]\n", - " return {\n", - " \"summary\": response.content,\n", - " \"messages\": delete_messages\n", - " }\n", - " \n", + " delete_messages = [RemoveMessage(id=m.id) for m in state[\"messages\"][:-2]]\n", + " return {\"summary\": response.content, \"messages\": delete_messages}\n", "\n", "\n", "# Define a new graph\n", @@ -212,10 +211,10 @@ "source": [ "def print_update(update):\n", " for k, v in update.items():\n", - " for m in v['messages']:\n", + " for m in v[\"messages\"]:\n", " m.pretty_print()\n", - " if 'summary' in v:\n", - " print(v['summary'])" + " if \"summary\" in v:\n", + " print(v[\"summary\"])" ] }, { diff --git a/examples/memory/delete-messages.ipynb b/examples/memory/delete-messages.ipynb index 2add95c03..feda6f023 100644 --- a/examples/memory/delete-messages.ipynb +++ b/examples/memory/delete-messages.ipynb @@ -252,7 +252,7 @@ } ], "source": [ - "messages = app.get_state(config).values['messages']\n", + "messages = app.get_state(config).values[\"messages\"]\n", "messages" ] }, @@ -292,6 +292,7 @@ ], "source": [ "from langchain_core.messages import RemoveMessage\n", + "\n", "app.update_state(config, {\"messages\": RemoveMessage(id=messages[0].id)})" ] }, @@ -323,7 +324,7 @@ } ], "source": [ - "messages = app.get_state(config).values['messages']\n", + "messages = app.get_state(config).values[\"messages\"]\n", "messages" ] }, @@ -349,10 +350,11 @@ "\n", "\n", "def delete_messages(state):\n", - " messages = state['messages']\n", + " messages = state[\"messages\"]\n", " if len(messages) > 3:\n", " return {\"messages\": [RemoveMessage(id=m.id) for m in messages[:-3]]}\n", "\n", + "\n", "# We need to modify the logic to call delete_messages rather than end right away\n", "def should_continue(state: MessagesState) -> Literal[\"action\", \"delete_messages\"]:\n", " \"\"\"Return the next node to execute.\"\"\"\n", @@ -374,7 +376,10 @@ "\n", "\n", "workflow.add_edge(START, \"agent\")\n", - "workflow.add_conditional_edges(\"agent\", should_continue,)\n", + "workflow.add_conditional_edges(\n", + " \"agent\",\n", + " should_continue,\n", + ")\n", "workflow.add_edge(\"action\", \"agent\")\n", "\n", "# This is the new edge we're adding: after we delete messages, we finish\n", @@ -450,7 +455,7 @@ } ], "source": [ - "messages = app.get_state(config).values['messages']\n", + "messages = app.get_state(config).values[\"messages\"]\n", "messages" ] }, diff --git a/examples/node-retries.ipynb b/examples/node-retries.ipynb index c50230ded..1af599840 100644 --- a/examples/node-retries.ipynb +++ b/examples/node-retries.ipynb @@ -65,23 +65,31 @@ "\n", "model = ChatAnthropic(model_name=\"claude-2.1\")\n", "\n", + "\n", "class AgentState(TypedDict):\n", " messages: Annotated[Sequence[BaseMessage], operator.add]\n", "\n", + "\n", "def query_database(state):\n", " query_result = db.run(\"SELECT * FROM Artist LIMIT 10;\")\n", " return {\"messages\": [AIMessage(content=query_result)]}\n", "\n", + "\n", "def call_model(state):\n", " response = model.invoke(state[\"messages\"])\n", " return {\"messages\": [response]}\n", "\n", + "\n", "# Define a new graph\n", "workflow = StateGraph(AgentState)\n", - "workflow.add_node(\"query_database\",query_database, retry=RetryPolicy(retry_on=sqlite3.OperationalError))\n", + "workflow.add_node(\n", + " \"query_database\",\n", + " query_database,\n", + " retry=RetryPolicy(retry_on=sqlite3.OperationalError),\n", + ")\n", "workflow.add_node(\"model\", call_model, retry=RetryPolicy(max_attempts=5))\n", "workflow.add_edge(START, \"model\")\n", - "workflow.add_edge(\"model\",\"query_database\")\n", + "workflow.add_edge(\"model\", \"query_database\")\n", "workflow.add_edge(\"query_database\", END)\n", "\n", "app = workflow.compile()" diff --git a/examples/pass-config-to-tools.ipynb b/examples/pass-config-to-tools.ipynb index c36344fcb..8e297e32f 100644 --- a/examples/pass-config-to-tools.ipynb +++ b/examples/pass-config-to-tools.ipynb @@ -91,7 +91,8 @@ "@tool(parse_docstring=True)\n", "def update_favorite_pets(\n", " # NOTE: config arg does not need to be added to docstring, as we don't want it to be included in the function signature attached to the LLM\n", - " pets: List[str], config: RunnableConfig\n", + " pets: List[str],\n", + " config: RunnableConfig,\n", ") -> None:\n", " \"\"\"Add the list of favorite pets.\n", "\n", diff --git a/examples/persistence_mongodb.ipynb b/examples/persistence_mongodb.ipynb index bec75dea0..ef96b17f5 100644 --- a/examples/persistence_mongodb.ipynb +++ b/examples/persistence_mongodb.ipynb @@ -82,6 +82,7 @@ " return pickle.loads(data)\n", " return super().loads(data)\n", "\n", + "\n", "class MongoDBSaver(AbstractContextManager, BaseCheckpointSaver):\n", " \"\"\"A checkpoint saver that stores checkpoints in a MongoDB database.\n", "\n", @@ -309,17 +310,19 @@ } ], "source": [ - "from langgraph.graph import StateGraph\n", + "from langgraph.graph import StateGraph, START, END\n", "\n", - "checkpointer = MongoDBSaver(MongoClient(MONGO_URI), \"checkpoints_db\", \"checkpoints_collection\")\n", + "checkpointer = MongoDBSaver(\n", + " MongoClient(MONGO_URI), \"checkpoints_db\", \"checkpoints_collection\"\n", + ")\n", "builder = StateGraph(int)\n", "builder.add_node(\"add_one\", lambda x: x + 1)\n", - "builder.set_entry_point(\"add_one\")\n", - "builder.set_finish_point(\"add_one\")\n", + "builder.add_edge(START, \"add_one\")\n", + "builder.add_edge(\"add_one\", END)\n", "graph = builder.compile(checkpointer=checkpointer)\n", "config = {\"configurable\": {\"thread_id\": \"123\"}}\n", "graph.get_state(config)\n", - "result = graph.invoke(3,config)\n", + "result = graph.invoke(3, config)\n", "graph.get_state(config)" ] }, @@ -572,7 +575,7 @@ "for doc in collection.find():\n", " print(doc)\n", "\n", - "#The checkpoints from both the examples have been saved in the database." + "# The checkpoints from both the examples have been saved in the database." ] }, { @@ -588,7 +591,7 @@ "metadata": {}, "outputs": [], "source": [ - "#Async package for MongoDB\n", + "# Async package for MongoDB\n", "%pip install motor" ] }, @@ -601,7 +604,7 @@ "import pickle\n", "from contextlib import AbstractContextManager\n", "from types import TracebackType\n", - "from typing import Any, Dict,Optional,AsyncIterator\n", + "from typing import Any, Dict, Optional, AsyncIterator\n", "\n", "from langchain_core.runnables import RunnableConfig\n", "from typing_extensions import Self\n", @@ -616,6 +619,7 @@ "from langgraph.serde.jsonplus import JsonPlusSerializer\n", "from motor.motor_asyncio import AsyncIOMotorClient\n", "\n", + "\n", "class JsonPlusSerializerCompat(JsonPlusSerializer):\n", " \"\"\"A serializer that supports loading pickled checkpoints for backwards compatibility.\n", "\n", @@ -643,6 +647,7 @@ " return pickle.loads(data)\n", " return super().loads(data)\n", "\n", + "\n", "class MongoDBSaver(AbstractContextManager, BaseCheckpointSaver):\n", " \"\"\"A checkpoint saver that stores checkpoints in a MongoDB database.\n", "\n", @@ -842,15 +847,18 @@ "metadata": {}, "outputs": [], "source": [ - "from langgraph.graph import StateGraph\n", - "checkpointer = MongoDBSaver(AsyncIOMotorClient(MONGO_URI), \"checkpoints_db\", \"checkpoints_collection\")\n", + "from langgraph.graph import StateGraph, START\n", + "\n", + "checkpointer = MongoDBSaver(\n", + " AsyncIOMotorClient(MONGO_URI), \"checkpoints_db\", \"checkpoints_collection\"\n", + ")\n", "builder = StateGraph(int)\n", "builder.add_node(\"add_one\", lambda x: x + 1)\n", - "builder.set_entry_point(\"add_one\")\n", - "builder.set_finish_point(\"add_one\")\n", + "builder.add_edge(START, \"add_one\")\n", + "builder.add_edge(\"add_one\", END)\n", "graph = builder.compile(checkpointer=checkpointer)\n", "config = {\"configurable\": {\"thread_id\": \"123\"}}\n", - "res = await graph.ainvoke(3,config)" + "res = await graph.ainvoke(3, config)" ] }, { @@ -971,6 +979,7 @@ ], "source": [ "from pymongo import MongoClient\n", + "\n", "client = MongoClient(MONGO_URI)\n", "database = client[\"checkpoints_db\"]\n", "collection = database[\"checkpoints_collection\"]\n", diff --git a/examples/persistence_postgres.ipynb b/examples/persistence_postgres.ipynb index 28145c104..5874aa819 100644 --- a/examples/persistence_postgres.ipynb +++ b/examples/persistence_postgres.ipynb @@ -51,7 +51,7 @@ " Union,\n", " Tuple,\n", " List,\n", - " Sequence\n", + " Sequence,\n", ")\n", "\n", "import psycopg\n", @@ -341,7 +341,6 @@ " writes: Sequence[Tuple[str, Any]],\n", " task_id: str,\n", " ) -> None:\n", - "\n", " async with self._get_async_connection() as conn:\n", " async with conn.cursor() as cur:\n", " await cur.executemany(\n", @@ -524,7 +523,7 @@ " pending_writes=[\n", " (task_id, channel, self.serde.loads(value))\n", " for task_id, channel, value in cur\n", - " ]\n", + " ],\n", " )\n", "\n", " async def aget_tuple(self, config: RunnableConfig) -> Optional[CheckpointTuple]:\n", @@ -594,7 +593,7 @@ " pending_writes=[\n", " (task_id, channel, self.serde.loads(value))\n", " async for task_id, channel, value in cur\n", - " ]\n", + " ],\n", " )\n", "\n", " def _search_where(\n", diff --git a/examples/persistence_redis.ipynb b/examples/persistence_redis.ipynb index 51a61880d..e42963f21 100644 --- a/examples/persistence_redis.ipynb +++ b/examples/persistence_redis.ipynb @@ -63,10 +63,13 @@ "logging.basicConfig(level=logging.INFO)\n", "logger = logging.getLogger(__name__)\n", "\n", + "\n", "class JsonAndBinarySerializer(JsonPlusSerializer):\n", " def _default(self, obj: Any) -> Any:\n", " if isinstance(obj, (bytes, bytearray)):\n", - " return self._encode_constructor_args(obj.__class__, method=\"fromhex\", args=[obj.hex()])\n", + " return self._encode_constructor_args(\n", + " obj.__class__, method=\"fromhex\", args=[obj.hex()]\n", + " )\n", " return super()._default(obj)\n", "\n", " def dumps(self, obj: Any) -> str:\n", @@ -87,17 +90,25 @@ " logger.error(f\"Deserialization error: {e}\")\n", " raise\n", "\n", - "def initialize_sync_pool(host: str = 'localhost', port: int = 6379, db: int = 0, **kwargs) -> redis.ConnectionPool:\n", + "\n", + "def initialize_sync_pool(\n", + " host: str = \"localhost\", port: int = 6379, db: int = 0, **kwargs\n", + ") -> redis.ConnectionPool:\n", " \"\"\"Initialize a synchronous Redis connection pool.\"\"\"\n", " try:\n", " pool = redis.ConnectionPool(host=host, port=port, db=db, **kwargs)\n", - " logger.info(f\"Synchronous Redis pool initialized with host={host}, port={port}, db={db}\")\n", + " logger.info(\n", + " f\"Synchronous Redis pool initialized with host={host}, port={port}, db={db}\"\n", + " )\n", " return pool\n", " except Exception as e:\n", " logger.error(f\"Error initializing sync pool: {e}\")\n", " raise\n", "\n", - "def initialize_async_pool(url: str = \"redis://localhost\", **kwargs) -> AsyncConnectionPool:\n", + "\n", + "def initialize_async_pool(\n", + " url: str = \"redis://localhost\", **kwargs\n", + ") -> AsyncConnectionPool:\n", " \"\"\"Initialize an asynchronous Redis connection pool.\"\"\"\n", " try:\n", " pool = AsyncConnectionPool.from_url(url, **kwargs)\n", @@ -107,8 +118,11 @@ " logger.error(f\"Error initializing async pool: {e}\")\n", " raise\n", "\n", + "\n", "@contextmanager\n", - "def _get_sync_connection(connection: Union[redis.Redis, redis.ConnectionPool, None]) -> Generator[redis.Redis, None, None]:\n", + "def _get_sync_connection(\n", + " connection: Union[redis.Redis, redis.ConnectionPool, None]\n", + ") -> Generator[redis.Redis, None, None]:\n", " conn = None\n", " try:\n", " if isinstance(connection, redis.Redis):\n", @@ -125,8 +139,11 @@ " if conn:\n", " conn.close()\n", "\n", + "\n", "@asynccontextmanager\n", - "async def _get_async_connection(connection: Union[AsyncRedis, AsyncConnectionPool, None]) -> AsyncGenerator[AsyncRedis, None]:\n", + "async def _get_async_connection(\n", + " connection: Union[AsyncRedis, AsyncConnectionPool, None]\n", + ") -> AsyncGenerator[AsyncRedis, None]:\n", " conn = None\n", " try:\n", " if isinstance(connection, AsyncRedis):\n", @@ -143,27 +160,42 @@ " if conn:\n", " await conn.aclose()\n", "\n", + "\n", "class RedisSaver(BaseCheckpointSaver):\n", " sync_connection: Optional[Union[redis.Redis, redis.ConnectionPool]] = None\n", " async_connection: Optional[Union[AsyncRedis, AsyncConnectionPool]] = None\n", "\n", - " def __init__(self, sync_connection: Optional[Union[redis.Redis, redis.ConnectionPool]] = None, async_connection: Optional[Union[AsyncRedis, AsyncConnectionPool]] = None):\n", + " def __init__(\n", + " self,\n", + " sync_connection: Optional[Union[redis.Redis, redis.ConnectionPool]] = None,\n", + " async_connection: Optional[Union[AsyncRedis, AsyncConnectionPool]] = None,\n", + " ):\n", " super().__init__(serde=JsonAndBinarySerializer())\n", " self.sync_connection = sync_connection\n", " self.async_connection = async_connection\n", "\n", - " def put(self, config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata) -> RunnableConfig:\n", + " def put(\n", + " self,\n", + " config: RunnableConfig,\n", + " checkpoint: Checkpoint,\n", + " metadata: CheckpointMetadata,\n", + " ) -> RunnableConfig:\n", " thread_id = config[\"configurable\"][\"thread_id\"]\n", " parent_ts = config[\"configurable\"].get(\"thread_ts\")\n", " key = f\"checkpoint:{thread_id}:{checkpoint['ts']}\"\n", " try:\n", " with _get_sync_connection(self.sync_connection) as conn:\n", - " conn.hset(key, mapping={\n", - " \"checkpoint\": self.serde.dumps(checkpoint),\n", - " \"metadata\": self.serde.dumps(metadata),\n", - " \"parent_ts\": parent_ts if parent_ts else \"\"\n", - " })\n", - " logger.info(f\"Checkpoint stored successfully for thread_id: {thread_id}, ts: {checkpoint['ts']}\")\n", + " conn.hset(\n", + " key,\n", + " mapping={\n", + " \"checkpoint\": self.serde.dumps(checkpoint),\n", + " \"metadata\": self.serde.dumps(metadata),\n", + " \"parent_ts\": parent_ts if parent_ts else \"\",\n", + " },\n", + " )\n", + " logger.info(\n", + " f\"Checkpoint stored successfully for thread_id: {thread_id}, ts: {checkpoint['ts']}\"\n", + " )\n", " except Exception as e:\n", " logger.error(f\"Failed to put checkpoint: {e}\")\n", " raise\n", @@ -174,18 +206,28 @@ " },\n", " }\n", "\n", - " async def aput(self, config: RunnableConfig, checkpoint: Checkpoint, metadata: CheckpointMetadata) -> RunnableConfig:\n", + " async def aput(\n", + " self,\n", + " config: RunnableConfig,\n", + " checkpoint: Checkpoint,\n", + " metadata: CheckpointMetadata,\n", + " ) -> RunnableConfig:\n", " thread_id = config[\"configurable\"][\"thread_id\"]\n", " parent_ts = config[\"configurable\"].get(\"thread_ts\")\n", " key = f\"checkpoint:{thread_id}:{checkpoint['ts']}\"\n", " try:\n", " async with _get_async_connection(self.async_connection) as conn:\n", - " await conn.hset(key, mapping={\n", - " \"checkpoint\": self.serde.dumps(checkpoint),\n", - " \"metadata\": self.serde.dumps(metadata),\n", - " \"parent_ts\": parent_ts if parent_ts else \"\"\n", - " })\n", - " logger.info(f\"Checkpoint stored successfully for thread_id: {thread_id}, ts: {checkpoint['ts']}\")\n", + " await conn.hset(\n", + " key,\n", + " mapping={\n", + " \"checkpoint\": self.serde.dumps(checkpoint),\n", + " \"metadata\": self.serde.dumps(metadata),\n", + " \"parent_ts\": parent_ts if parent_ts else \"\",\n", + " },\n", + " )\n", + " logger.info(\n", + " f\"Checkpoint stored successfully for thread_id: {thread_id}, ts: {checkpoint['ts']}\"\n", + " )\n", " except Exception as e:\n", " logger.error(f\"Failed to aput checkpoint: {e}\")\n", " raise\n", @@ -217,9 +259,20 @@ " checkpoint = self.serde.loads(checkpoint_data[b\"checkpoint\"].decode())\n", " metadata = self.serde.loads(checkpoint_data[b\"metadata\"].decode())\n", " parent_ts = checkpoint_data.get(b\"parent_ts\", b\"\").decode()\n", - " parent_config = {\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": parent_ts}} if parent_ts else None\n", - " logger.info(f\"Checkpoint retrieved successfully for thread_id: {thread_id}, ts: {thread_ts}\")\n", - " return CheckpointTuple(config=config, checkpoint=checkpoint, metadata=metadata, parent_config=parent_config)\n", + " parent_config = (\n", + " {\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": parent_ts}}\n", + " if parent_ts\n", + " else None\n", + " )\n", + " logger.info(\n", + " f\"Checkpoint retrieved successfully for thread_id: {thread_id}, ts: {thread_ts}\"\n", + " )\n", + " return CheckpointTuple(\n", + " config=config,\n", + " checkpoint=checkpoint,\n", + " metadata=metadata,\n", + " parent_config=parent_config,\n", + " )\n", " except Exception as e:\n", " logger.error(f\"Failed to get checkpoint tuple: {e}\")\n", " raise\n", @@ -245,22 +298,47 @@ " checkpoint = self.serde.loads(checkpoint_data[b\"checkpoint\"].decode())\n", " metadata = self.serde.loads(checkpoint_data[b\"metadata\"].decode())\n", " parent_ts = checkpoint_data.get(b\"parent_ts\", b\"\").decode()\n", - " parent_config = {\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": parent_ts}} if parent_ts else None\n", - " logger.info(f\"Checkpoint retrieved successfully for thread_id: {thread_id}, ts: {thread_ts}\")\n", - " return CheckpointTuple(config=config, checkpoint=checkpoint, metadata=metadata, parent_config=parent_config)\n", + " parent_config = (\n", + " {\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": parent_ts}}\n", + " if parent_ts\n", + " else None\n", + " )\n", + " logger.info(\n", + " f\"Checkpoint retrieved successfully for thread_id: {thread_id}, ts: {thread_ts}\"\n", + " )\n", + " return CheckpointTuple(\n", + " config=config,\n", + " checkpoint=checkpoint,\n", + " metadata=metadata,\n", + " parent_config=parent_config,\n", + " )\n", " except Exception as e:\n", " logger.error(f\"Failed to get checkpoint tuple: {e}\")\n", " raise\n", "\n", - " def list(self, config: Optional[RunnableConfig], *, filter: Optional[dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None) -> Generator[CheckpointTuple, None, None]:\n", + " def list(\n", + " self,\n", + " config: Optional[RunnableConfig],\n", + " *,\n", + " filter: Optional[dict[str, Any]] = None,\n", + " before: Optional[RunnableConfig] = None,\n", + " limit: Optional[int] = None,\n", + " ) -> Generator[CheckpointTuple, None, None]:\n", " thread_id = config[\"configurable\"][\"thread_id\"] if config else \"*\"\n", " pattern = f\"checkpoint:{thread_id}:*\"\n", " try:\n", " with _get_sync_connection(self.sync_connection) as conn:\n", " keys = conn.keys(pattern)\n", " if before:\n", - " keys = [k for k in keys if k.decode().split(\":\")[-1] < before[\"configurable\"][\"thread_ts\"]]\n", - " keys = sorted(keys, key=lambda k: k.decode().split(\":\")[-1], reverse=True)\n", + " keys = [\n", + " k\n", + " for k in keys\n", + " if k.decode().split(\":\")[-1]\n", + " < before[\"configurable\"][\"thread_ts\"]\n", + " ]\n", + " keys = sorted(\n", + " keys, key=lambda k: k.decode().split(\":\")[-1], reverse=True\n", + " )\n", " if limit:\n", " keys = keys[:limit]\n", " for key in keys:\n", @@ -268,25 +346,53 @@ " if data and \"checkpoint\" in data and \"metadata\" in data:\n", " thread_ts = key.decode().split(\":\")[-1]\n", " yield CheckpointTuple(\n", - " config={\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": thread_ts}},\n", + " config={\n", + " \"configurable\": {\n", + " \"thread_id\": thread_id,\n", + " \"thread_ts\": thread_ts,\n", + " }\n", + " },\n", " checkpoint=self.serde.loads(data[\"checkpoint\"].decode()),\n", " metadata=self.serde.loads(data[\"metadata\"].decode()),\n", - " parent_config={\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": data.get(\"parent_ts\", b\"\").decode()}} if data.get(\"parent_ts\") else None,\n", + " parent_config={\n", + " \"configurable\": {\n", + " \"thread_id\": thread_id,\n", + " \"thread_ts\": data.get(\"parent_ts\", b\"\").decode(),\n", + " }\n", + " }\n", + " if data.get(\"parent_ts\")\n", + " else None,\n", + " )\n", + " logger.info(\n", + " f\"Checkpoint listed for thread_id: {thread_id}, ts: {thread_ts}\"\n", " )\n", - " logger.info(f\"Checkpoint listed for thread_id: {thread_id}, ts: {thread_ts}\")\n", " except Exception as e:\n", " logger.error(f\"Failed to list checkpoints: {e}\")\n", " raise\n", "\n", - " async def alist(self, config: Optional[RunnableConfig], *, filter: Optional[dict[str, Any]] = None, before: Optional[RunnableConfig] = None, limit: Optional[int] = None) -> AsyncGenerator[CheckpointTuple, None]:\n", + " async def alist(\n", + " self,\n", + " config: Optional[RunnableConfig],\n", + " *,\n", + " filter: Optional[dict[str, Any]] = None,\n", + " before: Optional[RunnableConfig] = None,\n", + " limit: Optional[int] = None,\n", + " ) -> AsyncGenerator[CheckpointTuple, None]:\n", " thread_id = config[\"configurable\"][\"thread_id\"] if config else \"*\"\n", " pattern = f\"checkpoint:{thread_id}:*\"\n", " try:\n", " async with _get_async_connection(self.async_connection) as conn:\n", " keys = await conn.keys(pattern)\n", " if before:\n", - " keys = [k for k in keys if k.decode().split(\":\")[-1] < before[\"configurable\"][\"thread_ts\"]]\n", - " keys = sorted(keys, key=lambda k: k.decode().split(\":\")[-1], reverse=True)\n", + " keys = [\n", + " k\n", + " for k in keys\n", + " if k.decode().split(\":\")[-1]\n", + " < before[\"configurable\"][\"thread_ts\"]\n", + " ]\n", + " keys = sorted(\n", + " keys, key=lambda k: k.decode().split(\":\")[-1], reverse=True\n", + " )\n", " if limit:\n", " keys = keys[:limit]\n", " for key in keys:\n", @@ -294,15 +400,29 @@ " if data and \"checkpoint\" in data and \"metadata\" in data:\n", " thread_ts = key.decode().split(\":\")[-1]\n", " yield CheckpointTuple(\n", - " config={\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": thread_ts}},\n", + " config={\n", + " \"configurable\": {\n", + " \"thread_id\": thread_id,\n", + " \"thread_ts\": thread_ts,\n", + " }\n", + " },\n", " checkpoint=self.serde.loads(data[\"checkpoint\"].decode()),\n", " metadata=self.serde.loads(data[\"metadata\"].decode()),\n", - " parent_config={\"configurable\": {\"thread_id\": thread_id, \"thread_ts\": data.get(\"parent_ts\", b\"\").decode()}} if data.get(\"parent_ts\") else None,\n", + " parent_config={\n", + " \"configurable\": {\n", + " \"thread_id\": thread_id,\n", + " \"thread_ts\": data.get(\"parent_ts\", b\"\").decode(),\n", + " }\n", + " }\n", + " if data.get(\"parent_ts\")\n", + " else None,\n", + " )\n", + " logger.info(\n", + " f\"Checkpoint listed for thread_id: {thread_id}, ts: {thread_ts}\"\n", " )\n", - " logger.info(f\"Checkpoint listed for thread_id: {thread_id}, ts: {thread_ts}\")\n", " except Exception as e:\n", " logger.error(f\"Failed to list checkpoints: {e}\")\n", - " raise\n" + " raise" ] }, { @@ -538,7 +658,7 @@ "import redis\n", "\n", "# Initialize the Redis synchronous direct connection\n", - "sync_redis_direct = redis.Redis(host='172.25.0.4', port=6379, db=0)\n", + "sync_redis_direct = redis.Redis(host=\"172.25.0.4\", port=6379, db=0)\n", "\n", "# Initialize the RedisSaver with the synchronous direct connection\n", "checkpointer = RedisSaver(sync_connection=sync_redis_direct)\n", @@ -582,7 +702,7 @@ ], "source": [ "# Initialize a synchronous Redis connection pool\n", - "async_pool = initialize_async_pool(url='redis://172.25.0.4:6379/0')\n", + "async_pool = initialize_async_pool(url=\"redis://172.25.0.4:6379/0\")\n", "\n", "checkpointer = RedisSaver(async_connection=async_pool)" ] @@ -687,7 +807,7 @@ "source": [ "from redis.asyncio import Redis as AsyncRedis\n", "\n", - "async with await AsyncRedis(host='172.25.0.4', port=6379, db=0) as conn:\n", + "async with await AsyncRedis(host=\"172.25.0.4\", port=6379, db=0) as conn:\n", " checkpointer = RedisSaver(async_connection=conn)\n", " graph = create_react_agent(model, tools=tools, checkpointer=checkpointer)\n", " config = {\"configurable\": {\"thread_id\": \"4\"}}\n", diff --git a/examples/streaming-events-from-within-tools-without-langchain.ipynb b/examples/streaming-events-from-within-tools-without-langchain.ipynb index 1c703a921..e0906c837 100644 --- a/examples/streaming-events-from-within-tools-without-langchain.ipynb +++ b/examples/streaming-events-from-within-tools-without-langchain.ipynb @@ -88,40 +88,36 @@ "from openai import AsyncOpenAI\n", "from langchain_core.language_models.chat_models import ChatGenerationChunk\n", "from langchain_core.messages import AIMessageChunk\n", - "from langchain_core.runnables.config import ensure_config, get_callback_manager_for_config\n", + "from langchain_core.runnables.config import (\n", + " ensure_config,\n", + " get_callback_manager_for_config,\n", + ")\n", "\n", "openai_client = AsyncOpenAI()\n", "# define tool schema for openai tool calling\n", "\n", "tool = {\n", - " \"type\": \"function\",\n", - " \"function\": {\n", - " \"name\": \"get_items\",\n", - " \"description\": \"Use this tool to look up which items are in the given place.\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"place\": {\n", - " \"type\": \"string\"\n", - " }\n", - " },\n", - " \"required\": [\n", - " \"place\"\n", - " ]\n", - " }\n", - " }\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_items\",\n", + " \"description\": \"Use this tool to look up which items are in the given place.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\"place\": {\"type\": \"string\"}},\n", + " \"required\": [\"place\"],\n", + " },\n", + " },\n", "}\n", + "\n", + "\n", "async def call_model(state, config=None):\n", " config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n", " callback_manager = get_callback_manager_for_config(config)\n", " messages = state[\"messages\"]\n", - " \n", + "\n", " llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n", " response = await openai_client.chat.completions.create(\n", - " messages=messages,\n", - " model=\"gpt-3.5-turbo\",\n", - " tools=[tool],\n", - " stream=True\n", + " messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n", " )\n", "\n", " response_content = \"\"\n", @@ -147,7 +143,10 @@ "\n", " # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n", " tool_call_chunk = ChatGenerationChunk(\n", - " message=AIMessageChunk(content=\"\", additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]})\n", + " message=AIMessageChunk(\n", + " content=\"\",\n", + " additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n", + " )\n", " )\n", " llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n", " tool_call_function_arguments += delta.tool_calls[0].function.arguments\n", @@ -156,8 +155,11 @@ " tool_calls = [\n", " {\n", " \"id\": tool_call_id,\n", - " \"function\": {\"name\": tool_call_function_name, \"arguments\": tool_call_function_arguments},\n", - " \"type\": \"function\"\n", + " \"function\": {\n", + " \"name\": tool_call_function_name,\n", + " \"arguments\": tool_call_function_arguments,\n", + " },\n", + " \"type\": \"function\",\n", " }\n", " ]\n", " else:\n", @@ -166,7 +168,7 @@ " response_message = {\n", " \"role\": role,\n", " \"content\": response_content,\n", - " \"tool_calls\": tool_calls\n", + " \"tool_calls\": tool_calls,\n", " }\n", " return {\"messages\": [response_message]}" ] @@ -189,8 +191,10 @@ "import json\n", "from langchain_core.callbacks import adispatch_custom_event\n", "\n", + "\n", "async def get_items(place: str) -> str:\n", " \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n", + "\n", " # this can be replaced with any actual streaming logic that you might have\n", " def stream(place: str):\n", " if \"bed\" in place: # For under the bed\n", @@ -205,18 +209,22 @@ " await adispatch_custom_event(\n", " # this will allow you to filter events by name\n", " \"tool_call_token_stream\",\n", - " {\"function_name\": \"get_items\", \"arguments\": {\"place\": place}, \"tool_output_token\": token},\n", + " {\n", + " \"function_name\": \"get_items\",\n", + " \"arguments\": {\"place\": place},\n", + " \"tool_output_token\": token,\n", + " },\n", " # this will allow you to filter events by tags\n", - " config={\"tags\": [\"tool_call\"]}\n", + " config={\"tags\": [\"tool_call\"]},\n", " )\n", " tokens.append(token)\n", "\n", " return \", \".join(tokens)\n", "\n", + "\n", "# define mapping to look up functions when running tools\n", - "function_name_to_function = {\n", - " \"get_items\": get_items\n", - "}\n", + "function_name_to_function = {\"get_items\": get_items}\n", + "\n", "\n", "async def call_tools(state):\n", " messages = state[\"messages\"]\n", @@ -225,17 +233,15 @@ " function_name = tool_call[\"function\"][\"name\"]\n", " function_arguments = tool_call[\"function\"][\"arguments\"]\n", " arguments = json.loads(function_arguments)\n", - " \n", - " function_response = await function_name_to_function[function_name](**arguments) \n", + "\n", + " function_response = await function_name_to_function[function_name](**arguments)\n", " tool_message = {\n", " \"tool_call_id\": tool_call[\"id\"],\n", " \"role\": \"tool\",\n", " \"name\": function_name,\n", " \"content\": function_response,\n", " }\n", - " return {\n", - " \"messages\": [tool_message]\n", - " }" + " return {\"messages\": [tool_message]}" ] }, { @@ -258,16 +264,19 @@ "\n", "from langgraph.graph import StateGraph, END\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, operator.add]\n", "\n", + "\n", "def should_continue(state) -> Literal[\"tools\", END]:\n", - " messages = state['messages']\n", + " messages = state[\"messages\"]\n", " last_message = messages[-1]\n", " if last_message[\"tool_calls\"]:\n", " return \"tools\"\n", " return END\n", "\n", + "\n", "workflow = StateGraph(State)\n", "workflow.set_entry_point(\"model\")\n", "workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n", @@ -310,7 +319,9 @@ } ], "source": [ - "async for event in graph.astream_events({\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"):\n", + "async for event in graph.astream_events(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n", + "):\n", " tags = event.get(\"tags\", [])\n", " if event[\"event\"] == \"on_custom_event\" and \"tool_call\" in tags:\n", " print(\"Tool token\", event[\"data\"][\"tool_output_token\"])" diff --git a/examples/streaming-tokens-without-langchain.ipynb b/examples/streaming-tokens-without-langchain.ipynb index b2aba44ee..d31f287f8 100644 --- a/examples/streaming-tokens-without-langchain.ipynb +++ b/examples/streaming-tokens-without-langchain.ipynb @@ -88,40 +88,36 @@ "from openai import AsyncOpenAI\n", "from langchain_core.language_models.chat_models import ChatGenerationChunk\n", "from langchain_core.messages import AIMessageChunk\n", - "from langchain_core.runnables.config import ensure_config, get_callback_manager_for_config\n", + "from langchain_core.runnables.config import (\n", + " ensure_config,\n", + " get_callback_manager_for_config,\n", + ")\n", "\n", "openai_client = AsyncOpenAI()\n", "# define tool schema for openai tool calling\n", "\n", "tool = {\n", - " \"type\": \"function\",\n", - " \"function\": {\n", - " \"name\": \"get_items\",\n", - " \"description\": \"Use this tool to look up which items are in the given place.\",\n", - " \"parameters\": {\n", - " \"type\": \"object\",\n", - " \"properties\": {\n", - " \"place\": {\n", - " \"type\": \"string\"\n", - " }\n", - " },\n", - " \"required\": [\n", - " \"place\"\n", - " ]\n", - " }\n", - " }\n", + " \"type\": \"function\",\n", + " \"function\": {\n", + " \"name\": \"get_items\",\n", + " \"description\": \"Use this tool to look up which items are in the given place.\",\n", + " \"parameters\": {\n", + " \"type\": \"object\",\n", + " \"properties\": {\"place\": {\"type\": \"string\"}},\n", + " \"required\": [\"place\"],\n", + " },\n", + " },\n", "}\n", + "\n", + "\n", "async def call_model(state, config=None):\n", " config = ensure_config(config | {\"tags\": [\"agent_llm\"]})\n", " callback_manager = get_callback_manager_for_config(config)\n", " messages = state[\"messages\"]\n", - " \n", + "\n", " llm_run_manager = callback_manager.on_chat_model_start({}, [messages])[0]\n", " response = await openai_client.chat.completions.create(\n", - " messages=messages,\n", - " model=\"gpt-3.5-turbo\",\n", - " tools=[tool],\n", - " stream=True\n", + " messages=messages, model=\"gpt-3.5-turbo\", tools=[tool], stream=True\n", " )\n", "\n", " response_content = \"\"\n", @@ -147,7 +143,10 @@ "\n", " # note: we're wrapping the tools calls in ChatGenerationChunk so that the events from .astream_events in the graph can render tool calls correctly\n", " tool_call_chunk = ChatGenerationChunk(\n", - " message=AIMessageChunk(content=\"\", additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]})\n", + " message=AIMessageChunk(\n", + " content=\"\",\n", + " additional_kwargs={\"tool_calls\": [delta.tool_calls[0].dict()]},\n", + " )\n", " )\n", " llm_run_manager.on_llm_new_token(\"\", chunk=tool_call_chunk)\n", " tool_call_function_arguments += delta.tool_calls[0].function.arguments\n", @@ -156,8 +155,11 @@ " tool_calls = [\n", " {\n", " \"id\": tool_call_id,\n", - " \"function\": {\"name\": tool_call_function_name, \"arguments\": tool_call_function_arguments},\n", - " \"type\": \"function\"\n", + " \"function\": {\n", + " \"name\": tool_call_function_name,\n", + " \"arguments\": tool_call_function_arguments,\n", + " },\n", + " \"type\": \"function\",\n", " }\n", " ]\n", " else:\n", @@ -166,7 +168,7 @@ " response_message = {\n", " \"role\": role,\n", " \"content\": response_content,\n", - " \"tool_calls\": tool_calls\n", + " \"tool_calls\": tool_calls,\n", " }\n", " return {\"messages\": [response_message]}" ] @@ -188,6 +190,7 @@ "source": [ "import json\n", "\n", + "\n", "async def get_items(place: str) -> str:\n", " \"\"\"Use this tool to look up which items are in the given place.\"\"\"\n", " if \"bed\" in place: # For under the bed\n", @@ -197,10 +200,10 @@ " else: # if the agent decides to ask about a different place\n", " return \"cat snacks\"\n", "\n", + "\n", "# define mapping to look up functions when running tools\n", - "function_name_to_function = {\n", - " \"get_items\": get_items\n", - "}\n", + "function_name_to_function = {\"get_items\": get_items}\n", + "\n", "\n", "async def call_tools(state):\n", " messages = state[\"messages\"]\n", @@ -209,17 +212,15 @@ " function_name = tool_call[\"function\"][\"name\"]\n", " function_arguments = tool_call[\"function\"][\"arguments\"]\n", " arguments = json.loads(function_arguments)\n", - " \n", - " function_response = await function_name_to_function[function_name](**arguments) \n", + "\n", + " function_response = await function_name_to_function[function_name](**arguments)\n", " tool_message = {\n", " \"tool_call_id\": tool_call[\"id\"],\n", " \"role\": \"tool\",\n", " \"name\": function_name,\n", " \"content\": function_response,\n", " }\n", - " return {\n", - " \"messages\": [tool_message]\n", - " }" + " return {\"messages\": [tool_message]}" ] }, { @@ -242,16 +243,19 @@ "\n", "from langgraph.graph import StateGraph, END\n", "\n", + "\n", "class State(TypedDict):\n", " messages: Annotated[list, operator.add]\n", "\n", + "\n", "def should_continue(state) -> Literal[\"tools\", END]:\n", - " messages = state['messages']\n", + " messages = state[\"messages\"]\n", " last_message = messages[-1]\n", " if last_message[\"tool_calls\"]:\n", " return \"tools\"\n", " return END\n", "\n", + "\n", "workflow = StateGraph(State)\n", "workflow.set_entry_point(\"model\")\n", "workflow.add_node(\"model\", call_model) # i.e. our \"agent\"\n", @@ -325,7 +329,9 @@ } ], "source": [ - "async for event in graph.astream_events({\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"):\n", + "async for event in graph.astream_events(\n", + " {\"messages\": [{\"role\": \"user\", \"content\": \"what's in the bedroom\"}]}, version=\"v2\"\n", + "):\n", " tags = event.get(\"tags\", [])\n", " if event[\"event\"] == \"on_chat_model_stream\" and \"agent_llm\" in tags:\n", " print(\"LLM token\", event[\"data\"][\"chunk\"].dict())" diff --git a/examples/subgraph.ipynb b/examples/subgraph.ipynb index d9fbd4c64..9e98ba844 100644 --- a/examples/subgraph.ipynb +++ b/examples/subgraph.ipynb @@ -22,8 +22,7 @@ "metadata": {}, "outputs": [], "source": [ - "%%capture --no-stderr\n", - "%pip install -U langgraph" + "%%capture --no-stderr\n%pip install -U langgraph" ] }, { @@ -55,15 +54,17 @@ "from langgraph.checkpoint.memory import MemorySaver\n", "from langgraph.graph import StateGraph, START, END\n", "\n", + "\n", "# The structure of the logs\n", "class Logs(TypedDict):\n", " id: str\n", " question: str\n", " docs: Optional[List]\n", - " answer: str \n", + " answer: str\n", " grade: Optional[int]\n", " grader: Optional[str]\n", - " feedback: Optional[str] \n", + " feedback: Optional[str]\n", + "\n", "\n", "# Failure Analysis Sub-graph\n", "class FailureAnalysisState(TypedDict):\n", @@ -71,23 +72,27 @@ " failures: List[Logs]\n", " fa_summary: str\n", "\n", + "\n", "def get_failures(state):\n", - " docs = state['docs']\n", + " docs = state[\"docs\"]\n", " failures = [doc for doc in docs if \"grade\" in doc]\n", " return {\"failures\": failures}\n", "\n", + "\n", "def generate_summary(state):\n", - " failures = state['failures']\n", + " failures = state[\"failures\"]\n", " # Add fxn: fa_summary = summarize(failures)\n", - " fa_summary = \"Poor quality retrieval of Chroma documentation.\" \n", + " fa_summary = \"Poor quality retrieval of Chroma documentation.\"\n", " return {\"fa_summary\": fa_summary}\n", "\n", + "\n", "fa_builder = StateGraph(FailureAnalysisState)\n", "fa_builder.add_node(\"get_failures\", get_failures)\n", "fa_builder.add_node(\"generate_summary\", generate_summary)\n", - "fa_builder.set_entry_point(\"get_failures\")\n", - "fa_builder.add_edge(\"get_failures\",\"generate_summary\")\n", - "fa_builder.set_finish_point(\"generate_summary\")\n", + "fa_builder.add_edge(START, \"get_failures\")\n", + "fa_builder.add_edge(\"get_failures\", \"generate_summary\")\n", + "fa_builder.add_edge(\"generate_summary\", END)\n", + "\n", "\n", "# Summarization subgraph\n", "class QuestionSummarizationState(TypedDict):\n", @@ -95,29 +100,33 @@ " qs_summary: str\n", " report: str\n", "\n", + "\n", "def generate_summary(state):\n", - " docs = state['docs']\n", + " docs = state[\"docs\"]\n", " # Add fxn: summary = summarize(docs)\n", - " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\" \n", + " summary = \"Questions focused on usage of ChatOllama and Chroma vector store.\"\n", " return {\"qs_summary\": summary}\n", "\n", + "\n", "def send_to_slack(state):\n", - " qs_summary = state['qs_summary']\n", + " qs_summary = state[\"qs_summary\"]\n", " # Add fxn: report = report_generation(qs_summary)\n", " report = \"foo bar baz\"\n", " return {\"report\": report}\n", "\n", + "\n", "def format_report_for_slack(state):\n", - " report = state['report']\n", + " report = state[\"report\"]\n", " # Add fxn: formatted_report = report_format(report)\n", " formatted_report = \"foo bar\"\n", " return {\"report\": formatted_report}\n", "\n", + "\n", "qs_builder = StateGraph(QuestionSummarizationState)\n", "qs_builder.add_node(\"generate_summary\", generate_summary)\n", "qs_builder.add_node(\"send_to_slack\", send_to_slack)\n", "qs_builder.add_node(\"format_report_for_slack\", format_report_for_slack)\n", - "qs_builder.set_entry_point(\"generate_summary\")\n", + "qs_builder.add_edge(START, \"generate_summary\")\n", "qs_builder.add_edge(\"generate_summary\", \"send_to_slack\")\n", "qs_builder.add_edge(\"send_to_slack\", \"format_report_for_slack\")\n", "qs_builder.add_edge(\"format_report_for_slack\", END)" @@ -165,25 +174,28 @@ " feedback=\"The retrieved documents discuss vector stores in general, but not Chroma specifically\",\n", ")\n", "\n", + "\n", "# Entry Graph\n", "class EntryGraphState(TypedDict):\n", " raw_logs: Annotated[List[Dict], add]\n", - " docs: Annotated[List[Logs], add] # This will be used in sub-graphs\n", - " fa_summary: str # This will be generated in the FA sub-graph\n", - " report: str # This will be generated in the QS sub-graph\n", + " docs: Annotated[List[Logs], add] # This will be used in sub-graphs\n", + " fa_summary: str # This will be generated in the FA sub-graph\n", + " report: str # This will be generated in the QS sub-graph\n", + "\n", "\n", "def convert_logs_to_docs(state):\n", " # Get logs\n", - " raw_logs = state['raw_logs']\n", - " docs = [question_answer,question_answer_feedback]\n", - " return {\"docs\": docs} \n", + " raw_logs = state[\"raw_logs\"]\n", + " docs = [question_answer, question_answer_feedback]\n", + " return {\"docs\": docs}\n", + "\n", "\n", "entry_builder = StateGraph(EntryGraphState)\n", "entry_builder.add_node(\"convert_logs_to_docs\", convert_logs_to_docs)\n", "entry_builder.add_node(\"question_summarization\", qs_builder.compile())\n", "entry_builder.add_node(\"failure_analysis\", fa_builder.compile())\n", "\n", - "entry_builder.set_entry_point(\"convert_logs_to_docs\")\n", + "entry_builder.add_edge(START, \"convert_logs_to_docs\")\n", "entry_builder.add_edge(\"convert_logs_to_docs\", \"failure_analysis\")\n", "entry_builder.add_edge(\"convert_logs_to_docs\", \"question_summarization\")\n", "entry_builder.add_edge(\"failure_analysis\", END)\n", @@ -192,6 +204,7 @@ "graph = entry_builder.compile()\n", "\n", "from IPython.display import Image, display\n", + "\n", "# Setting xray to 1 will show the internal structure of the nested graph\n", "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" ] @@ -233,7 +246,7 @@ } ], "source": [ - "raw_logs = [{\"foo\":\"bar\"},{\"foo\":\"baz\"}]\n", + "raw_logs = [{\"foo\": \"bar\"}, {\"foo\": \"baz\"}]\n", "graph.invoke({\"raw_logs\": raw_logs}, debug=False)" ] }, @@ -260,6 +273,7 @@ "\n", "from typing_extensions import TypedDict\n", "\n", + "\n", "def reduce_list(left: list | None, right: list | None) -> list:\n", " if not left:\n", " left = []\n", @@ -324,10 +338,7 @@ } ], "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + "from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))" ] }, { @@ -445,42 +456,7 @@ "metadata": {}, "outputs": [], "source": [ - "import uuid\n", - "\n", - "\n", - "def reduce_list(left: list | None, right: list | None) -> list:\n", - " \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n", - " if not left:\n", - " left = []\n", - " if not right:\n", - " right = []\n", - " left_, right_ = [], []\n", - " for orig, new in [(left, left_), (right, right_)]:\n", - " for val in orig:\n", - " if not isinstance(val, dict):\n", - " val = {\"val\": val}\n", - " if \"id\" not in val:\n", - " val[\"id\"] = str(uuid.uuid4())\n", - " new.append(val)\n", - " # Merge the two lists\n", - " left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n", - " merged = left_.copy()\n", - " for val in right_:\n", - " if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n", - " merged[existing_idx] = val\n", - " else:\n", - " merged.append(val)\n", - " return merged\n", - "\n", - "\n", - "class ChildState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]\n", - "\n", - "\n", - "class ParentState(TypedDict):\n", - " name: str\n", - " path: Annotated[list[str], reduce_list]" + "import uuid\n\n\ndef reduce_list(left: list | None, right: list | None) -> list:\n \"\"\"Append the right-hand list, replacing any elements with the same id in the left-hand list.\"\"\"\n if not left:\n left = []\n if not right:\n right = []\n left_, right_ = [], []\n for orig, new in [(left, left_), (right, right_)]:\n for val in orig:\n if not isinstance(val, dict):\n val = {\"val\": val}\n if \"id\" not in val:\n val[\"id\"] = str(uuid.uuid4())\n new.append(val)\n # Merge the two lists\n left_idx_by_id = {val[\"id\"]: i for i, val in enumerate(left_)}\n merged = left_.copy()\n for val in right_:\n if (existing_idx := left_idx_by_id.get(val[\"id\"])) is not None:\n merged[existing_idx] = val\n else:\n merged.append(val)\n return merged\n\n\nclass ChildState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]\n\n\nclass ParentState(TypedDict):\n name: str\n path: Annotated[list[str], reduce_list]" ] }, { @@ -489,33 +465,7 @@ "metadata": {}, "outputs": [], "source": [ - "child_builder = StateGraph(ChildState)\n", - "\n", - "child_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\n", - "child_builder.add_edge(START, \"child_start\")\n", - "child_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\n", - "child_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\n", - "child_builder.add_edge(\"child_start\", \"child_middle\")\n", - "child_builder.add_edge(\"child_middle\", \"child_end\")\n", - "child_builder.add_edge(\"child_end\", END)\n", - "\n", - "builder = StateGraph(ParentState)\n", - "\n", - "builder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\n", - "builder.add_edge(START, \"grandparent\")\n", - "builder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\n", - "builder.add_node(\"child\", child_builder.compile())\n", - "builder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\n", - "builder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n", - "\n", - "# Add connections\n", - "builder.add_edge(\"grandparent\", \"parent\")\n", - "builder.add_edge(\"parent\", \"child\")\n", - "builder.add_edge(\"parent\", \"sibling\")\n", - "builder.add_edge(\"child\", \"fin\")\n", - "builder.add_edge(\"sibling\", \"fin\")\n", - "builder.add_edge(\"fin\", END)\n", - "graph = builder.compile()" + "child_builder = StateGraph(ChildState)\n\nchild_builder.add_node(\"child_start\", lambda state: {\"path\": [\"child_start\"]})\nchild_builder.add_edge(START, \"child_start\")\nchild_builder.add_node(\"child_middle\", lambda state: {\"path\": [\"child_middle\"]})\nchild_builder.add_node(\"child_end\", lambda state: {\"path\": [\"child_end\"]})\nchild_builder.add_edge(\"child_start\", \"child_middle\")\nchild_builder.add_edge(\"child_middle\", \"child_end\")\nchild_builder.add_edge(\"child_end\", END)\n\nbuilder = StateGraph(ParentState)\n\nbuilder.add_node(\"grandparent\", lambda state: {\"path\": [\"grandparent\"]})\nbuilder.add_edge(START, \"grandparent\")\nbuilder.add_node(\"parent\", lambda state: {\"path\": [\"parent\"]})\nbuilder.add_node(\"child\", child_builder.compile())\nbuilder.add_node(\"sibling\", lambda state: {\"path\": [\"sibling\"]})\nbuilder.add_node(\"fin\", lambda state: {\"path\": [\"fin\"]})\n\n# Add connections\nbuilder.add_edge(\"grandparent\", \"parent\")\nbuilder.add_edge(\"parent\", \"child\")\nbuilder.add_edge(\"parent\", \"sibling\")\nbuilder.add_edge(\"child\", \"fin\")\nbuilder.add_edge(\"sibling\", \"fin\")\nbuilder.add_edge(\"fin\", END)\ngraph = builder.compile()" ] }, { @@ -535,10 +485,7 @@ } ], "source": [ - "from IPython.display import Image, display\n", - "\n", - "# Setting xray to 1 will show the internal structure of the nested graph\n", - "display(Image(graph.get_graph(xray=1).draw_mermaid_png()))" + "from IPython.display import Image, display\n\n# Setting xray to 1 will show the internal structure of the nested graph\ndisplay(Image(graph.get_graph(xray=1).draw_mermaid_png()))" ] }, { diff --git a/examples/tool-calling-errors.ipynb b/examples/tool-calling-errors.ipynb index 677468309..592db9817 100644 --- a/examples/tool-calling-errors.ipynb +++ b/examples/tool-calling-errors.ipynb @@ -547,8 +547,8 @@ ], "source": [ "stream = app.stream(\n", - " {\"messages\": [(\"human\", \"Write me an incredible haiku about water.\")]},\n", - " {\"recursion_limit\": 10},\n", + " {\"messages\": [(\"human\", \"Write me an incredible haiku about water.\")]},\n", + " {\"recursion_limit\": 10},\n", ")\n", "\n", "for chunk in stream:\n", diff --git a/examples/tool-calling.ipynb b/examples/tool-calling.ipynb index e6b088ed5..0b5148574 100644 --- a/examples/tool-calling.ipynb +++ b/examples/tool-calling.ipynb @@ -139,7 +139,14 @@ "source": [ "message_with_single_tool_call = AIMessage(\n", " content=\"\",\n", - " tool_calls=[{'name': 'get_weather', 'args': {'location': 'sf'}, 'id': 'tool_call_id', 'type': 'tool_call'}]\n", + " tool_calls=[\n", + " {\n", + " \"name\": \"get_weather\",\n", + " \"args\": {\"location\": \"sf\"},\n", + " \"id\": \"tool_call_id\",\n", + " \"type\": \"tool_call\",\n", + " }\n", + " ],\n", ")\n", "\n", "tool_node.invoke({\"messages\": [message_with_single_tool_call]})" @@ -175,9 +182,19 @@ "message_with_multiple_tool_calls = AIMessage(\n", " content=\"\",\n", " tool_calls=[\n", - " {'name': 'get_coolest_cities', 'args': {}, 'id': 'tool_call_id_1', 'type': 'tool_call'},\n", - " {'name': 'get_weather', 'args': {'location': 'sf'}, 'id': 'tool_call_id_2', 'type': 'tool_call'}\n", - " ]\n", + " {\n", + " \"name\": \"get_coolest_cities\",\n", + " \"args\": {},\n", + " \"id\": \"tool_call_id_1\",\n", + " \"type\": \"tool_call\",\n", + " },\n", + " {\n", + " \"name\": \"get_weather\",\n", + " \"args\": {\"location\": \"sf\"},\n", + " \"id\": \"tool_call_id_2\",\n", + " \"type\": \"tool_call\",\n", + " },\n", + " ],\n", ")\n", "\n", "tool_node.invoke({\"messages\": [message_with_multiple_tool_calls]})" @@ -210,7 +227,6 @@ "from langgraph.prebuilt import ToolNode\n", "\n", "\n", - "\n", "model_with_tools = ChatAnthropic(\n", " model=\"claude-3-haiku-20240307\", temperature=0\n", ").bind_tools(tools)" @@ -454,7 +470,8 @@ "# example with a multiple tool calls in succession\n", "\n", "for chunk in app.stream(\n", - " {\"messages\": [(\"human\", \"what's the weather in the coolest cities?\")]}, stream_mode=\"values\"\n", + " {\"messages\": [(\"human\", \"what's the weather in the coolest cities?\")]},\n", + " stream_mode=\"values\",\n", "):\n", " chunk[\"messages\"][-1].pretty_print()" ] From 610b6cc78cf682e4dbc7c155fbda04227136ebf0 Mon Sep 17 00:00:00 2001 From: Bagatur <22008038+baskaryan@users.noreply.github.com> Date: Fri, 19 Jul 2024 20:07:21 -0700 Subject: [PATCH 28/28] langgraph[patch]: InjectedState annotation (#1067) Add annotated for injecting state vars into a Tool --- docs/docs/reference/prebuilt.md | 11 +- examples/pass-run-time-values-to-tools.ipynb | 105 +++++------- libs/langgraph/langgraph/prebuilt/__init__.py | 3 +- .../langgraph/langgraph/prebuilt/tool_node.py | 159 ++++++++++++++++-- libs/langgraph/poetry.lock | 2 +- libs/langgraph/pyproject.toml | 2 +- libs/langgraph/tests/test_prebuilt.py | 85 ++++++++-- 7 files changed, 277 insertions(+), 90 deletions(-) diff --git a/docs/docs/reference/prebuilt.md b/docs/docs/reference/prebuilt.md index fc276aea0..4bfc9b314 100644 --- a/docs/docs/reference/prebuilt.md +++ b/docs/docs/reference/prebuilt.md @@ -55,4 +55,13 @@ from langgraph.prebuilt import tools_condition from langgraph.prebuilt import ValidationNode ``` -::: langgraph.prebuilt.ValidationNode \ No newline at end of file +::: langgraph.prebuilt.ValidationNode + +## InjectedState + +```python +from langgraph.prebuilt import InjectedState +``` + +::: langgraph.prebuilt.InjectedState + handler: python diff --git a/examples/pass-run-time-values-to-tools.ipynb b/examples/pass-run-time-values-to-tools.ipynb index 48d575aa1..3c74bd06d 100644 --- a/examples/pass-run-time-values-to-tools.ipynb +++ b/examples/pass-run-time-values-to-tools.ipynb @@ -86,30 +86,30 @@ "source": [ "## Defining the tools\n", "\n", - "We'll want our tool to take graph state as an input, but we don't want the model to try to generate this input when calling the tool. We can use the `InjectedToolArg` annotation to mark `state` as being injected at runtime. Any argument annotated with `InjectedToolArg` will not be generated by the model.\n", + "We'll want our tool to take graph state as an input, but we don't want the model to try to generate this input when calling the tool. We can use the `InjectedState` annotation to mark arguments as required graph state (or some field of graph state. These arguments will not be generated by the model. When using `ToolNode`, graph state will automatically be passed in to the relevant tools and arguments.\n", "\n", "In this example we'll create a tool that returns Documents and then another tool that actually cites the Documents that justify a claim." ] }, { "cell_type": "code", - "execution_count": 63, + "execution_count": 6, "id": "1d36e782-80f4-4334-b7d7-ee4c79864480", "metadata": {}, "outputs": [], "source": [ "from typing import List, Tuple\n", + "from typing_extensions import Annotated\n", "\n", "from langchain_core.documents import Document\n", "from langchain_core.pydantic_v1 import BaseModel\n", - "from langchain_core.tools import InjectedToolArg, tool\n", - "from typing_extensions import Annotated\n", + "from langchain_core.tools import tool\n", + "\n", + "from langgraph.prebuilt import InjectedState\n", "\n", "\n", "@tool(parse_docstring=True, response_format=\"content_and_artifact\")\n", - "def get_context(\n", - " question: List[str], state: Annotated[dict, InjectedToolArg]\n", - ") -> Tuple[str, List[Document]]:\n", + "def get_context(question: List[str]) -> Tuple[str, List[Document]]:\n", " \"\"\"Get context on the question.\n", "\n", " Args:\n", @@ -136,7 +136,7 @@ "\n", "@tool(parse_docstring=True, response_format=\"content_and_artifact\")\n", "def cite_context_sources(\n", - " claim: str, state: Annotated[dict, InjectedToolArg]\n", + " claim: str, state: Annotated[dict, InjectedState]\n", ") -> Tuple[str, List[Document]]:\n", " \"\"\"Cite which source a claim was based on.\n", "\n", @@ -175,31 +175,30 @@ }, { "cell_type": "code", - "execution_count": 64, + "execution_count": 9, "id": "1092929b-c939-4b2a-9f9c-e725b0e34af2", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'title': 'get_contextSchema',\n", - " 'description': 'Get context on the question.',\n", + "{'title': 'cite_context_sourcesSchema',\n", + " 'description': 'Cite which source a claim was based on.',\n", " 'type': 'object',\n", - " 'properties': {'question': {'title': 'Question',\n", - " 'description': 'The user question',\n", - " 'type': 'array',\n", - " 'items': {'type': 'string'}},\n", + " 'properties': {'claim': {'title': 'Claim',\n", + " 'description': 'The claim that was made.',\n", + " 'type': 'string'},\n", " 'state': {'title': 'State', 'type': 'object'}},\n", - " 'required': ['question', 'state']}" + " 'required': ['claim', 'state']}" ] }, - "execution_count": 64, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "get_context.get_input_schema().schema()" + "cite_context_sources.get_input_schema().schema()" ] }, { @@ -212,30 +211,29 @@ }, { "cell_type": "code", - "execution_count": 65, + "execution_count": 11, "id": "3912bb51-3107-4335-a659-021c5d89fb37", "metadata": {}, "outputs": [ { "data": { "text/plain": [ - "{'title': 'get_context',\n", - " 'description': 'Get context on the question.',\n", + "{'title': 'cite_context_sources',\n", + " 'description': 'Cite which source a claim was based on.',\n", " 'type': 'object',\n", - " 'properties': {'question': {'title': 'Question',\n", - " 'description': 'The user question',\n", - " 'type': 'array',\n", - " 'items': {'type': 'string'}}},\n", - " 'required': ['question']}" + " 'properties': {'claim': {'title': 'Claim',\n", + " 'description': 'The claim that was made.',\n", + " 'type': 'string'}},\n", + " 'required': ['claim']}" ] }, - "execution_count": 65, + "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "get_context.tool_call_schema.schema()" + "cite_context_sources.tool_call_schema.schema()" ] }, { @@ -258,7 +256,7 @@ }, { "cell_type": "code", - "execution_count": 66, + "execution_count": 12, "id": "ea793afa-2eab-4901-910d-6eed90cd6564", "metadata": {}, "outputs": [], @@ -302,7 +300,7 @@ }, { "cell_type": "code", - "execution_count": 67, + "execution_count": 18, "id": "3b541bb9-900c-40d0-964d-7b5dfee30667", "metadata": {}, "outputs": [], @@ -312,7 +310,7 @@ "from langchain_core.messages import ToolMessage\n", "from langchain_openai import ChatOpenAI\n", "\n", - "from langgraph.prebuilt import ToolExecutor, ToolInvocation\n", + "from langgraph.prebuilt import ToolNode\n", "\n", "model = ChatOpenAI(model=\"gpt-4o\", temperature=0)\n", "\n", @@ -330,8 +328,6 @@ "\n", "\n", "tools = [get_context, cite_context_sources]\n", - "tool_map = {tool_.name: tool_ for tool_ in tools}\n", - "\n", "\n", "# Define the function that calls the model\n", "def call_model(state, config):\n", @@ -342,25 +338,8 @@ " return {\"messages\": [response]}\n", "\n", "\n", - "# Helper function for adding state to each tool call's arguments\n", - "def inject_state(message, state):\n", - " tool_calls = []\n", - " for tool_call in message.tool_calls:\n", - " tool_call_copy = deepcopy(tool_call)\n", - " tool_call_copy[\"args\"][\"state\"] = state\n", - " tool_calls.append(tool_call_copy)\n", - " return tool_calls\n", - "\n", - "\n", - "# Define the function to execute tools\n", - "def call_tool(state, config):\n", - " messages = state[\"messages\"]\n", - " last_message = messages[-1]\n", - " tool_messages = []\n", - " for tool_call in inject_state(last_message, state):\n", - " tool_messages.append(tool_map[tool_call[\"name\"]].invoke(tool_call, config))\n", - " # We return a list, because this will get added to the existing list\n", - " return {\"messages\": tool_messages}" + "# ToolNode will automatically take care of injecting state into tools\n", + "tool_node = ToolNode(tools)" ] }, { @@ -375,7 +354,7 @@ }, { "cell_type": "code", - "execution_count": 68, + "execution_count": 19, "id": "813ae66c-3b58-4283-a02a-36da72a2ab90", "metadata": {}, "outputs": [], @@ -387,7 +366,7 @@ "\n", "# Define the two nodes we will cycle between\n", "workflow.add_node(\"agent\", call_model)\n", - "workflow.add_node(\"action\", call_tool)\n", + "workflow.add_node(\"action\", tool_node)\n", "\n", "# Set the entrypoint as `agent`\n", "# This means that this node is the first one called\n", @@ -426,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 69, + "execution_count": 20, "id": "a8afd6ef", "metadata": {}, "outputs": [ @@ -464,7 +443,7 @@ }, { "cell_type": "code", - "execution_count": 70, + "execution_count": 21, "id": "8edb04b9-40b6-46f1-a7a8-4b2d8aba7752", "metadata": {}, "outputs": [ @@ -474,19 +453,19 @@ "text": [ "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_aFUFt3TdazRnmD3FTZfxFAgL', 'function': {'arguments': '{\"question\":[\"what\\'s the latest news about FooBar\"]}', 'name': 'get_context'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 22, 'prompt_tokens': 87, 'total_tokens': 109}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-adf99f00-a903-49f2-b0c3-37b84b9b801f-0', tool_calls=[{'name': 'get_context', 'args': {'question': [\"what's the latest news about FooBar\"]}, 'id': 'call_aFUFt3TdazRnmD3FTZfxFAgL', 'type': 'tool_call'}], usage_metadata={'input_tokens': 87, 'output_tokens': 22, 'total_tokens': 109})]}\n", + "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_BidVTw5NiW2wp8Ez7m8dDoHI', 'function': {'arguments': '{\"question\":[\"latest news about FooBar\"]}', 'name': 'get_context'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 87, 'total_tokens': 106}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-fcac1b73-563e-4f4c-b1b0-626f55d377be-0', tool_calls=[{'name': 'get_context', 'args': {'question': ['latest news about FooBar']}, 'id': 'call_BidVTw5NiW2wp8Ez7m8dDoHI', 'type': 'tool_call'}], usage_metadata={'input_tokens': 87, 'output_tokens': 19, 'total_tokens': 106})]}\n", "\n", "---\n", "\n", "Output from node 'action':\n", "---\n", - "{'messages': [ToolMessage(content=\"FooBar company just raised 1 Billion dollars!\\n\\nFooBar company is now only hiring AI's\\n\\nFooBar company was founded in 2019\\n\\nFooBar company makes friendly robots\", name='get_context', tool_call_id='call_aFUFt3TdazRnmD3FTZfxFAgL', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!'), Document(metadata={'source': 'twitter'}, page_content=\"FooBar company is now only hiring AI's\"), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company was founded in 2019'), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company makes friendly robots')])]}\n", + "{'messages': [ToolMessage(content=\"FooBar company just raised 1 Billion dollars!\\n\\nFooBar company is now only hiring AI's\\n\\nFooBar company was founded in 2019\\n\\nFooBar company makes friendly robots\", name='get_context', tool_call_id='call_BidVTw5NiW2wp8Ez7m8dDoHI', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!'), Document(metadata={'source': 'twitter'}, page_content=\"FooBar company is now only hiring AI's\"), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company was founded in 2019'), Document(metadata={'source': 'wikipedia'}, page_content='FooBar company makes friendly robots')])]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='The latest news about FooBar is that the company just raised 1 billion dollars!', response_metadata={'token_usage': {'completion_tokens': 18, 'prompt_tokens': 153, 'total_tokens': 171}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'stop', 'logprobs': None}, id='run-c229a397-fda3-415b-a188-1416fd5f21b7-0', usage_metadata={'input_tokens': 153, 'output_tokens': 18, 'total_tokens': 171})]}\n", + "{'messages': [AIMessage(content='The latest news about FooBar is that the company has just raised 1 billion dollars!', response_metadata={'token_usage': {'completion_tokens': 19, 'prompt_tokens': 150, 'total_tokens': 169}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'stop', 'logprobs': None}, id='run-a8407471-7715-4c16-bd46-c29e5751e882-0', usage_metadata={'input_tokens': 150, 'output_tokens': 19, 'total_tokens': 169})]}\n", "\n", "---\n", "\n" @@ -509,7 +488,7 @@ }, { "cell_type": "code", - "execution_count": 71, + "execution_count": 22, "id": "4a2128ed-e23f-4f25-a026-0c6590f01a1c", "metadata": {}, "outputs": [ @@ -519,19 +498,19 @@ "text": [ "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_qqB4kucZnVhrZ5mJSH1dF8Lb', 'function': {'arguments': '{\"claim\":\"The latest news about FooBar is that the company just raised 1 billion dollars!\"}', 'name': 'cite_context_sources'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 32, 'prompt_tokens': 185, 'total_tokens': 217}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-686d4706-81c9-4ca0-8f09-d9af02f4ad7f-0', tool_calls=[{'name': 'cite_context_sources', 'args': {'claim': 'The latest news about FooBar is that the company just raised 1 billion dollars!'}, 'id': 'call_qqB4kucZnVhrZ5mJSH1dF8Lb', 'type': 'tool_call'}], usage_metadata={'input_tokens': 185, 'output_tokens': 32, 'total_tokens': 217})]}\n", + "{'messages': [AIMessage(content='', additional_kwargs={'tool_calls': [{'id': 'call_EB0zaQypXMqEUzaqwflUr0zH', 'function': {'arguments': '{\"claim\":\"FooBar company just raised 1 Billion dollars!\"}', 'name': 'cite_context_sources'}, 'type': 'function'}]}, response_metadata={'token_usage': {'completion_tokens': 25, 'prompt_tokens': 183, 'total_tokens': 208}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_c4e5b6fa31', 'finish_reason': 'tool_calls', 'logprobs': None}, id='run-b4952777-e2b3-4448-be87-200e6e80981b-0', tool_calls=[{'name': 'cite_context_sources', 'args': {'claim': 'FooBar company just raised 1 Billion dollars!'}, 'id': 'call_EB0zaQypXMqEUzaqwflUr0zH', 'type': 'tool_call'}], usage_metadata={'input_tokens': 183, 'output_tokens': 25, 'total_tokens': 208})]}\n", "\n", "---\n", "\n", "Output from node 'action':\n", "---\n", - "{'messages': [ToolMessage(content='twitter', name='cite_context_sources', tool_call_id='call_qqB4kucZnVhrZ5mJSH1dF8Lb', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!')])]}\n", + "{'messages': [ToolMessage(content='twitter', name='cite_context_sources', tool_call_id='call_EB0zaQypXMqEUzaqwflUr0zH', artifact=[Document(metadata={'source': 'twitter'}, page_content='FooBar company just raised 1 Billion dollars!')])]}\n", "\n", "---\n", "\n", "Output from node 'agent':\n", "---\n", - "{'messages': [AIMessage(content='The information about FooBar raising 1 billion dollars came from Twitter.', response_metadata={'token_usage': {'completion_tokens': 15, 'prompt_tokens': 227, 'total_tokens': 242}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_18cc0f1fa0', 'finish_reason': 'stop', 'logprobs': None}, id='run-343ad465-9a62-4d72-91bf-ab29c4fe8781-0', usage_metadata={'input_tokens': 227, 'output_tokens': 15, 'total_tokens': 242})]}\n", + "{'messages': [AIMessage(content='The information that FooBar company just raised 1 billion dollars comes from Twitter.', response_metadata={'token_usage': {'completion_tokens': 17, 'prompt_tokens': 218, 'total_tokens': 235}, 'model_name': 'gpt-4o-2024-05-13', 'system_fingerprint': 'fp_400f27fa1f', 'finish_reason': 'stop', 'logprobs': None}, id='run-a0dede05-dadd-46f6-8654-746520d4cef8-0', usage_metadata={'input_tokens': 218, 'output_tokens': 17, 'total_tokens': 235})]}\n", "\n", "---\n", "\n" diff --git a/libs/langgraph/langgraph/prebuilt/__init__.py b/libs/langgraph/langgraph/prebuilt/__init__.py index b05e0e660..615de4c7b 100644 --- a/libs/langgraph/langgraph/prebuilt/__init__.py +++ b/libs/langgraph/langgraph/prebuilt/__init__.py @@ -3,7 +3,7 @@ from langgraph.prebuilt import chat_agent_executor from langgraph.prebuilt.agent_executor import create_agent_executor from langgraph.prebuilt.chat_agent_executor import create_react_agent from langgraph.prebuilt.tool_executor import ToolExecutor, ToolInvocation -from langgraph.prebuilt.tool_node import ToolNode, tools_condition +from langgraph.prebuilt.tool_node import InjectedState, ToolNode, tools_condition from langgraph.prebuilt.tool_validator import ValidationNode __all__ = [ @@ -15,4 +15,5 @@ __all__ = [ "ToolNode", "tools_condition", "ValidationNode", + "InjectedState", ] diff --git a/libs/langgraph/langgraph/prebuilt/tool_node.py b/libs/langgraph/langgraph/prebuilt/tool_node.py index fc70d261b..72c0d37e2 100644 --- a/libs/langgraph/langgraph/prebuilt/tool_node.py +++ b/libs/langgraph/langgraph/prebuilt/tool_node.py @@ -1,11 +1,24 @@ import asyncio -from typing import Any, Callable, Dict, Literal, Optional, Sequence, Tuple, Union, cast +from copy import copy +from typing import ( + Any, + Callable, + Dict, + List, + Literal, + Optional, + Sequence, + Tuple, + Union, + cast, +) from langchain_core.messages import AIMessage, AnyMessage, ToolCall, ToolMessage from langchain_core.runnables import RunnableConfig from langchain_core.runnables.config import get_config_list, get_executor_for_config -from langchain_core.tools import BaseTool +from langchain_core.tools import BaseTool, InjectedToolArg from langchain_core.tools import tool as create_tool +from typing_extensions import get_args from langgraph.utils import RunnableCallable @@ -60,18 +73,18 @@ class ToolNode(RunnableCallable): def _func( self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig ) -> Any: - message, output_type = self._parse_input(input) - config_list = get_config_list(config, len(message.tool_calls)) + tool_calls, output_type = self._parse_input(input) + config_list = get_config_list(config, len(tool_calls)) with get_executor_for_config(config) as executor: - outputs = [*executor.map(self._run_one, message.tool_calls, config_list)] + outputs = [*executor.map(self._run_one, tool_calls, config_list)] return outputs if output_type == "list" else {"messages": outputs} async def _afunc( self, input: Union[list[AnyMessage], dict[str, Any]], config: RunnableConfig ) -> Any: - message, output_type = self._parse_input(input) + tool_calls, output_type = self._parse_input(input) outputs = await asyncio.gather( - *(self._arun_one(call, config) for call in message.tool_calls) + *(self._arun_one(call, config) for call in tool_calls) ) return outputs if output_type == "list" else {"messages": outputs} @@ -102,7 +115,7 @@ class ToolNode(RunnableCallable): def _parse_input( self, input: Union[list[AnyMessage], dict[str, Any]] - ) -> Tuple[AIMessage, Literal["list", "dict"]]: + ) -> Tuple[List[ToolCall], Literal["list", "dict"]]: if isinstance(input, list): output_type = "list" message: AnyMessage = input[-1] @@ -114,8 +127,12 @@ class ToolNode(RunnableCallable): if not isinstance(message, AIMessage): raise ValueError("Last message is not an AIMessage") - else: - return cast(AIMessage, message), output_type + + tool_calls = [ + self._inject_state(call, input) + for call in cast(AIMessage, message).tool_calls + ] + return tool_calls, output_type def _validate_tool_call(self, call: ToolCall) -> Optional[ToolMessage]: if (requested_tool := call["name"]) not in self.tools_by_name: @@ -127,6 +144,39 @@ class ToolNode(RunnableCallable): else: return None + def _inject_state( + self, tool_call: ToolCall, input: Union[list[AnyMessage], dict[str, Any]] + ) -> ToolCall: + if tool_call["name"] not in self.tools_by_name: + return tool_call + state_args = _get_state_args(self.tools_by_name[tool_call["name"]]) + if state_args and not isinstance(input, dict): + required_fields = list(state_args.values()) + if ( + len(required_fields) == 1 + and required_fields[0] == "messages" + or required_fields[0] is None + ): + input = {"messages": input} + else: + err_msg = ( + f"Invalid input to ToolNode. Tool {tool_call['name']} requires " + f"graph state dict as input." + ) + if any(state_field for state_field in state_args.values()): + required_fields_str = ", ".join(f for f in required_fields if f) + err_msg += f" State should contain fields {required_fields_str}." + raise ValueError(err_msg) + tool_call_copy: ToolCall = copy(tool_call) + tool_call_copy["args"] = { + **tool_call_copy["args"], + **{ + tool_arg: cast(dict, input)[state_field] if state_field else input + for tool_arg, state_field in state_args.items() + }, + } + return tool_call_copy + def tools_condition( state: Union[list[AnyMessage], dict[str, Any]], @@ -183,3 +233,92 @@ def tools_condition( if hasattr(ai_message, "tool_calls") and len(ai_message.tool_calls) > 0: return "tools" return "__end__" + + +class InjectedState(InjectedToolArg): + """Annotation for a Tool arg that is meant to be populated with the graph state. + + Any Tool argument annotated with InjectedState will be hidden from a tool-calling + model, so that the model doesn't attempt to generate the argument. If using + ToolNode, the appropriate graph state field will be automatically injected into + the model-generated tool args. + + Args: + field: The key from state to insert. If None, the entire state is expected to + be passed in. + + Example: + ```python + from typing import List + from typing_extensions import Annotated, TypedDict + + from langchain_core.messages import BaseMessage, AIMessage + from langchain_core.tools import tool + + from langgraph.prebuilt import InjectedState, ToolNode + + + class AgentState(TypedDict): + messages: List[BaseMessage] + foo: str + + @tool + def state_tool(x: int, state: Annotated[dict, InjectedState]) -> str: + '''Do something with state.''' + if len(state["messages"]) > 2: + return state["foo"] + str(x) + else: + return "not enough messages" + + @tool + def foo_tool(x: int, foo: Annotated[str, InjectedState("foo")]) -> str: + '''Do something else with state.''' + return foo + str(x + 1) + + node = ToolNode([state_tool, foo_tool]) + + tool_call1 = {"name": "state_tool", "args": {"x": 1}, "id": "1", "type": "tool_call"} + tool_call2 = {"name": "foo_tool", "args": {"x": 1}, "id": "2", "type": "tool_call"} + state = { + "messages": [AIMessage("", tool_calls=[tool_call1, tool_call2])], + "foo": "bar", + } + node.invoke(state) + ``` + + ```pycon + [ + ToolMessage(content='not enough messages', name='state_tool', tool_call_id='1'), + ToolMessage(content='bar2', name='foo_tool', tool_call_id='2') + ] + ``` + """ # noqa: E501 + + def __init__(self, field: Optional[str] = None) -> None: + self.field = field + + +def _get_state_args(tool: BaseTool) -> Dict[str, Optional[str]]: + full_schema = tool.get_input_schema() + tool_args_to_state_fields: Dict = {} + for name, type_ in full_schema.__annotations__.items(): + injections = [ + type_arg + for type_arg in get_args(type_) + if isinstance(type_arg, InjectedState) + or (isinstance(type_arg, type) and issubclass(type_arg, InjectedState)) + ] + if len(injections) > 1: + raise ValueError( + "A tool argument should not be annotated with InjectedState more than " + f"once. Received arg {name} with annotations {injections}." + ) + elif len(injections) == 1: + injection = injections[0] + if isinstance(injection, InjectedState) and injection.field: + tool_args_to_state_fields[name] = injection.field + else: + tool_args_to_state_fields[name] = None + else: + pass + return tool_args_to_state_fields diff --git a/libs/langgraph/poetry.lock b/libs/langgraph/poetry.lock index 3bee7a407..94075d612 100644 --- a/libs/langgraph/poetry.lock +++ b/libs/langgraph/poetry.lock @@ -4165,4 +4165,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", [metadata] lock-version = "2.0" python-versions = ">=3.9.0,<4.0" -content-hash = "5fb6190a1b01d0cd351ea9a0023c8c8d6acf4fe831101ab87f9fc41308f74b74" +content-hash = "0d877d3879473de43aca1e1d36a8f420ff3f4b140807cb5cc24935d9114947be" diff --git a/libs/langgraph/pyproject.toml b/libs/langgraph/pyproject.toml index 7b4ab2078..593e659c8 100644 --- a/libs/langgraph/pyproject.toml +++ b/libs/langgraph/pyproject.toml @@ -9,7 +9,7 @@ repository = "https://www.github.com/langchain-ai/langgraph" [tool.poetry.dependencies] python = ">=3.9.0,<4.0" -langchain-core = ">=0.2.19,<0.3" +langchain-core = ">=0.2.22,<0.3" [tool.poetry.group.dev.dependencies] diff --git a/libs/langgraph/tests/test_prebuilt.py b/libs/langgraph/tests/test_prebuilt.py index a0a8436e5..e21766cd0 100644 --- a/libs/langgraph/tests/test_prebuilt.py +++ b/libs/langgraph/tests/test_prebuilt.py @@ -1,15 +1,11 @@ -from typing import Any, Callable, Dict, List, Optional, Sequence, Type, Union +from typing import Annotated, Any, Callable, Dict, List, Optional, Sequence, Type, Union import pytest -from langchain_core.callbacks import ( - CallbackManagerForLLMRun, -) -from langchain_core.language_models import ( - BaseChatModel, - LanguageModelInput, -) +from langchain_core.callbacks import CallbackManagerForLLMRun +from langchain_core.language_models import BaseChatModel, LanguageModelInput from langchain_core.messages import ( AIMessage, + AnyMessage, BaseMessage, HumanMessage, SystemMessage, @@ -23,11 +19,8 @@ from langchain_core.tools import tool as dec_tool from pydantic import BaseModel as BaseModelV2 from langgraph.checkpoint.base import BaseCheckpointSaver -from langgraph.prebuilt import ( - ToolNode, - ValidationNode, - create_react_agent, -) +from langgraph.prebuilt import ToolNode, ValidationNode, create_react_agent +from langgraph.prebuilt.tool_node import InjectedState from tests.any_str import AnyStr from tests.memory_assert import MemorySaverAssertImmutable @@ -453,3 +446,69 @@ async def test_validation_node(tool_schema: Any, use_message_key: bool): if use_message_key: result_sync = result_sync["messages"] check_results(result_sync) + + +def test_tool_node_inject_state() -> None: + def tool1(some_val: int, state: Annotated[dict, InjectedState]) -> str: + """Tool 1 docstring.""" + return state["foo"] + + def tool2(some_val: int, state: Annotated[dict, InjectedState()]) -> str: + """Tool 1 docstring.""" + return state["foo"] + + def tool3( + some_val: int, + foo: Annotated[str, InjectedState("foo")], + msgs: Annotated[List[AnyMessage], InjectedState("messages")], + ) -> str: + """Tool 1 docstring.""" + return foo + + def tool4( + some_val: int, msgs: Annotated[List[AnyMessage], InjectedState("messages")] + ) -> str: + """Tool 1 docstring.""" + return msgs[0].content + + node = ToolNode([tool1, tool2, tool3, tool4]) + for tool_name in ("tool1", "tool2", "tool3"): + tool_call = { + "name": tool_name, + "args": {"some_val": 1}, + "id": "some 0", + "type": "tool_call", + } + msg = AIMessage("hi?", tool_calls=[tool_call]) + result = node.invoke({"messages": [msg], "foo": "bar"}) + tool_message = result["messages"][-1] + assert tool_message.content == "bar" + + if tool_name == "tool3": + with pytest.raises(KeyError): + node.invoke({"messages": [msg], "notfoo": "bar"}) + + with pytest.raises(ValueError): + node.invoke([msg]) + else: + tool_message = node.invoke({"messages": [msg], "notfoo": "bar"})[ + "messages" + ][-1] + assert "KeyError" in tool_message.content + tool_message = node.invoke([msg])[-1] + assert "KeyError" in tool_message.content + + tool_call = { + "name": "tool4", + "args": {"some_val": 1}, + "id": "some 0", + "type": "tool_call", + } + msg = AIMessage("hi?", tool_calls=[tool_call]) + result = node.invoke({"messages": [msg]}) + tool_message = result["messages"][-1] + assert tool_message.content == "hi?" + + result = node.invoke([msg]) + tool_message = result[-1] + assert tool_message.content == "hi?"