-
Notifications
You must be signed in to change notification settings - Fork 2.7k
POC for state inpsection tools #10936
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
Closed
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| """ | ||
| POC: Agent State Access - skills-as-state pattern | ||
|
|
||
| The agent has two skills stored as strings in state: | ||
| - summarize_skill: a prompt telling the agent how to summarise text | ||
| - translate_skill: a prompt telling the agent how to translate text | ||
|
|
||
| Rather than loading both into the system prompt upfront, the agent discovers | ||
| available skills via ls_state, loads the one it needs via read_state, and writes | ||
| its final output to a `final_answer` key via write_state. | ||
| """ | ||
|
|
||
| from haystack.components.agents import Agent | ||
| from haystack.components.agents.state import LsStateTool, ReadStateTool, WriteStateTool | ||
| from haystack.components.generators.chat import OpenAIChatGenerator | ||
| from haystack.components.generators.utils import print_streaming_chunk | ||
| from haystack.dataclasses import ChatMessage | ||
|
|
||
| agent = Agent( | ||
| chat_generator=OpenAIChatGenerator(model="gpt-5.4"), | ||
| tools=[LsStateTool(), ReadStateTool(), WriteStateTool()], | ||
| state_schema={"summarize_skill": {"type": str}, "translate_skill": {"type": str}, "final_answer": {"type": str}}, | ||
| system_prompt="""You are a helpful assistant. | ||
| Use ls_state to discover available state keys, read_state to read their values, and write_state to record your final | ||
| response in the `final_answer` key. | ||
|
|
||
| If you see a key that ends in `_skill`, it contains instructions for how to perform a specific task. | ||
| Use these instructions to guide your actions.""", | ||
| ) | ||
|
|
||
| result = agent.run( | ||
| messages=[ | ||
| ChatMessage.from_user( | ||
| """Please summarise the following text: | ||
|
|
||
| Haystack is an open-source AI orchestration framework that you can use to build powerful, production-ready applications with Large Language Models (LLMs) for various use cases. Whether you’re creating autonomous agents, multimodal apps, or scalable RAG systems, Haystack provides the tools to move from idea to production easily. | ||
| Haystack is designed in a modular way, allowing you to combine the best technology from OpenAI, Google, Anthropic, and open-source projects like Hugging Face's Transformers. | ||
| The core foundation of Haystack consists of components and pipelines, along with Document Stores, Agents, Tools, and many integrations. Read more about Haystack concepts in the Haystack Concepts Overview. | ||
| Supported by an engaged community of developers, Haystack has grown into a comprehensive and user-friendly framework for LLM-based development. | ||
| """ # noqa: E501 | ||
| ) | ||
| ], | ||
| summarize_skill=( | ||
| "To summarise text: identify the main topic, strip filler words, and return a single concise sentence." | ||
| ), | ||
| translate_skill=( | ||
| "To translate text: preserve meaning and tone exactly, and return only the translated text without commentary." | ||
| ), | ||
| streaming_callback=print_streaming_chunk, | ||
| ) | ||
|
|
||
| print("Final answer:", result["final_answer"]) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| # SPDX-FileCopyrightText: 2022-present deepset GmbH <info@deepset.ai> | ||
| # | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| from typing import Any | ||
|
|
||
| from haystack.components.agents.state.state import State | ||
| from haystack.core.serialization import generate_qualified_class_name | ||
| from haystack.tools import Tool, Toolset | ||
|
|
||
|
|
||
| def _ls_state(state: State) -> str: | ||
| """List all available keys in the agent state and their types.""" | ||
| lines = [] | ||
| for key, definition in state.schema.items(): | ||
| type_name = getattr(definition["type"], "__name__", str(definition["type"])) | ||
| lines.append(f"- {key} ({type_name})") | ||
| return "\n".join(lines) | ||
|
|
||
|
|
||
| def _read_state(key: str, state: State, truncate: bool = True) -> str: | ||
| """Read the value of a key from the agent state.""" | ||
| if not state.has(key): | ||
| return f"Key '{key}' not found in state. Call ls_state to see available keys." | ||
| value = repr(state.get(key)) | ||
| if truncate and len(value) > 200: | ||
| value = value[:200] + "... [truncated, call cat_state with truncate=False to see full value]" | ||
| return value | ||
|
|
||
|
|
||
| def _write_state(key: str, value: str, state: State) -> str: | ||
| """Write a string value to a key in the agent state. Only string-typed keys are supported.""" | ||
| definition = state.schema.get(key) | ||
| if definition is None: | ||
| return f"Key '{key}' not found in state. Call ls_state to see available keys." | ||
| if definition["type"] is not str: | ||
| type_name = getattr(definition["type"], "__name__", str(definition["type"])) | ||
| return f"Key '{key}' has type '{type_name}'. write_state only supports string-typed keys." | ||
| state.set(key, value) | ||
| return f"State key '{key}' updated successfully." | ||
|
|
||
|
|
||
| class LsStateTool(Tool): | ||
| """ | ||
| A pre-built tool that lists all keys and their types in the agent state. | ||
|
|
||
| The agent can call this as a cheap first step to discover what state is available | ||
| before deciding whether to read any values. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| name: str = "ls_state", | ||
| description: str = "List all available keys in the agent state and their types.", | ||
| ) -> None: | ||
| """ | ||
| Initialize the LsStateTool. | ||
|
|
||
| :param name: Name of the tool. | ||
| :param description: Description of the tool shown to the LLM. | ||
| """ | ||
| super().__init__( | ||
| name=name, description=description, parameters={"type": "object", "properties": {}}, function=_ls_state | ||
| ) | ||
|
|
||
| def to_dict(self) -> dict[str, Any]: | ||
| """ | ||
| Serializes the tool to a dictionary. | ||
|
|
||
| :returns: Dictionary with serialized data. | ||
| """ | ||
| return { | ||
| "type": generate_qualified_class_name(type(self)), | ||
| "data": {"name": self.name, "description": self.description}, | ||
| } | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, data: dict[str, Any]) -> "LsStateTool": | ||
| """ | ||
| Deserializes the tool from a dictionary. | ||
|
|
||
| :param data: Dictionary to deserialize from. | ||
| :returns: Deserialized tool. | ||
| """ | ||
| return cls(**data["data"]) | ||
|
|
||
|
|
||
| class ReadStateTool(Tool): | ||
| """ | ||
| A pre-built tool that reads the value of a single key from the agent state. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| name: str = "read_state", | ||
| description: str = "Read the value of a key from the agent state.", | ||
| key_description: str = "The state key to read. Call ls_state to see available keys.", | ||
| truncate_description: str = ( | ||
| "If True, the value is truncated to 200 characters. Set to False to see the full value." | ||
| ), | ||
| ) -> None: | ||
| """ | ||
| Initialize the CatStateTool. | ||
|
|
||
| :param name: Name of the tool. | ||
| :param description: Description of the tool shown to the LLM. | ||
| :param key_description: Description of the `key` parameter shown to the LLM. | ||
| :param truncate_description: Description of the `truncate` parameter shown to the LLM. | ||
| """ | ||
| self.key_description = key_description | ||
| self.truncate_description = truncate_description | ||
|
|
||
| super().__init__( | ||
| name=name, | ||
| description=description, | ||
| parameters={ | ||
| "type": "object", | ||
| "properties": { | ||
| "key": {"type": "string", "description": key_description}, | ||
| "truncate": {"type": "boolean", "description": truncate_description, "default": False}, | ||
| }, | ||
| "required": ["key"], | ||
| }, | ||
| function=_read_state, | ||
| ) | ||
|
|
||
| def to_dict(self) -> dict[str, Any]: | ||
| """ | ||
| Serializes the tool to a dictionary. | ||
|
|
||
| :returns: Dictionary with serialized data. | ||
| """ | ||
| return { | ||
| "type": generate_qualified_class_name(type(self)), | ||
| "data": { | ||
| "name": self.name, | ||
| "description": self.description, | ||
| "key_description": self.key_description, | ||
| "truncate_description": self.truncate_description, | ||
| }, | ||
| } | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, data: dict[str, Any]) -> "ReadStateTool": | ||
| """ | ||
| Deserializes the tool from a dictionary. | ||
|
|
||
| :param data: Dictionary to deserialize from. | ||
| :returns: Deserialized tool. | ||
| """ | ||
| return cls(**data["data"]) | ||
|
|
||
|
|
||
| class WriteStateTool(Tool): | ||
| """ | ||
| A pre-built tool that writes a string value to a key in the agent state. | ||
|
|
||
| Only string-typed state keys are supported. This is primarily useful for the agent to surface structured string | ||
| outputs (e.g. a `final_answer` field) that downstream pipeline components can consume. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| *, | ||
| name: str = "write_state", | ||
| description: str = "Write a string value to a key in the agent state. Only string-typed keys are supported.", | ||
| key_description: str = ( | ||
| "The state key to write. Must be a string-typed key. Call ls_state to see available keys." | ||
| ), | ||
| value_description: str = "The string value to write.", | ||
| ) -> None: | ||
| """ | ||
| Initialize the WriteStateTool. | ||
|
|
||
| :param name: Name of the tool. | ||
| :param description: Description of the tool shown to the LLM. | ||
| :param key_description: Description of the `key` parameter shown to the LLM. | ||
| :param value_description: Description of the `value` parameter shown to the LLM. | ||
| """ | ||
| self.key_description = key_description | ||
| self.value_description = value_description | ||
|
|
||
| super().__init__( | ||
| name=name, | ||
| description=description, | ||
| parameters={ | ||
| "type": "object", | ||
| "properties": { | ||
| "key": {"type": "string", "description": key_description}, | ||
| "value": {"type": "string", "description": value_description}, | ||
| }, | ||
| "required": ["key", "value"], | ||
| }, | ||
| function=_write_state, | ||
| ) | ||
|
|
||
| def to_dict(self) -> dict[str, Any]: | ||
| """ | ||
| Serializes the tool to a dictionary. | ||
|
|
||
| :returns: Dictionary with serialized data. | ||
| """ | ||
| return { | ||
| "type": generate_qualified_class_name(type(self)), | ||
| "data": { | ||
| "name": self.name, | ||
| "description": self.description, | ||
| "key_description": self.key_description, | ||
| "value_description": self.value_description, | ||
| }, | ||
| } | ||
|
|
||
| @classmethod | ||
| def from_dict(cls, data: dict[str, Any]) -> "WriteStateTool": | ||
| """ | ||
| Deserializes the tool from a dictionary. | ||
|
|
||
| :param data: Dictionary to deserialize from. | ||
| :returns: Deserialized tool. | ||
| """ | ||
| return cls(**data["data"]) | ||
|
|
||
|
|
||
| StateToolset = Toolset([LsStateTool(), ReadStateTool(), WriteStateTool()]) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although it's clear to me what's the purpose of this tool, wouldn't it be a better idea to just call it
ListStateTool? It would make it more explicit, and I imagine some users can be unfamiliar with unix.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah that's fair happy to go with
ListStateTool