railtracks
The Railtracks Framework for building resilient agentic systems in simple python
1# ------------------------------------------------------------- 2# Copyright (c) Railtown AI. All rights reserved. 3# Licensed under the MIT License. See LICENSE in project root for information. 4# ------------------------------------------------------------- 5"""The Railtracks Framework for building resilient agentic systems in simple python""" 6 7from __future__ import annotations 8 9import importlib 10import logging 11from typing import TYPE_CHECKING 12 13from dotenv import load_dotenv 14 15if TYPE_CHECKING: 16 from railtracks import retrieval 17 from railtracks.interaction import interactive as interactive 18 19__all__ = [ 20 "Session", 21 "session", 22 "call", 23 "broadcast", 24 "call_batch", 25 "ExecutionInfo", 26 "ExecutorConfig", 27 "llm", 28 "guardrails", 29 "context", 30 "set_config", 31 "context", 32 "function_node", 33 "agent_node", 34 "integrations", 35 "prebuilt", 36 "MCPStdioParams", 37 "MCPHttpParams", 38 "connect_mcp", 39 "create_mcp_server", 40 "ToolManifest", 41 "session_id", 42 "evaluations", 43 "observability", 44 "retrieval", 45 "Flow", 46 "enable_logging", 47 "escape_braces", 48] 49 50from railtracks.built_nodes import ( 51 agent_node, 52 function_node, 53) 54 55from . import ( 56 context, 57 evaluations, 58 guardrails, 59 integrations, 60 llm, 61 observability, 62 prebuilt, 63) 64from ._session import ExecutionInfo, Session, session 65from .context.central import session_id, set_config 66from .interaction import broadcast, call, call_batch 67from .llm.prompt_injection_utils import escape_braces 68from .nodes.manifest import ToolManifest 69from .orchestration.flow import Flow 70from .rt_mcp import MCPHttpParams, MCPStdioParams, connect_mcp, create_mcp_server 71from .utils.config import ExecutorConfig 72from .utils.deprecation import warn_pending_change 73from .utils.logging.config import enable_logging 74 75load_dotenv() 76 77# Library does not configure logging by default. Add NullHandler so the RT logger 78# never emits "No handlers could be found". Call enable_logging() to opt in. 79logging.getLogger("RT").addHandler(logging.NullHandler()) 80 81# Do not worry about changing this version number manually. It will updated on release. 82__version__ = "1.0.0" 83 84 85def __getattr__(name: str): 86 if name == "interactive": 87 # Not cached in globals() 88 warn_pending_change( 89 "rt.interactive", 90 change="is removed", 91 detail="There is no replacement; the local chat UI is going away.", 92 ) 93 return importlib.import_module("railtracks.interaction.interactive") 94 if name == "retrieval": 95 try: 96 module = importlib.import_module("railtracks.retrieval") 97 except ImportError as exc: 98 raise ImportError( 99 "railtracks.retrieval requires the retrieval extras. " 100 "Install with: pip install 'railtracks[retrieval]'" 101 ) from exc 102 globals()[name] = module 103 return module 104 raise AttributeError(f"module {__name__!r} has no attribute {name!r}") 105 106 107def __dir__() -> list[str]: 108 # "interactive" is not in __all__ but is still reachable 109 return sorted({*__all__, "interactive"})
37class Session: 38 """ 39 The main class for managing an execution session. 40 41 This class is responsible for setting up all the necessary components for running a Railtracks execution, including the coordinator, publisher, and state management. 42 43 For the configuration parameters of the setting. It will follow this precedence: 44 1. The parameters in the `Session` constructor. 45 2. The parameters in global context variables. 46 3. The default values. 47 48 Default Values: 49 - `name`: None 50 - `timeout`: 150.0 seconds 51 - `end_on_error`: False 52 - `broadcast_callback`: None (no callback for broadcast messages) 53 - `prompt_injection`: True (the prompt will be automatically injected from context variables) 54 - `save_state`: True (the state of the execution will be saved to a file at the end of the run in the `.railtracks/data/sessions/` directory) 55 56 57 Args: 58 name (str | None, optional): Optional name for the session. This name will be included in the saved state file if `save_state` is True. 59 context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution. 60 flow_name (str | None, optional): The name of the flow this session is associated with. 61 flow_id (str | None, optional): The unique identifier of the flow this session is associated with. 62 timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request. 63 end_on_error (bool, optional): If True, the execution will stop when an exception is encountered. 64 broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages. 65 prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables. 66 save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the `.railtracks/data/sessions/` directory. 67 """ 68 69 def __init__( 70 self, 71 context: Dict[str, Any] | None = None, 72 *, 73 flow_name: str | None = None, 74 flow_id: str | None = None, 75 name: str | None = None, 76 timeout: float | None = None, 77 end_on_error: bool | None = None, 78 broadcast_callback: ( 79 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 80 ) = None, 81 prompt_injection: bool | None = None, 82 save_state: bool | None = None, 83 payload_callback: Callable[[dict[str, Any]], None] | None = None, 84 ): 85 # first lets read from defaults if nessecary for the provided input config 86 87 if flow_name is None: 88 warnings.warn( 89 "Sessions should be tied to a flow for better observability and state management. Please use the Flow object to create and manage your sessions (see __ for more details). This warning will become an error in future versions.", 90 DeprecationWarning, 91 ) 92 93 self.executor_config = self.global_config_precedence( 94 timeout=timeout, 95 end_on_error=end_on_error, 96 broadcast_callback=broadcast_callback, 97 prompt_injection=prompt_injection, 98 save_state=save_state, 99 payload_callback=payload_callback, 100 ) 101 102 if context is None: 103 context = {} 104 105 self.name = name 106 self.flow_name = flow_name 107 self.flow_id = flow_id 108 109 self.publisher: RTPublisher = RTPublisher() 110 111 self._identifier = str(uuid.uuid4()) 112 113 executor_info = ExecutionInfo.create_new() 114 self.coordinator = Coordinator( 115 execution_modes={"async": AsyncioExecutionStrategy()} 116 ) 117 self.rt_state = RTState( 118 executor_info, self.executor_config, self.coordinator, self.publisher 119 ) 120 121 self.coordinator.start(self.publisher) 122 self._setup_subscriber() 123 register_globals( 124 session_id=self._identifier, 125 rt_publisher=self.publisher, 126 parent_id=None, 127 executor_config=self.executor_config, 128 global_context_vars=context, 129 ) 130 131 self._start_time = time.time() 132 133 logger.debug("Session %s is initialized" % self._identifier) 134 135 @classmethod 136 def global_config_precedence( 137 cls, 138 timeout: float | None, 139 end_on_error: bool | None, 140 broadcast_callback: ( 141 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 142 ), 143 prompt_injection: bool | None, 144 save_state: bool | None, 145 payload_callback: Callable[[dict[str, Any]], None] | None, 146 ) -> ExecutorConfig: 147 """ 148 Uses the following precedence order to determine the configuration parameters: 149 1. The parameters in the method parameters. 150 2. The parameters in global context variables. 151 3. The default values. 152 """ 153 global_executor_config = get_global_config() 154 155 return global_executor_config.precedence_overwritten( 156 timeout=timeout, 157 end_on_error=end_on_error, 158 subscriber=broadcast_callback, 159 prompt_injection=prompt_injection, 160 save_state=save_state, 161 payload_callback=payload_callback, 162 ) 163 164 def __enter__(self): 165 return self 166 167 def __exit__(self, exc_type, exc_val, exc_tb): 168 if self.executor_config.save_state: 169 try: 170 railtracks_dir = resolve_railtracks_home() 171 sessions_dir = railtracks_dir / "data" / "sessions" 172 sessions_dir.mkdir( 173 parents=True, exist_ok=True 174 ) # Creates directory structure if doesn't exist, skips otherwise. 175 176 # Try to create file path with name, fallback to identifier only if there's an issue 177 if self.flow_name is not None: 178 name = self.flow_name 179 elif self.name is not None: 180 name = self.name 181 else: 182 name = "" 183 184 candidate = sessions_dir / f"{name}_{self._identifier}.json" 185 try: 186 candidate.touch() 187 candidate.unlink() 188 file_path = candidate 189 except OSError: 190 logger.warning( 191 get_message( 192 ExceptionMessageKey.INVALID_SESSION_FILE_NAME_WARN 193 ).format(name=name, identifier=self._identifier) 194 ) 195 file_path = sessions_dir / f"{self._identifier}.json" 196 197 logger.info("Saving execution info to %s" % file_path) 198 199 content = json.dumps(self.payload()) 200 file_path.write_text(content) 201 202 except Exception as e: 203 logger.error( 204 "Error while saving execution info to file: %s", 205 e, 206 exc_info=True, 207 ) 208 try: 209 if self.executor_config.payload_callback is not None: 210 self.executor_config.payload_callback(self.payload()) 211 except Exception: 212 # TODO: add logging here. 213 pass 214 215 self._close() 216 217 def _setup_subscriber(self): 218 """ 219 Prepares and attaches the saved broadcast_callback to the publisher attached to this runner. 220 """ 221 222 if self.executor_config.subscriber is not None: 223 self.publisher.subscribe( 224 stream_subscriber(self.executor_config.subscriber), 225 name="Streaming Subscriber", 226 ) 227 228 def _close(self): 229 """ 230 Closes the runner and cleans up all resources. 231 232 - Shuts down the state object 233 - Deletes all the global variables that were registered in the context 234 """ 235 # FIX: Resource leak - publisher background task wasn't being shut down on Session exit 236 # VISION: Session owns publisher lifecycle and must clean up all resources when exiting 237 if self.publisher.is_running(): 238 try: 239 # Signal shutdown by setting the flag - the loop will check this and exit 240 self.publisher._running = False 241 242 # Try to cancel the background task if it exists and isn't done 243 if ( 244 self.publisher.pub_loop is not None 245 and not self.publisher.pub_loop.done() 246 ): 247 try: 248 # Cancel the task - it will check _running and exit naturally 249 self.publisher.pub_loop.cancel() 250 except Exception: 251 # Task might be done or in a different loop, that's okay 252 pass 253 except Exception: 254 # If shutdown fails for any reason, log it but don't crash 255 logger.warning( 256 "Failed to shutdown publisher during Session cleanup. " 257 "This may indicate a resource leak.", 258 exc_info=True, 259 ) 260 261 self.rt_state.shutdown() 262 263 delete_globals() 264 # by deleting all of the state variables we are ensuring that the next time we create a runner it is fresh 265 266 @property 267 def info(self) -> ExecutionInfo: 268 """ 269 Returns the current state of the runner. 270 271 This is useful for debugging and viewing the current state of the run. 272 """ 273 return self.rt_state.info 274 275 def payload(self) -> Dict[str, Any]: 276 """ 277 Gets the complete json payload tied to this session. 278 279 The outputted json schema is maintained in (link here) 280 """ 281 info = self.info 282 283 run_list = info.graph_serialization() 284 285 full_dict = { 286 "flow_name": self.flow_name, 287 "flow_id": self.flow_id, 288 "session_id": self._identifier, 289 "session_name": self.name, 290 "start_time": self._start_time, 291 "end_time": time.time(), 292 "runs": run_list, 293 } 294 295 return json.loads(json.dumps(full_dict))
The main class for managing an execution session.
This class is responsible for setting up all the necessary components for running a Railtracks execution, including the coordinator, publisher, and state management.
For the configuration parameters of the setting. It will follow this precedence:
- The parameters in the
Sessionconstructor. - The parameters in global context variables.
- The default values.
Default Values:
name: Nonetimeout: 150.0 secondsend_on_error: Falsebroadcast_callback: None (no callback for broadcast messages)prompt_injection: True (the prompt will be automatically injected from context variables)save_state: True (the state of the execution will be saved to a file at the end of the run in the.railtracks/data/sessions/directory)
Arguments:
- name (str | None, optional): Optional name for the session. This name will be included in the saved state file if
save_stateis True. - context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution.
- flow_name (str | None, optional): The name of the flow this session is associated with.
- flow_id (str | None, optional): The unique identifier of the flow this session is associated with.
- timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request.
- end_on_error (bool, optional): If True, the execution will stop when an exception is encountered.
- broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages.
- prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables.
- save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the
.railtracks/data/sessions/directory.
69 def __init__( 70 self, 71 context: Dict[str, Any] | None = None, 72 *, 73 flow_name: str | None = None, 74 flow_id: str | None = None, 75 name: str | None = None, 76 timeout: float | None = None, 77 end_on_error: bool | None = None, 78 broadcast_callback: ( 79 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 80 ) = None, 81 prompt_injection: bool | None = None, 82 save_state: bool | None = None, 83 payload_callback: Callable[[dict[str, Any]], None] | None = None, 84 ): 85 # first lets read from defaults if nessecary for the provided input config 86 87 if flow_name is None: 88 warnings.warn( 89 "Sessions should be tied to a flow for better observability and state management. Please use the Flow object to create and manage your sessions (see __ for more details). This warning will become an error in future versions.", 90 DeprecationWarning, 91 ) 92 93 self.executor_config = self.global_config_precedence( 94 timeout=timeout, 95 end_on_error=end_on_error, 96 broadcast_callback=broadcast_callback, 97 prompt_injection=prompt_injection, 98 save_state=save_state, 99 payload_callback=payload_callback, 100 ) 101 102 if context is None: 103 context = {} 104 105 self.name = name 106 self.flow_name = flow_name 107 self.flow_id = flow_id 108 109 self.publisher: RTPublisher = RTPublisher() 110 111 self._identifier = str(uuid.uuid4()) 112 113 executor_info = ExecutionInfo.create_new() 114 self.coordinator = Coordinator( 115 execution_modes={"async": AsyncioExecutionStrategy()} 116 ) 117 self.rt_state = RTState( 118 executor_info, self.executor_config, self.coordinator, self.publisher 119 ) 120 121 self.coordinator.start(self.publisher) 122 self._setup_subscriber() 123 register_globals( 124 session_id=self._identifier, 125 rt_publisher=self.publisher, 126 parent_id=None, 127 executor_config=self.executor_config, 128 global_context_vars=context, 129 ) 130 131 self._start_time = time.time() 132 133 logger.debug("Session %s is initialized" % self._identifier)
135 @classmethod 136 def global_config_precedence( 137 cls, 138 timeout: float | None, 139 end_on_error: bool | None, 140 broadcast_callback: ( 141 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 142 ), 143 prompt_injection: bool | None, 144 save_state: bool | None, 145 payload_callback: Callable[[dict[str, Any]], None] | None, 146 ) -> ExecutorConfig: 147 """ 148 Uses the following precedence order to determine the configuration parameters: 149 1. The parameters in the method parameters. 150 2. The parameters in global context variables. 151 3. The default values. 152 """ 153 global_executor_config = get_global_config() 154 155 return global_executor_config.precedence_overwritten( 156 timeout=timeout, 157 end_on_error=end_on_error, 158 subscriber=broadcast_callback, 159 prompt_injection=prompt_injection, 160 save_state=save_state, 161 payload_callback=payload_callback, 162 )
Uses the following precedence order to determine the configuration parameters:
- The parameters in the method parameters.
- The parameters in global context variables.
- The default values.
266 @property 267 def info(self) -> ExecutionInfo: 268 """ 269 Returns the current state of the runner. 270 271 This is useful for debugging and viewing the current state of the run. 272 """ 273 return self.rt_state.info
Returns the current state of the runner.
This is useful for debugging and viewing the current state of the run.
275 def payload(self) -> Dict[str, Any]: 276 """ 277 Gets the complete json payload tied to this session. 278 279 The outputted json schema is maintained in (link here) 280 """ 281 info = self.info 282 283 run_list = info.graph_serialization() 284 285 full_dict = { 286 "flow_name": self.flow_name, 287 "flow_id": self.flow_id, 288 "session_id": self._identifier, 289 "session_name": self.name, 290 "start_time": self._start_time, 291 "end_time": time.time(), 292 "runs": run_list, 293 } 294 295 return json.loads(json.dumps(full_dict))
Gets the complete json payload tied to this session.
The outputted json schema is maintained in (link here)
352def session( 353 func: Callable[_P, Coroutine[Any, Any, _TOutput]] | None = None, 354 *, 355 name: str | None = None, 356 context: Dict[str, Any] | None = None, 357 timeout: float | None = None, 358 end_on_error: bool | None = None, 359 broadcast_callback: ( 360 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 361 ) = None, 362 prompt_injection: bool | None = None, 363 save_state: bool | None = None, 364) -> ( 365 Callable[_P, Coroutine[Any, Any, Tuple[_TOutput, Session]]] 366 | Callable[ 367 [Callable[_P, Coroutine[Any, Any, _TOutput]]], 368 Callable[_P, Coroutine[Any, Any, Tuple[_TOutput, Session]]], 369 ] 370): 371 """ 372 This decorator automatically creates and manages a Session context for the decorated function, 373 allowing async functions to use Railtracks operations without manually managing the session lifecycle. 374 375 Can be used as: 376 - @session (without parentheses) - uses default settings 377 - @session() (with empty parentheses) - uses default settings 378 - @session(name="my_task", timeout=30) (with configuration parameters) 379 380 When using this decorator, the function returns a tuple containing: 381 1. The original function's return value 382 2. The Session object used during execution 383 384 This allows access to session information (like execution state, logs, etc.) after the function completes, 385 while maintaining the simplicity of decorator usage. 386 387 Args: 388 name (str | None, optional): Optional name for the session. This name will be included in the saved state file if `save_state` is True. 389 context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution. 390 timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request. 391 end_on_error (bool, optional): If True, the execution will stop when an exception is encountered. 392 broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages. 393 prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables. 394 save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the `.railtracks/data/sessions/` directory. 395 396 Returns: 397 When used as @session (without parentheses): Returns the decorated function that returns (result, session). 398 When used as @session(...) (with parameters): Returns a decorator function that takes an async function 399 and returns a new async function that returns (result, session). 400 """ 401 402 def decorator( 403 target_func: Callable[_P, Coroutine[Any, Any, _TOutput]], 404 ) -> Callable[_P, Coroutine[Any, Any, Tuple[_TOutput, Session]]]: 405 # Validate that the decorated function is async 406 if not inspect.iscoroutinefunction(target_func): 407 raise TypeError( 408 f"@session decorator can only be applied to async functions. " 409 f"Function '{target_func.__name__}' is not async. " 410 f"Add 'async' keyword to your function definition." 411 ) 412 413 @wraps(target_func) 414 async def wrapper( 415 *args: _P.args, **kwargs: _P.kwargs 416 ) -> Tuple[_TOutput, Session]: 417 session_obj = Session( 418 context=context, 419 timeout=timeout, 420 end_on_error=end_on_error, 421 broadcast_callback=broadcast_callback, 422 name=name, 423 prompt_injection=prompt_injection, 424 save_state=save_state, 425 ) 426 427 with session_obj: 428 result = await target_func(*args, **kwargs) 429 return result, session_obj 430 431 return wrapper 432 433 # If used as @session without parentheses 434 if func is not None: 435 return decorator(func) 436 437 # If used as @session(...) 438 return decorator
This decorator automatically creates and manages a Session context for the decorated function, allowing async functions to use Railtracks operations without manually managing the session lifecycle.
Can be used as:
- @session (without parentheses) - uses default settings
- @session() (with empty parentheses) - uses default settings
- @session(name="my_task", timeout=30) (with configuration parameters)
When using this decorator, the function returns a tuple containing:
- The original function's return value
- The Session object used during execution
This allows access to session information (like execution state, logs, etc.) after the function completes, while maintaining the simplicity of decorator usage.
Arguments:
- name (str | None, optional): Optional name for the session. This name will be included in the saved state file if
save_stateis True. - context (Dict[str, Any], optional): A dictionary of global context variables to be used during the execution.
- timeout (float, optional): The maximum number of seconds to wait for a response to your top-level request.
- end_on_error (bool, optional): If True, the execution will stop when an exception is encountered.
- broadcast_callback (Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None, optional): A callback function that will be called with the broadcast messages.
- prompt_injection (bool, optional): If True, the prompt will be automatically injected from context variables.
- save_state (bool, optional): If True, the state of the execution will be saved to a file at the end of the run in the
.railtracks/data/sessions/directory.
Returns:
When used as @session (without parentheses): Returns the decorated function that returns (result, session). When used as @session(...) (with parameters): Returns a decorator function that takes an async function and returns a new async function that returns (result, session).
59async def call( 60 node_: Callable[_P, Node[_TOutput]] | RTFunction[_P, _TOutput], 61 *args: _P.args, 62 **kwargs: _P.kwargs, 63) -> _TOutput: 64 """ 65 Call a node from within a node inside the framework. This will return a coroutine that you can interact with 66 in whatever way using async/await logic. 67 68 Usage: 69 ```python 70 # for sequential operation 71 result = await call(NodeA, "hello world", 42) 72 73 # for parallel operation 74 tasks = [call(NodeA, "hello world", i) for i in range(10)] 75 results = await asyncio.gather(*tasks) 76 ``` 77 78 Args: 79 node: The node type you would like to create. This could be a function decorated with `@function_node`, a function, or a Node instance. 80 *args: The arguments to pass to the node 81 **kwargs: The keyword arguments to pass to the node 82 """ 83 node: Callable[_P, Node[_TOutput]] 84 # this entire section is a bit of a typing nightmare becuase all overloads we provide. 85 if isinstance(node_, FunctionType): 86 node = extract_node_from_function(node_) 87 else: 88 node = node_ 89 # if the context is none then we will need to create a wrapper for the state object to work with. 90 if not is_context_present(): 91 # we have to use lazy import here to prevent a circular import issue. This is a must have unfortunately. 92 from railtracks import Session 93 94 with Session(): 95 result = await _start(node, args=args, kwargs=kwargs) 96 return result 97 98 # if the context is not active then we know this is the top level request 99 if not is_context_active(): 100 result = await _start(node, args=args, kwargs=kwargs) 101 return result 102 103 # if the context is active then we can just run the node 104 result = await _run(node, args=args, kwargs=kwargs) 105 return result
Call a node from within a node inside the framework. This will return a coroutine that you can interact with in whatever way using async/await logic.
Usage:
# for sequential operation
result = await call(NodeA, "hello world", 42)
# for parallel operation
tasks = [call(NodeA, "hello world", i) for i in range(10)]
results = await asyncio.gather(*tasks)
Arguments:
- node: The node type you would like to create. This could be a function decorated with
@function_node, a function, or a Node instance. - *args: The arguments to pass to the node
- **kwargs: The keyword arguments to pass to the node
6async def broadcast(item: str): 7 """ 8 Streams the given message 9 10 This will trigger the broadcast_callback callback you have already provided. 11 12 Args: 13 item (str): The item you want to stream. 14 """ 15 publisher = get_publisher() 16 17 await publisher.publish(Streaming(node_id=get_parent_id(), streamed_object=item))
Streams the given message
This will trigger the broadcast_callback callback you have already provided.
Arguments:
- item (str): The item you want to stream.
21async def call_batch( 22 node: Callable[..., Node[_TOutput]] | Callable[..., _TOutput], 23 *iterables: Iterable[Any], 24 return_exceptions: bool = True, 25): 26 """ 27 Complete a node over multiple iterables, allowing for parallel execution. 28 29 Note the results will be returned in the order of the iterables, not the order of completion. 30 31 If one of the nodes returns an exception, the thrown exception will be included as a response. 32 33 Args: 34 node: The node type to create. 35 *iterables: The iterables to map the node over. 36 return_exceptions: If True, exceptions will be returned as part of the results. 37 If False, exceptions will be raised immediately, and you will lose access to the results. 38 Defaults to true. 39 40 Returns: 41 An iterable of results from the node. 42 43 Usage: 44 ```python 45 results = await batch(NodeA, ["hello world"] * 10) 46 for result in results: 47 handle(result) 48 ``` 49 """ 50 # this is big typing disaster but there is no way around it. Try if if you want to. 51 contracts = [call(node, *args) for args in zip(*iterables)] 52 53 results = await asyncio.gather(*contracts, return_exceptions=return_exceptions) 54 return results
Complete a node over multiple iterables, allowing for parallel execution.
Note the results will be returned in the order of the iterables, not the order of completion.
If one of the nodes returns an exception, the thrown exception will be included as a response.
Arguments:
- node: The node type to create.
- *iterables: The iterables to map the node over.
- return_exceptions: If True, exceptions will be returned as part of the results. If False, exceptions will be raised immediately, and you will lose access to the results. Defaults to true.
Returns:
An iterable of results from the node.
Usage:
results = await batch(NodeA, ["hello world"] * 10) for result in results: handle(result)
19class ExecutionInfo: 20 """ 21 A class that contains the full details of the state of a run at any given point in time. 22 23 The class is designed to be used as a snapshot of state that can be used to display the state of the run, or to 24 create a graphical representation of the system. 25 """ 26 27 def __init__( 28 self, 29 request_forest: RequestForest, 30 node_forest: NodeForest, 31 stamper: StampManager, 32 ): 33 self.request_forest = request_forest 34 self.node_forest = node_forest 35 self.stamper = stamper 36 37 @classmethod 38 def default(cls) -> ExecutionInfo: 39 """Creates a new "empty" instance of the ExecutionInfo class with the default values.""" 40 return cls.create_new() 41 42 @classmethod 43 def create_new( 44 cls, 45 ) -> ExecutionInfo: 46 """ 47 Creates a new empty instance of state variables with the provided executor configuration. 48 49 """ 50 request_heap = RequestForest() 51 node_heap = NodeForest() 52 stamper = StampManager() 53 54 return ExecutionInfo( 55 request_forest=request_heap, 56 node_forest=node_heap, 57 stamper=stamper, 58 ) 59 60 @property 61 def answer(self): 62 """Convenience method to access the answer of the run.""" 63 return self.request_forest.answer 64 65 @property 66 def all_stamps(self) -> List[Stamp]: 67 """Convenience method to access all the stamps of the run.""" 68 return self.stamper.all_stamps 69 70 @property 71 def name(self): 72 """ 73 Gets the name of the graph by pulling the name of the insertion request. It will raise a ValueError if the insertion 74 request is not present or there are multiple insertion requests. 75 """ 76 insertion_requests = self.insertion_requests 77 78 # The name is only defined for the length of 1. 79 # NOTE: Maybe we should send a warning once to user in other cases. 80 if len(insertion_requests) != 1: 81 return None 82 83 i_r = insertion_requests[0] 84 85 return self.node_forest.get_node_type(i_r.sink_id).name() 86 87 @property 88 def insertion_requests(self): 89 """A convenience method to access all the insertion requests of the run.""" 90 return self.request_forest.insertion_request 91 92 def _get_info(self, ids: List[str] | str | None = None) -> ExecutionInfo: 93 """ 94 Gets a subset of the current state based on the provided node ids. It will contain all the children of the provided node ids 95 96 Note: If no ids are provided, the full state is returned. 97 98 Args: 99 ids (List[str] | str | None): A list of node ids to filter the state by. If None, the full state is returned. 100 101 Returns: 102 ExecutionInfo: A new instance of ExecutionInfo containing only the children of the provided ids. 103 104 """ 105 if ids is None: 106 return self 107 else: 108 # firstly lets 109 if isinstance(ids, str): 110 ids = [ids] 111 112 # we need to quickly check to make sure these ids are valid 113 for identifier in ids: 114 if identifier not in self.request_forest: 115 raise ValueError( 116 f"Identifier '{identifier}' not found in the current state." 117 ) 118 119 new_node_forest, new_request_forest = create_sub_state_info( 120 self.node_forest.heap(), 121 self.request_forest.heap(), 122 ids, 123 ) 124 return ExecutionInfo( 125 node_forest=new_node_forest, 126 request_forest=new_request_forest, 127 stamper=self.stamper, 128 ) 129 130 def _to_graph(self) -> Tuple[List[Vertex], List[Edge]]: 131 """ 132 Converts the current state into its graph representation. 133 134 Returns: 135 List[Node]: An iterable of nodes in the graph. 136 List[Edge]: An iterable of edges in the graph. 137 """ 138 return self.node_forest.to_vertices(), self.request_forest.to_edges() 139 140 def graph_serialization(self) -> dict[str, Any]: 141 """ 142 Creates a string (JSON) representation of this info object designed to be used to construct a graph for this 143 info object. 144 145 Some important notes about its structure are outlined below: 146 - The `nodes` key contains a list of all the nodes in the graph, represented as `Vertex` objects. 147 - The `edges` key contains a list of all the edges in the graph, represented as `Edge` objects. 148 - The `stamps` key contains an ease of use list of all the stamps associated with the run, represented as `Stamp` objects. 149 150 - The "nodes" and "requests" key will be outlined with normal graph details like connections and identifiers in addition to a loose details object. 151 - However, both will carry an addition param called "stamp" which is a timestamp style object. 152 - They also will carry a "parent" param which is a recursive structure that allows you to traverse the graph in time. 153 154 155 ``` 156 """ 157 parent_nodes = [x.identifier for x in self.insertion_requests] 158 159 infos = [self._get_info(parent_node) for parent_node in parent_nodes] 160 161 runs = [] 162 163 for info, parent_node_id in zip(infos, parent_nodes): 164 insertion_requests = info.request_forest.insertion_request 165 166 assert len(insertion_requests) == 1 167 parent_request = insertion_requests[0] 168 169 all_parents = parent_request.get_all_parents() 170 171 start_time = all_parents[-1].stamp.time 172 173 assert len([x for x in all_parents if x.status == "Completed"]) <= 1 174 end_time = None 175 for req in all_parents: 176 if req.status in ["Completed", "Failed"]: 177 end_time = req.stamp.time 178 break 179 180 entry = { 181 "name": info.name, 182 "run_id": parent_node_id, 183 "nodes": info.node_forest.to_vertices(), 184 "status": parent_request.status, 185 "edges": info.request_forest.to_edges(), 186 "steps": _get_stamps_from_forests( 187 info.node_forest, info.request_forest 188 ), 189 "start_time": start_time, 190 "end_time": end_time, 191 } 192 runs.append(entry) 193 194 return json.loads( 195 json.dumps( 196 runs, 197 cls=RTJSONEncoder, 198 ) 199 )
A class that contains the full details of the state of a run at any given point in time.
The class is designed to be used as a snapshot of state that can be used to display the state of the run, or to create a graphical representation of the system.
37 @classmethod 38 def default(cls) -> ExecutionInfo: 39 """Creates a new "empty" instance of the ExecutionInfo class with the default values.""" 40 return cls.create_new()
Creates a new "empty" instance of the ExecutionInfo class with the default values.
42 @classmethod 43 def create_new( 44 cls, 45 ) -> ExecutionInfo: 46 """ 47 Creates a new empty instance of state variables with the provided executor configuration. 48 49 """ 50 request_heap = RequestForest() 51 node_heap = NodeForest() 52 stamper = StampManager() 53 54 return ExecutionInfo( 55 request_forest=request_heap, 56 node_forest=node_heap, 57 stamper=stamper, 58 )
Creates a new empty instance of state variables with the provided executor configuration.
60 @property 61 def answer(self): 62 """Convenience method to access the answer of the run.""" 63 return self.request_forest.answer
Convenience method to access the answer of the run.
65 @property 66 def all_stamps(self) -> List[Stamp]: 67 """Convenience method to access all the stamps of the run.""" 68 return self.stamper.all_stamps
Convenience method to access all the stamps of the run.
70 @property 71 def name(self): 72 """ 73 Gets the name of the graph by pulling the name of the insertion request. It will raise a ValueError if the insertion 74 request is not present or there are multiple insertion requests. 75 """ 76 insertion_requests = self.insertion_requests 77 78 # The name is only defined for the length of 1. 79 # NOTE: Maybe we should send a warning once to user in other cases. 80 if len(insertion_requests) != 1: 81 return None 82 83 i_r = insertion_requests[0] 84 85 return self.node_forest.get_node_type(i_r.sink_id).name()
Gets the name of the graph by pulling the name of the insertion request. It will raise a ValueError if the insertion request is not present or there are multiple insertion requests.
87 @property 88 def insertion_requests(self): 89 """A convenience method to access all the insertion requests of the run.""" 90 return self.request_forest.insertion_request
A convenience method to access all the insertion requests of the run.
140 def graph_serialization(self) -> dict[str, Any]: 141 """ 142 Creates a string (JSON) representation of this info object designed to be used to construct a graph for this 143 info object. 144 145 Some important notes about its structure are outlined below: 146 - The `nodes` key contains a list of all the nodes in the graph, represented as `Vertex` objects. 147 - The `edges` key contains a list of all the edges in the graph, represented as `Edge` objects. 148 - The `stamps` key contains an ease of use list of all the stamps associated with the run, represented as `Stamp` objects. 149 150 - The "nodes" and "requests" key will be outlined with normal graph details like connections and identifiers in addition to a loose details object. 151 - However, both will carry an addition param called "stamp" which is a timestamp style object. 152 - They also will carry a "parent" param which is a recursive structure that allows you to traverse the graph in time. 153 154 155 ``` 156 """ 157 parent_nodes = [x.identifier for x in self.insertion_requests] 158 159 infos = [self._get_info(parent_node) for parent_node in parent_nodes] 160 161 runs = [] 162 163 for info, parent_node_id in zip(infos, parent_nodes): 164 insertion_requests = info.request_forest.insertion_request 165 166 assert len(insertion_requests) == 1 167 parent_request = insertion_requests[0] 168 169 all_parents = parent_request.get_all_parents() 170 171 start_time = all_parents[-1].stamp.time 172 173 assert len([x for x in all_parents if x.status == "Completed"]) <= 1 174 end_time = None 175 for req in all_parents: 176 if req.status in ["Completed", "Failed"]: 177 end_time = req.stamp.time 178 break 179 180 entry = { 181 "name": info.name, 182 "run_id": parent_node_id, 183 "nodes": info.node_forest.to_vertices(), 184 "status": parent_request.status, 185 "edges": info.request_forest.to_edges(), 186 "steps": _get_stamps_from_forests( 187 info.node_forest, info.request_forest 188 ), 189 "start_time": start_time, 190 "end_time": end_time, 191 } 192 runs.append(entry) 193 194 return json.loads( 195 json.dumps( 196 runs, 197 cls=RTJSONEncoder, 198 ) 199 )
Creates a string (JSON) representation of this info object designed to be used to construct a graph for this info object.
Some important notes about its structure are outlined below:
- The `nodes` key contains a list of all the nodes in the graph, represented as `Vertex` objects.
- The `edges` key contains a list of all the edges in the graph, represented as `Edge` objects.
- The `stamps` key contains an ease of use list of all the stamps associated with the run, represented as `Stamp` objects.
- The "nodes" and "requests" key will be outlined with normal graph details like connections and identifiers in addition to a loose details object.
- However, both will carry an addition param called "stamp" which is a timestamp style object.
- They also will carry a "parent" param which is a recursive structure that allows you to traverse the graph in time.
```
8class ExecutorConfig: 9 def __init__( 10 self, 11 *, 12 timeout: float | None = None, 13 end_on_error: bool = False, 14 broadcast_callback: ( 15 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 16 ) = None, 17 prompt_injection: bool = True, 18 save_state: bool = True, 19 payload_callback: Callable[[dict[str, Any]], None] | None = None, 20 ): 21 """ 22 ExecutorConfig is special configuration object designed to allow customization of the executor in the RT system. 23 24 Args: 25 timeout (float | None): The maximum number of seconds to wait for a response to your top level request. Pass None (or omit) to disable the timeout entirely. 26 end_on_error (bool): If true, the executor will stop execution when an exception is encountered. 27 broadcast_callback (Callable or Coroutine): A function or coroutine that receives items published with `rt.broadcast`. 28 prompt_injection (bool): If true, prompts can be injected with global context 29 save_state (bool): If true, the state of the executor will be saved to disk. 30 """ 31 self.timeout = timeout 32 self.end_on_error = end_on_error 33 self.subscriber = broadcast_callback 34 self.prompt_injection = prompt_injection 35 # During test runs, disable save_state by default unless RAILTRACKS_ALLOW_PERSISTENCE is set 36 self._user_save_state = save_state 37 38 self.payload_callback = payload_callback 39 40 # this is done because if we try to lock the save_state in init 41 # later when we want to allow a few tests to actually run persistance, they wont be able to do so 42 @property 43 def save_state(self) -> bool: 44 if os.getenv("RAILTRACKS_TEST_MODE") and not os.getenv( 45 "RAILTRACKS_ALLOW_PERSISTENCE" 46 ): 47 return False 48 return self._user_save_state 49 50 def precedence_overwritten( 51 self, 52 *, 53 timeout: float | None = None, 54 end_on_error: bool | None = None, 55 subscriber: ( 56 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 57 ) = None, 58 prompt_injection: bool | None = None, 59 save_state: bool | None = None, 60 payload_callback: Callable[[dict[str, Any]], None] | None = None, 61 ): 62 """ 63 If any of the parameters are provided (not None), it will create a new update the current instance with the new values and return a deep copied reference to it. 64 """ 65 return ExecutorConfig( 66 timeout=timeout, 67 end_on_error=end_on_error 68 if end_on_error is not None 69 else self.end_on_error, 70 broadcast_callback=subscriber 71 if subscriber is not None 72 else self.subscriber, 73 prompt_injection=prompt_injection 74 if prompt_injection is not None 75 else self.prompt_injection, 76 save_state=save_state if save_state is not None else self.save_state, 77 payload_callback=payload_callback 78 if payload_callback is not None 79 else self.payload_callback, 80 ) 81 82 def __repr__(self): 83 return ( 84 f"ExecutorConfig(timeout={self.timeout}, end_on_error={self.end_on_error}, " 85 f"prompt_injection={self.prompt_injection}, " 86 f"save_state={self.save_state}, payload_callback={self.payload_callback})" 87 )
9 def __init__( 10 self, 11 *, 12 timeout: float | None = None, 13 end_on_error: bool = False, 14 broadcast_callback: ( 15 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 16 ) = None, 17 prompt_injection: bool = True, 18 save_state: bool = True, 19 payload_callback: Callable[[dict[str, Any]], None] | None = None, 20 ): 21 """ 22 ExecutorConfig is special configuration object designed to allow customization of the executor in the RT system. 23 24 Args: 25 timeout (float | None): The maximum number of seconds to wait for a response to your top level request. Pass None (or omit) to disable the timeout entirely. 26 end_on_error (bool): If true, the executor will stop execution when an exception is encountered. 27 broadcast_callback (Callable or Coroutine): A function or coroutine that receives items published with `rt.broadcast`. 28 prompt_injection (bool): If true, prompts can be injected with global context 29 save_state (bool): If true, the state of the executor will be saved to disk. 30 """ 31 self.timeout = timeout 32 self.end_on_error = end_on_error 33 self.subscriber = broadcast_callback 34 self.prompt_injection = prompt_injection 35 # During test runs, disable save_state by default unless RAILTRACKS_ALLOW_PERSISTENCE is set 36 self._user_save_state = save_state 37 38 self.payload_callback = payload_callback
ExecutorConfig is special configuration object designed to allow customization of the executor in the RT system.
Arguments:
- timeout (float | None): The maximum number of seconds to wait for a response to your top level request. Pass None (or omit) to disable the timeout entirely.
- end_on_error (bool): If true, the executor will stop execution when an exception is encountered.
- broadcast_callback (Callable or Coroutine): A function or coroutine that receives items published with
rt.broadcast. - prompt_injection (bool): If true, prompts can be injected with global context
- save_state (bool): If true, the state of the executor will be saved to disk.
50 def precedence_overwritten( 51 self, 52 *, 53 timeout: float | None = None, 54 end_on_error: bool | None = None, 55 subscriber: ( 56 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 57 ) = None, 58 prompt_injection: bool | None = None, 59 save_state: bool | None = None, 60 payload_callback: Callable[[dict[str, Any]], None] | None = None, 61 ): 62 """ 63 If any of the parameters are provided (not None), it will create a new update the current instance with the new values and return a deep copied reference to it. 64 """ 65 return ExecutorConfig( 66 timeout=timeout, 67 end_on_error=end_on_error 68 if end_on_error is not None 69 else self.end_on_error, 70 broadcast_callback=subscriber 71 if subscriber is not None 72 else self.subscriber, 73 prompt_injection=prompt_injection 74 if prompt_injection is not None 75 else self.prompt_injection, 76 save_state=save_state if save_state is not None else self.save_state, 77 payload_callback=payload_callback 78 if payload_callback is not None 79 else self.payload_callback, 80 )
If any of the parameters are provided (not None), it will create a new update the current instance with the new values and return a deep copied reference to it.
365def set_config( 366 *, 367 timeout: float | None = None, 368 end_on_error: bool | None = None, 369 broadcast_callback: ( 370 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 371 ) = None, 372 prompt_injection: bool | None = None, 373 save_state: bool | None = None, 374) -> None: 375 """ 376 Sets the global configuration for the executor. This will be propagated to all new runners created after this call. 377 378 - If you call this function after the runner has been created, it will not affect the current runner. 379 - This function will only overwrite the values that are provided, leaving the rest unchanged. 380 381 382 """ 383 384 if is_context_active(): 385 warnings.warn( 386 "The executor config is being set after the runner has been created, this is not recommended" 387 ) 388 389 config = global_executor_config.get() 390 391 new_config = config.precedence_overwritten( 392 timeout=timeout, 393 end_on_error=end_on_error, 394 subscriber=broadcast_callback, 395 prompt_injection=prompt_injection, 396 save_state=save_state, 397 ) 398 399 global_executor_config.set(new_config)
Sets the global configuration for the executor. This will be propagated to all new runners created after this call.
- If you call this function after the runner has been created, it will not affect the current runner.
- This function will only overwrite the values that are provided, leaving the rest unchanged.
173def function_node( 174 func: Callable[_P, Coroutine[None, None, _TOutput] | _TOutput] 175 | List[Callable[_P, Coroutine[None, None, _TOutput] | _TOutput]], 176 /, 177 *, 178 name: str | None = None, 179 manifest: ToolManifest | None = None, 180) -> ( 181 Callable[_P, Coroutine[None, None, _TOutput] | _TOutput] 182 | List[Callable[_P, Coroutine[None, None, _TOutput] | _TOutput]] 183 | None 184): 185 """ 186 Creates a new Node type from a function that can be used in `rt.call()`. 187 188 By default, it will parse the function's docstring and turn them into tool details and parameters. However, if 189 you provide custom ToolManifest it will override that logic. 190 191 WARNING: If you overriding tool parameters. It is on you to make sure they will work with your function. 192 193 NOTE: If you have already converted this function to a node this function will do nothing 194 195 Args: 196 func (Callable): The function to convert into a Node. 197 name (str, optional): Human-readable name for the node/tool. 198 manifest (ToolManifest, optional): The details you would like to override the tool with. 199 """ 200 201 # handle the case where a list of functions is provided 202 if isinstance(func, list): 203 return [function_node(f, name=name, manifest=manifest) for f in func] 204 205 # check if the function has already been converted to a node 206 if hasattr(func, "node_type"): 207 warnings.warn( 208 "The provided function has already been converted to a node.", 209 UserWarning, 210 ) 211 return func 212 213 # validate_function_parameters is separated out to allow for easier testing. 214 validate_function_parameters(func, manifest) 215 216 # assign the correct node class based on whether the function is async or sync 217 if asyncio.iscoroutinefunction(func): 218 if inspect.ismethod(func): 219 func = _function_preserving_metadata(func) 220 node_class = AsyncDynamicFunctionNode 221 elif inspect.isfunction(func): 222 node_class = SyncDynamicFunctionNode 223 elif inspect.ismethod(func): 224 # Bound methods can't hold attributes, so wrap in a plain function first. 225 # functools.wraps preserves __name__, __doc__, and __wrapped__ for the NodeBuilder. 226 func = _function_preserving_metadata(func) 227 node_class = SyncDynamicFunctionNode 228 elif inspect.isbuiltin(func): 229 # builtin functions are written in C and do not have space for the addition of metadata like our node type. 230 # so instead we wrap them in a function that allows for the addition of the node type. 231 # this logic preserved details like the function name, docstring, and signature, but allows us to add the node type. 232 func = _function_preserving_metadata(func) 233 node_class = SyncDynamicFunctionNode 234 else: 235 raise NodeCreationError( 236 message=f"The provided function is not a valid coroutine or sync function it is {type(func)}.", 237 notes=[ 238 "You must provide a valid function or coroutine function to make a node.", 239 ], 240 ) 241 242 # build the node using the NodeBuilder 243 builder = NodeBuilder( 244 node_class, 245 name=name if name is not None else f"{func.__name__}", 246 ) 247 248 builder.setup_function_node( 249 func, 250 tool_details=manifest.description if manifest is not None else None, 251 tool_params=manifest.parameters if manifest is not None else None, 252 ) 253 254 completed_node_type = builder.build() 255 256 # there is some pretty scary logic here. 257 if issubclass(completed_node_type, AsyncDynamicFunctionNode): 258 setattr(func, "node_type", completed_node_type) 259 return func 260 elif issubclass(completed_node_type, SyncDynamicFunctionNode): 261 setattr(func, "node_type", completed_node_type) 262 return func 263 else: 264 raise NodeCreationError( 265 message="The provided function did not create a valid node type.", 266 notes=[ 267 "Please make a github issue with the details of what went wrong.", 268 ], 269 )
Creates a new Node type from a function that can be used in rt.call().
By default, it will parse the function's docstring and turn them into tool details and parameters. However, if you provide custom ToolManifest it will override that logic.
WARNING: If you overriding tool parameters. It is on you to make sure they will work with your function.
NOTE: If you have already converted this function to a node this function will do nothing
Arguments:
- func (Callable): The function to convert into a Node.
- name (str, optional): Human-readable name for the node/tool.
- manifest (ToolManifest, optional): The details you would like to override the tool with.
305def agent_node( 306 name: str | None = None, 307 *, 308 tool_nodes: Iterable[Type[Node] | Callable | RTFunction] | None = None, 309 output_schema: Type[_TBaseModel] | None = None, 310 llm: ModelBase[_TStream] | None = None, 311 system_message: SystemMessage | str | None = None, 312 manifest: ToolManifest | None = None, 313 guardrails: Guard | None = None, 314): 315 """ 316 Dynamically creates an agent based on the provided parameters. 317 318 Args: 319 name (str | None): The name of the agent. If none the default will be used. 320 tool_nodes (set[Type[Node] | Callable | RTFunction] | None): If your agent is a LLM with access to tools, what does it have access to? 321 output_schema (Type[_TBaseModel] | None): If your agent should return a structured output, what is the output_schema? 322 llm (ModelBase): The LLM model to use. If None it will need to be passed in at instance time. 323 Deferring the model this way is going away: `llm` becomes a required keyword in 324 railtracks 1.5.0. 325 system_message (SystemMessage | str | None): System message for the agent. 326 manifest (ToolManifest | None): If you want to use this as a tool in other agents you can pass in a ToolManifest. 327 guardrails (Guard | None): Guardrail config. When provided, the agent runs input/output guardrails. 328 Removed in railtracks 1.5.0, where guards attach as model middleware instead. 329 """ 330 if guardrails is not None: 331 warn_pending_change( 332 "The `guardrails=` argument of agent_node", 333 change="is removed", 334 detail=( 335 "Guards attach as model middleware in 1.5.0. Writing a guard is " 336 "unchanged; only the way it is attached to the agent changes." 337 ), 338 ) 339 340 if llm is None: 341 warn_pending_change( 342 "Creating an agent without an `llm`", 343 change="stops working", 344 detail=( 345 "Pass `llm=` when building the agent; it becomes a required keyword. " 346 "If you rely on supplying the model at instance time, 1.5.0 replaces " 347 "that with a zero-argument model factory." 348 ), 349 ) 350 351 unpacked_tool_nodes = _unpack_tool_nodes(tool_nodes) 352 353 # See issue (___) this logic should be migrated soon. 354 if manifest is not None: 355 tool_details = manifest.description 356 tool_params = manifest.parameters 357 else: 358 tool_details = None 359 tool_params = None 360 361 return _build_dynamic_agent( 362 unpacked_tool_nodes=unpacked_tool_nodes, 363 output_schema=output_schema, 364 name=name, 365 llm=llm, 366 system_message=system_message, 367 tool_details=tool_details, 368 tool_params=tool_params, 369 guardrails=guardrails, 370 )
Dynamically creates an agent based on the provided parameters.
Arguments:
- name (str | None): The name of the agent. If none the default will be used.
- tool_nodes (set[Type[Node] | Callable | RTFunction] | None): If your agent is a LLM with access to tools, what does it have access to?
- output_schema (Type[_TBaseModel] | None): If your agent should return a structured output, what is the output_schema?
- llm (ModelBase): The LLM model to use. If None it will need to be passed in at instance time.
Deferring the model this way is going away:
llmbecomes a required keyword in railtracks 1.5.0. - system_message (SystemMessage | str | None): System message for the agent.
- manifest (ToolManifest | None): If you want to use this as a tool in other agents you can pass in a ToolManifest.
- guardrails (Guard | None): Guardrail config. When provided, the agent runs input/output guardrails. Removed in railtracks 1.5.0, where guards attach as model middleware instead.
19class MCPStdioParams(StdioServerParameters): 20 """ 21 Configuration parameters for STDIO-based MCP server connections. 22 23 Extends the standard StdioServerParameters with a timeout field. 24 25 Attributes: 26 timeout: Maximum time to wait for operations (default: 30 seconds) 27 """ 28 29 timeout: timedelta = timedelta(seconds=30) 30 31 def as_stdio_params(self) -> StdioServerParameters: 32 """ 33 Convert to standard StdioServerParameters, excluding the timeout field. 34 35 Returns: 36 StdioServerParameters without the timeout attribute 37 """ 38 stdio_kwargs = self.dict(exclude={"timeout"}) 39 return StdioServerParameters(**stdio_kwargs)
Configuration parameters for STDIO-based MCP server connections.
Extends the standard StdioServerParameters with a timeout field.
Attributes:
- timeout: Maximum time to wait for operations (default: 30 seconds)
31 def as_stdio_params(self) -> StdioServerParameters: 32 """ 33 Convert to standard StdioServerParameters, excluding the timeout field. 34 35 Returns: 36 StdioServerParameters without the timeout attribute 37 """ 38 stdio_kwargs = self.dict(exclude={"timeout"}) 39 return StdioServerParameters(**stdio_kwargs)
Convert to standard StdioServerParameters, excluding the timeout field.
Returns:
StdioServerParameters without the timeout attribute
42class MCPHttpParams(BaseModel): 43 """ 44 Configuration parameters for HTTP-based MCP server connections. 45 46 Supports both SSE (Server-Sent Events) and streamable HTTP transports. 47 The transport type is automatically determined based on the URL. 48 49 Attributes: 50 url: The MCP server URL (use /sse suffix for SSE transport) 51 headers: Optional HTTP headers for authentication 52 timeout: Connection timeout (default: 30 seconds) 53 sse_read_timeout: SSE read timeout (default: 5 minutes) 54 terminate_on_close: Whether to terminate connection on close (default: True) 55 """ 56 57 url: str 58 headers: dict[str, Any] | None = None 59 timeout: timedelta = timedelta(seconds=30) 60 sse_read_timeout: timedelta = timedelta(seconds=60 * 5) 61 terminate_on_close: bool = True
Configuration parameters for HTTP-based MCP server connections.
Supports both SSE (Server-Sent Events) and streamable HTTP transports. The transport type is automatically determined based on the URL.
Attributes:
- url: The MCP server URL (use /sse suffix for SSE transport)
- headers: Optional HTTP headers for authentication
- timeout: Connection timeout (default: 30 seconds)
- sse_read_timeout: SSE read timeout (default: 5 minutes)
- terminate_on_close: Whether to terminate connection on close (default: True)
8def connect_mcp( 9 config: MCPStdioParams | MCPHttpParams, 10 client_session: ClientSession | None = None, 11 setup_timeout: float = 30, 12) -> MCPServer: 13 """ 14 Connect to an MCP server and return a server instance with available tools. 15 16 This is the primary entry point for using MCP servers in Railtracks. 17 The server will connect in the background, discover available tools, 18 and convert them to Railtracks Node classes. 19 20 The connection remains active until explicitly closed or the context exits. 21 22 Usage Examples: 23 # STDIO connection (local MCP server) 24 config = rt.MCPStdioParams( 25 command="uvx", 26 args=["mcp-server-time"] 27 ) 28 server = rt.connect_mcp(config) 29 30 # HTTP connection (remote MCP server) 31 config = rt.MCPHttpParams( 32 url="https://mcp.example.com/sse", 33 headers={"Authorization": "Bearer token"} 34 ) 35 server = rt.connect_mcp(config) 36 37 # Context manager (recommended) 38 with rt.connect_mcp(config) as server: 39 tools = server.tools 40 # Use tools... 41 # Automatically closed 42 43 # Access tools 44 for tool in server.tools: 45 print(f"Tool: {tool.name()}") 46 print(f"Description: {tool.tool_info().description}") 47 48 Args: 49 config: Server configuration: 50 - MCPStdioParams: For local servers via stdin/stdout 51 - MCPHttpParams: For remote servers via HTTP/SSE 52 client_session: Optional pre-configured ClientSession for advanced use cases. 53 If not provided, a new session will be created automatically. 54 setup_timeout: Maximum seconds to wait for connection (default: 30). 55 Increase for slow servers or complex authentication flows. 56 57 Returns: 58 MCPServer: Connected server instance with: 59 - tools: List of Node classes representing MCP tools 60 - close(): Method to explicitly close the connection 61 - Context manager support for automatic cleanup 62 63 Raises: 64 FileNotFoundError: If STDIO command not found. Verify the command is: 65 - Installed and in your PATH 66 - Spelled correctly (check for typos) 67 - Executable (check permissions on Unix) 68 ConnectionError: If connection to server fails. Check: 69 - Server URL is correct and accessible 70 - Network connectivity and firewall settings 71 - Authentication credentials are valid 72 - Server is running and accepting connections 73 TimeoutError: If connection exceeds setup_timeout. Try: 74 - Increasing setup_timeout parameter 75 - Checking server performance/load 76 - Verifying server is responding 77 RuntimeError: For other setup failures (e.g., protocol errors, config issues) 78 79 Note: 80 - The connection runs in a background thread for sync/async bridging 81 - Tools are cached after first retrieval for performance 82 - Always close() the server when done or use context manager 83 - Jupyter compatibility patches are applied automatically 84 """ 85 # Apply Jupyter compatibility patches if needed 86 apply_patches() 87 88 return MCPServer( 89 config=config, client_session=client_session, setup_timeout=setup_timeout 90 )
Connect to an MCP server and return a server instance with available tools.
This is the primary entry point for using MCP servers in Railtracks. The server will connect in the background, discover available tools, and convert them to Railtracks Node classes.
The connection remains active until explicitly closed or the context exits.
Usage Examples:
STDIO connection (local MCP server)
config = rt.MCPStdioParams( command="uvx", args=["mcp-server-time"] ) server = rt.connect_mcp(config)
HTTP connection (remote MCP server)
config = rt.MCPHttpParams( url="https://mcp.example.com/sse", headers={"Authorization": "Bearer token"} ) server = rt.connect_mcp(config)
Context manager (recommended)
with rt.connect_mcp(config) as server: tools = server.tools # Use tools...
Automatically closed
Access tools
for tool in server.tools: print(f"Tool: {tool.name()}") print(f"Description: {tool.tool_info().description}")
Arguments:
- config: Server configuration:
- MCPStdioParams: For local servers via stdin/stdout
- MCPHttpParams: For remote servers via HTTP/SSE
- client_session: Optional pre-configured ClientSession for advanced use cases. If not provided, a new session will be created automatically.
- setup_timeout: Maximum seconds to wait for connection (default: 30). Increase for slow servers or complex authentication flows.
Returns:
MCPServer: Connected server instance with: - tools: List of Node classes representing MCP tools - close(): Method to explicitly close the connection - Context manager support for automatic cleanup
Raises:
- FileNotFoundError: If STDIO command not found. Verify the command is:
- Installed and in your PATH
- Spelled correctly (check for typos)
- Executable (check permissions on Unix)
- ConnectionError: If connection to server fails. Check:
- Server URL is correct and accessible
- Network connectivity and firewall settings
- Authentication credentials are valid
- Server is running and accepting connections
- TimeoutError: If connection exceeds setup_timeout. Try:
- Increasing setup_timeout parameter
- Checking server performance/load
- Verifying server is responding
- RuntimeError: For other setup failures (e.g., protocol errors, config issues)
Note:
- The connection runs in a background thread for sync/async bridging
- Tools are cached after first retrieval for performance
- Always close() the server when done or use context manager
- Jupyter compatibility patches are applied automatically
86def create_mcp_server( 87 nodes: List[Node | RTFunction], 88 server_name: str = "MCP Server", 89 fastmcp: FastMCP | None = None, 90): 91 """ 92 Create a FastMCP server that can be used to run nodes as MCP tools. 93 94 Args: 95 nodes: List of Node classes to be registered as tools with the MCP server. 96 server_name: Name of the MCP server instance. 97 fastmcp: Optional FastMCP instance to use instead of creating a new one. 98 99 Returns: 100 A FastMCP server instance. 101 """ 102 if fastmcp is not None: 103 if not isinstance(fastmcp, FastMCP): 104 raise ValueError("Provided fastmcp must be an instance of FastMCP.") 105 mcp = fastmcp 106 else: 107 mcp = FastMCP(server_name) 108 109 for node in [n if not hasattr(n, "node_type") else n.node_type for n in nodes]: 110 node_info = node.tool_info() 111 func = _create_tool_function(node, node_info) 112 113 mcp._tool_manager._tools[node_info.name] = MCPTool( 114 fn=func, 115 name=node_info.name, 116 description=node_info.detail, 117 parameters=( 118 _parameters_to_json_schema(node_info.parameters) 119 if node_info.parameters is not None 120 else {} 121 ), 122 fn_metadata=func_metadata(func, []), 123 is_async=True, 124 context_kwarg=None, 125 annotations=None, 126 ) # Register the node as a tool 127 128 return mcp
Create a FastMCP server that can be used to run nodes as MCP tools.
Arguments:
- nodes: List of Node classes to be registered as tools with the MCP server.
- server_name: Name of the MCP server instance.
- fastmcp: Optional FastMCP instance to use instead of creating a new one.
Returns:
A FastMCP server instance.
7class ToolManifest: 8 """ 9 Creates a manifest for a tool, which includes its description and parameters. 10 11 Args: 12 description (str): A description of the tool. 13 parameters (Iterable[Parameter] | None): An iterable of parameters for the tool. If None, there are no paramerters. 14 """ 15 16 def __init__( 17 self, 18 description: str, 19 parameters: Iterable[Parameter] | None = None, 20 ): 21 self.description = description 22 self.parameters: List[Parameter] = ( 23 list(parameters) if parameters is not None else [] 24 )
Creates a manifest for a tool, which includes its description and parameters.
Arguments:
- description (str): A description of the tool.
- parameters (Iterable[Parameter] | None): An iterable of parameters for the tool. If None, there are no paramerters.
427def session_id() -> str | None: 428 """ 429 Gets the current session ID if it exists, otherwise returns None. 430 """ 431 try: 432 return get_session_id() 433 except ContextError: 434 return None
Gets the current session ID if it exists, otherwise returns None.
25class Flow(Generic[_P, _TOutput]): 26 """A reusable, configured entry point for running an agent graph. 27 28 Binds an entry-point node to a fixed set of runtime options so the same 29 configuration can be invoked repeatedly. Each invocation is fully isolated. 30 31 Typical usage:: 32 33 flow = Flow("my-agent", entry_point=my_node, context={"user": "alice"}) 34 result = await flow.ainvoke(query) # async (preferred) 35 result = flow.invoke(query) # sync 36 37 Args: 38 name: Unique human-readable name used in logging and state filenames. 39 entry_point: The node (or decorated function) that starts the graph. 40 context: Key/value pairs available to every node via ``rt.context``. 41 Deep-copied at invocation time so mutations never affect later runs. 42 timeout: Maximum seconds to wait for the run. ``None`` means no limit. 43 end_on_error: When ``True``, the first unhandled exception aborts the run. 44 broadcast_callback: Called with each string emitted by ``rt.broadcast()``. 45 May be sync or async. 46 prompt_injection: When ``True``, prompt text is injected from context 47 variables before the run starts. 48 save_state: When ``True``, session state is persisted to 49 ``.railtracks/data/sessions/`` after the run. 50 payload_callback: Called with the final result payload on success. 51 """ 52 53 def __init__( 54 self, 55 name: str, 56 entry_point: ( 57 Callable[_P, Node[_TOutput]] 58 | RTSyncFunction[_P, _TOutput] 59 | RTAsyncFunction[_P, _TOutput] 60 ), 61 *, 62 context: dict[str, Any] | None = None, 63 timeout: float | None = None, 64 end_on_error: bool | None = None, 65 broadcast_callback: ( 66 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 67 ) = None, 68 prompt_injection: bool | None = None, 69 save_state: bool | None = None, 70 payload_callback: Callable[[dict[str, Any]], Any] | None = None, 71 ) -> None: 72 self.entry_point: Callable[_P, Node[_TOutput]] 73 74 if hasattr(entry_point, "node_type"): 75 self.entry_point = entry_point.node_type 76 else: 77 self.entry_point = entry_point 78 79 self.name = name 80 self._context: dict[str, Any] = context or {} 81 self._timeout = timeout 82 self._end_on_error = end_on_error 83 self._broadcast_callback = broadcast_callback 84 self._prompt_injection = prompt_injection 85 self._save_state = save_state 86 self._payload_callback = payload_callback 87 88 def update_context(self, context: dict[str, Any]) -> Flow[_P, _TOutput]: 89 """Return a new Flow with additional context values merged in. 90 91 The original flow is not modified. Values in ``context`` override 92 any existing keys; keys not present in ``context`` are preserved. 93 94 Args: 95 context: Entries to add or override in the flow's context. 96 97 Returns: 98 A new :class:`Flow` instance with the merged context. 99 """ 100 new_obj = deepcopy(self) 101 new_obj._context.update(context) 102 return new_obj 103 104 async def ainvoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput: 105 """Run the flow asynchronously and return the entry-point result. 106 107 Args: 108 *args: Positional arguments forwarded to the entry-point node. 109 **kwargs: Keyword arguments forwarded to the entry-point node. 110 111 Returns: 112 The value returned by the entry-point node. 113 """ 114 with Session( 115 context=deepcopy(self._context), 116 flow_name=self.name, 117 flow_id=self.equality_hash(), 118 name=None, 119 timeout=self._timeout, 120 end_on_error=self._end_on_error, 121 broadcast_callback=self._broadcast_callback, 122 prompt_injection=self._prompt_injection, 123 save_state=self._save_state, 124 payload_callback=self._payload_callback, 125 ): 126 result = await call(self.entry_point, *args, **kwargs) 127 128 return result 129 130 def invoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput: 131 """Run the flow synchronously and return the entry-point result. 132 133 Prefer ``await flow.ainvoke()`` in async contexts. 134 135 Args: 136 *args: Positional arguments forwarded to the entry-point node. 137 **kwargs: Keyword arguments forwarded to the entry-point node. 138 139 Returns: 140 The value returned by the entry-point node. 141 142 Note: 143 When no event loop is running, delegates to ``asyncio.run()``. 144 When a loop is already running (e.g. Jupyter, FastAPI), submits 145 the coroutine to a ``ThreadPoolExecutor`` worker thread. 146 """ 147 try: 148 asyncio.get_running_loop() 149 except RuntimeError: 150 return asyncio.run(self.ainvoke(*args, **kwargs)) 151 152 ctx = contextvars.copy_context() 153 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: 154 future = pool.submit(ctx.run, asyncio.run, self.ainvoke(*args, **kwargs)) 155 return future.result() 156 157 def equality_hash(self) -> str: 158 """Return a stable hash that identifies this flow's configuration. 159 160 Two flows with the same name produce the same hash regardless of 161 other parameters (timeout, context, etc.). 162 """ 163 config_string = json.dumps(self._get_hash_content(), sort_keys=True) 164 return hashlib.sha256(config_string.encode()).hexdigest() 165 166 def _get_hash_content(self) -> dict: 167 return { 168 "name": self.name, 169 }
A reusable, configured entry point for running an agent graph.
Binds an entry-point node to a fixed set of runtime options so the same configuration can be invoked repeatedly. Each invocation is fully isolated.
Typical usage::
flow = Flow("my-agent", entry_point=my_node, context={"user": "alice"})
result = await flow.ainvoke(query) # async (preferred)
result = flow.invoke(query) # sync
Arguments:
- name: Unique human-readable name used in logging and state filenames.
- entry_point: The node (or decorated function) that starts the graph.
- context: Key/value pairs available to every node via
rt.context. Deep-copied at invocation time so mutations never affect later runs. - timeout: Maximum seconds to wait for the run.
Nonemeans no limit. - end_on_error: When
True, the first unhandled exception aborts the run. - broadcast_callback: Called with each string emitted by
rt.broadcast(). May be sync or async. - prompt_injection: When
True, prompt text is injected from context variables before the run starts. - save_state: When
True, session state is persisted to.railtracks/data/sessions/after the run. - payload_callback: Called with the final result payload on success.
53 def __init__( 54 self, 55 name: str, 56 entry_point: ( 57 Callable[_P, Node[_TOutput]] 58 | RTSyncFunction[_P, _TOutput] 59 | RTAsyncFunction[_P, _TOutput] 60 ), 61 *, 62 context: dict[str, Any] | None = None, 63 timeout: float | None = None, 64 end_on_error: bool | None = None, 65 broadcast_callback: ( 66 Callable[[str], None] | Callable[[str], Coroutine[None, None, None]] | None 67 ) = None, 68 prompt_injection: bool | None = None, 69 save_state: bool | None = None, 70 payload_callback: Callable[[dict[str, Any]], Any] | None = None, 71 ) -> None: 72 self.entry_point: Callable[_P, Node[_TOutput]] 73 74 if hasattr(entry_point, "node_type"): 75 self.entry_point = entry_point.node_type 76 else: 77 self.entry_point = entry_point 78 79 self.name = name 80 self._context: dict[str, Any] = context or {} 81 self._timeout = timeout 82 self._end_on_error = end_on_error 83 self._broadcast_callback = broadcast_callback 84 self._prompt_injection = prompt_injection 85 self._save_state = save_state 86 self._payload_callback = payload_callback
88 def update_context(self, context: dict[str, Any]) -> Flow[_P, _TOutput]: 89 """Return a new Flow with additional context values merged in. 90 91 The original flow is not modified. Values in ``context`` override 92 any existing keys; keys not present in ``context`` are preserved. 93 94 Args: 95 context: Entries to add or override in the flow's context. 96 97 Returns: 98 A new :class:`Flow` instance with the merged context. 99 """ 100 new_obj = deepcopy(self) 101 new_obj._context.update(context) 102 return new_obj
Return a new Flow with additional context values merged in.
The original flow is not modified. Values in context override
any existing keys; keys not present in context are preserved.
Arguments:
- context: Entries to add or override in the flow's context.
Returns:
A new
Flowinstance with the merged context.
104 async def ainvoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput: 105 """Run the flow asynchronously and return the entry-point result. 106 107 Args: 108 *args: Positional arguments forwarded to the entry-point node. 109 **kwargs: Keyword arguments forwarded to the entry-point node. 110 111 Returns: 112 The value returned by the entry-point node. 113 """ 114 with Session( 115 context=deepcopy(self._context), 116 flow_name=self.name, 117 flow_id=self.equality_hash(), 118 name=None, 119 timeout=self._timeout, 120 end_on_error=self._end_on_error, 121 broadcast_callback=self._broadcast_callback, 122 prompt_injection=self._prompt_injection, 123 save_state=self._save_state, 124 payload_callback=self._payload_callback, 125 ): 126 result = await call(self.entry_point, *args, **kwargs) 127 128 return result
Run the flow asynchronously and return the entry-point result.
Arguments:
- *args: Positional arguments forwarded to the entry-point node.
- **kwargs: Keyword arguments forwarded to the entry-point node.
Returns:
The value returned by the entry-point node.
130 def invoke(self, *args: _P.args, **kwargs: _P.kwargs) -> _TOutput: 131 """Run the flow synchronously and return the entry-point result. 132 133 Prefer ``await flow.ainvoke()`` in async contexts. 134 135 Args: 136 *args: Positional arguments forwarded to the entry-point node. 137 **kwargs: Keyword arguments forwarded to the entry-point node. 138 139 Returns: 140 The value returned by the entry-point node. 141 142 Note: 143 When no event loop is running, delegates to ``asyncio.run()``. 144 When a loop is already running (e.g. Jupyter, FastAPI), submits 145 the coroutine to a ``ThreadPoolExecutor`` worker thread. 146 """ 147 try: 148 asyncio.get_running_loop() 149 except RuntimeError: 150 return asyncio.run(self.ainvoke(*args, **kwargs)) 151 152 ctx = contextvars.copy_context() 153 with concurrent.futures.ThreadPoolExecutor(max_workers=1) as pool: 154 future = pool.submit(ctx.run, asyncio.run, self.ainvoke(*args, **kwargs)) 155 return future.result()
Run the flow synchronously and return the entry-point result.
Prefer await flow.ainvoke() in async contexts.
Arguments:
- *args: Positional arguments forwarded to the entry-point node.
- **kwargs: Keyword arguments forwarded to the entry-point node.
Returns:
The value returned by the entry-point node.
Note:
When no event loop is running, delegates to
asyncio.run(). When a loop is already running (e.g. Jupyter, FastAPI), submits the coroutine to aThreadPoolExecutorworker thread.
157 def equality_hash(self) -> str: 158 """Return a stable hash that identifies this flow's configuration. 159 160 Two flows with the same name produce the same hash regardless of 161 other parameters (timeout, context, etc.). 162 """ 163 config_string = json.dumps(self._get_hash_content(), sort_keys=True) 164 return hashlib.sha256(config_string.encode()).hexdigest()
Return a stable hash that identifies this flow's configuration.
Two flows with the same name produce the same hash regardless of other parameters (timeout, context, etc.).
324def enable_logging( 325 level: AllowableLogLevels = "INFO", 326 log_file: str | os.PathLike | None = None, 327 *, 328 name_style: LoggerNameDisplay = "short", 329) -> None: 330 """ 331 Opt-in helper to enable Railtracks logging. Call this explicitly from your 332 application entry point (CLI, main.py, server startup); the library never 333 calls it automatically. 334 335 Uses the given level and log_file; when None, reads RT_LOG_LEVEL and 336 RT_LOG_FILE from the environment. Sets up console output (and optional file) 337 with a ThreadAwareFilter for per-thread level control. 338 339 Args: 340 level: Logging level (default "INFO"). Overridden by RT_LOG_LEVEL when None. 341 log_file: Optional path for a log file. Overridden by RT_LOG_FILE when None. 342 name_style: Console column for logger name: ``full`` (dotted name) or 343 ``short`` (``RT.<Label>``: last segment with leading non-letters stripped, 344 then capitalized). Default ``short``. 345 """ 346 initialize_module_logging(level=level, log_file=log_file, name_style=name_style)
Opt-in helper to enable Railtracks logging. Call this explicitly from your application entry point (CLI, main.py, server startup); the library never calls it automatically.
Uses the given level and log_file; when None, reads RT_LOG_LEVEL and RT_LOG_FILE from the environment. Sets up console output (and optional file) with a ThreadAwareFilter for per-thread level control.
Arguments:
- level: Logging level (default "INFO"). Overridden by RT_LOG_LEVEL when None.
- log_file: Optional path for a log file. Overridden by RT_LOG_FILE when None.
- name_style: Console column for logger name:
full(dotted name) orshort(RT.<Label>: last segment with leading non-letters stripped, then capitalized). Defaultshort.
10def escape_braces(text: str) -> str: 11 """ 12 Escape the braces in `text` so that prompt injection treats it as data. 13 14 Apply this to untrusted or arbitrary strings before embedding them in a message that 15 will have context values injected into it. Injecting the returned string yields 16 `text` back unchanged. 17 18 Args: 19 text: The string to escape. 20 21 Returns: 22 `text` with every `{` and `}` doubled. 23 """ 24 return text.replace("{", "{{").replace("}", "}}")
Escape the braces in text so that prompt injection treats it as data.
Apply this to untrusted or arbitrary strings before embedding them in a message that
will have context values injected into it. Injecting the returned string yields
text back unchanged.
Arguments:
- text: The string to escape.
Returns:
textwith every{and}doubled.