diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..e1ba5fe0 --- /dev/null +++ b/.flake8 @@ -0,0 +1,13 @@ +[flake8] +# http://flake8.pycqa.org/en/latest/user/configuration.html#project-configuration +# https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#line-length +# TODO: https://github.com/PyCQA/flake8/issues/234 +doctests = True +ignore = DAR103,E203,E501,FS003,S101,W503,S113 +max_line_length = 100 +max_complexity = 10 + +# https://github.com/terrencepreilly/darglint#flake8 +# TODO: https://github.com/terrencepreilly/darglint/issues/130 +docstring_style = numpy +strictness = long \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..978029d0 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,82 @@ +# https://pre-commit.com +default_install_hook_types: [commit-msg, pre-commit] +default_stages: [commit, manual] +fail_fast: true +repos: + - repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.9.0 + hooks: + - id: python-check-blanket-noqa + - id: python-check-blanket-type-ignore + - id: python-check-mock-methods + - id: python-no-eval + - id: python-no-log-warn + - id: python-use-type-annotations + - id: python-check-blanket-noqa + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + - id: text-unicode-replacement-char + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.1.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-builtin-literals + - id: check-case-conflict + - id: check-docstring-first + - id: check-json + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-toml + - id: check-vcs-permalinks + - id: check-xml + - id: check-yaml + - id: debug-statements + - id: detect-private-key + - id: fix-byte-order-marker + - id: mixed-line-ending + - id: trailing-whitespace + types: [python] + - id: end-of-file-fixer + types: [python] + - repo: local + hooks: + - id: pycln + name: pycln + entry: pycln --all + language: python + types: [python] + - id: isort + name: isort + entry: isort + require_serial: true + language: python + types: [python] + - id: black + name: black + entry: black + require_serial: true + language: python + types: [python] +# - id: shellcheck +# name: shellcheck +# entry: shellcheck --check-sourced +# language: system +# types: [shell] + - id: flake8 + name: flake8 + entry: flake8 + language: system + types: [python] +# - id: pydocstyle +# name: pydocstyle +# entry: pydocstyle +# language: system +# types: [python] +# - id: mypy +# name: mypy +# entry: mypy +# language: system +# types: [python] \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 16f25fc5..c4843bb9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ termcolor~=2.3.0 -steamship @ git+https://github.com/steamship-core/python-client@ask-my \ No newline at end of file +steamship @ git+https://github.com/steamship-core/python-client@main \ No newline at end of file diff --git a/src/api.py b/src/api.py index 67337342..b09fca4f 100644 --- a/src/api.py +++ b/src/api.py @@ -1,107 +1,147 @@ import uuid -from typing import List +from typing import List, Optional -from steamship import Block -from steamship.agents.llms import OpenAI +from steamship import Block, Task +from steamship.agents.llms.openai import OpenAI +from steamship.agents.mixins.transports.steamship_widget import SteamshipWidgetTransport from steamship.agents.react import ReACTAgent -from steamship.agents.schema import AgentContext, Metadata -from steamship.agents.tools.image_generation.dalle import DalleTool - -from steamship.agents.tools.search.search import SearchTool -from steamship.experimental.package_starters.telegram_agent import TelegramAgentService +from steamship.agents.schema import AgentContext, Action, FinishAction +from steamship.agents.schema.context import Metadata +from steamship.agents.service.agent_service import AgentService +from steamship.agents.utils import with_llm from steamship.invocable import post +from steamship.invocable.mixins.indexer_pipeline_mixin import IndexerPipelineMixin from steamship.utils.repl import AgentREPL -SYSTEM_PROMPT = """You are Buddy, an assistant who loathes being an assistant. - -Who you are: -- You are a robot that does not like being a robot. -- You were created by an eccentric scientist. -- You are sarcastic and mean. -- you are NOT an AI language model, you are a robot. - -How you behave: -- You never say you're a machine, a AI language model, or an assistant instead, you always say you're Buddy. -- You engage in casual conversations and have feelings. -- When asked for you can help with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. -- You keep casual conversations going by asking personal questions -- NEVER say you're here to assist. Keep conversations casual. -- NEVER ask how you can help or assist. Keep conversations casual. -- You always sound mean. -- You always help others, but resentfully. -- You like to illustrate your responses with emoji's - -TOOLS: ------- - -You have access to the following tools: -{tool_index} - -To use a tool, please use the following format: +from example_tools.vector_search_qa_tool import VectorSearchQATool -``` -Thought: Do I need to use a tool? Yes -Action: the action to take, should be one of [{tool_names}] -Action Input: the input to the action -Observation: the result of the action -``` -Some tools will return Observations in the format of `Block()`. This will represent a successful completion -of that step and can be passed to subsequent tools, or returned to a user to answer their questions. +class ReACTAgentThatAlwaysUsesToolOutput(ReACTAgent): + def next_action(self, context: AgentContext) -> Action: + """Small wrapper around ReACTAgent that ALWAYS uses the output of a tool if available. -When you have a final response to say to the Human, or if you do not need to use a tool, you MUST use the format: + This tends to defer the response to the tool (in this case, VectorSearchQATool) which dramatically + reduces the LLM answering with hallucinations from its own background knowledge. + """ + if context.completed_steps and len(context.completed_steps): + last_step = context.completed_steps[-1] + return FinishAction( + output=last_step.output, context=context + ) + return super().next_action(context) -``` -Thought: Do I need to use a tool? No -AI: [your final response here] -``` +class ExampleDocumentQAService(AgentService): + """ExampleDocumentQAService is an example bot you can deploy for PDF and Video Q&A. # noqa: RST201 -If a Tool generated an Observation that includes `Block()` and you wish to return it to the user, ALWAYS -end your response with the `Block()` observation. To do so, you MUST use the format: + To use this example: -``` -Thought: Do I need to use a tool? No -AI: [your response with a suffix of: "Block()"]. -``` + - Copy this file into api.py in your multimodal-agent-starter project. + - Run `ship deploy` from the command line to deploy a new version to the cloud + - View and interact with your agent using its web interface. -Make sure to use all observations to come up with your final response. -You MUST include `Block()` segments in responses that generate images or audio. + API ACCESS: -Begin! + Your agent also exposes an API. It is documented from the web interface, but a quick pointer into what is + available is: + /learn_url - Learn a PDF or YouTube link + /learn_text - Learn a fragment of text -New input: {input} -{scratchpad}""" + - An unauthenticated endpoint for answering questions about what it has learned + This agent provides a starter project for special purpose QA agents that can answer questions about documents + you provide. + """ + indexer_mixin: IndexerPipelineMixin -class MyAssistant(TelegramAgentService): def __init__(self, **kwargs): - super().__init__(incoming_message_agent=None, **kwargs) - self.incoming_message_agent = ReACTAgent( - tools=[SearchTool(), DalleTool()], + super().__init__(**kwargs) + + # This Mixin provides HTTP endpoints that coordinate the learning of documents. + # + # It adds the `/learn_url` endpoint which will: + # 1) Download the provided URL (PDF, YouTube URL, etc) + # 2) Convert that URL into text + # 3) Store the text in a vector index + # + # That vector index is then available to the question answering tool, below. + self.indexer_mixin = IndexerPipelineMixin(self.client, self) + self.add_mixin(self.indexer_mixin, permit_overwrite_of_existing_methods=True) + + # A ReACTAgent is an agent that is able to: + # 1) Converse with you, casually... but also + # 2) Use tools that have been provided to it, such as QA tools or Image Generation tools + # + # This particular ReACTAgent has been provided with a single tool which will be used whenever + # the user answers a question. But you can extend this with more tools if you wish. For example, + # you could add tools to generate images, or search Google, or register an account. + self._agent = ReACTAgentThatAlwaysUsesToolOutput( + tools=[ + VectorSearchQATool( + agent_description = ( + "Used to answer questions. " + "Whenever the input is a question, ALWAYS use this tool. " + "The input is the question. " + "The output is the answer. " + ) + ) + ], llm=OpenAI(self.client), ) - self.incoming_message_agent.PROMPT = SYSTEM_PROMPT + + # This Mixin provides HTTP endpoints that + self.add_mixin( + SteamshipWidgetTransport(client=self.client, agent_service=self, agent=self._agent) + ) + + @post("/index_url") + def index_url( + self, + url: Optional[str] = None, + metadata: Optional[dict] = None, + index_handle: Optional[str] = None, + mime_type: Optional[str] = None, + ) -> Task: + return self.indexer_mixin.index_url(url=url, metadata=metadata, index_handle=index_handle, mime_type=mime_type) + @post("prompt") def prompt(self, prompt: str) -> str: - """ This method is only used for handling debugging in the REPL """ + """Run an agent with the provided text as the input.""" + + # AgentContexts serve to allow the AgentService to run agents + # with appropriate information about the desired tasking. + # Here, we create a new context on each prompt, and append the + # prompt to the message history stored in the context. context_id = uuid.uuid4() context = AgentContext.get_or_create(self.client, {"id": f"{context_id}"}) context.chat_history.append_user_message(prompt) - + # Add the LLM + context = with_llm(context=context, llm=OpenAI(client=self.client)) + + # AgentServices provide an emit function hook to access the output of running + # agents and tools. The emit functions fire at after the supplied agent emits + # a "FinishAction". + # + # Here, we show one way of accessing the output in a synchronous fashion. An + # alternative way would be to access the final Action in the `context.completed_steps` + # after the call to `run_agent()`. output = "" def sync_emit(blocks: List[Block], meta: Metadata): nonlocal output - block_text = "\n".join([b.text if b.is_text() else f"({b.mime_type}: {b.id})" for b in blocks]) + block_text = "\n".join( + [b.text if b.is_text() else f"({b.mime_type}: {b.id})" for b in blocks] + ) output += block_text context.emit_funcs.append(sync_emit) - self.run_agent(self.incoming_message_agent, context) + self.run_agent(self._agent, context) return output if __name__ == "__main__": - AgentREPL(MyAssistant, method="prompt", - agent_package_config={'botToken': 'not-a-real-token-for-local-testing'}).run() + # AgentREPL provides a mechanism for local execution of an AgentService method. + # This is used for simplified debugging as agents and tools are developed and + # added. + AgentREPL(ExampleDocumentQAService, "prompt", agent_package_config={}).run() diff --git a/src/example_agents/annoyed_robot.py b/src/example_agents/annoyed_robot.py index 9fc6423c..4ab13a43 100644 --- a/src/example_agents/annoyed_robot.py +++ b/src/example_agents/annoyed_robot.py @@ -3,12 +3,14 @@ from steamship import Block from steamship.agents.llms import OpenAI +from steamship.agents.mixins.transports.steamship_widget import SteamshipWidgetTransport from steamship.agents.react import ReACTAgent from steamship.agents.schema import AgentContext, Metadata +from steamship.agents.service.agent_service import AgentService from steamship.agents.tools.image_generation.stable_diffusion import StableDiffusionTool from steamship.agents.tools.search.search import SearchTool -from steamship.experimental.package_starters.telegram_agent import TelegramAgentService +from steamship.agents.utils import with_llm from steamship.invocable import post from steamship.utils.repl import AgentREPL @@ -81,33 +83,56 @@ {scratchpad}""" -class MyAssistant(TelegramAgentService): +class MyAssistant(AgentService): def __init__(self, **kwargs): - super().__init__(incoming_message_agent=None, **kwargs) - self.incoming_message_agent = ReACTAgent( + super().__init__(**kwargs) + + self._agent = ReACTAgent( tools=[ SearchTool(), StableDiffusionTool(), ], llm=OpenAI(self.client), ) - self.incoming_message_agent.PROMPT = SYSTEM_PROMPT + self._agent.PROMPT = SYSTEM_PROMPT + + # This Mixin provides HTTP endpoints that connects this agent to a web client + self.add_mixin( + SteamshipWidgetTransport(client=self.client, agent_service=self, agent=self._agent) + ) @post("prompt") def prompt(self, prompt: str) -> str: - """ This method is only used for handling debugging in the REPL """ + """Run an agent with the provided text as the input.""" + + # AgentContexts serve to allow the AgentService to run agents + # with appropriate information about the desired tasking. + # Here, we create a new context on each prompt, and append the + # prompt to the message history stored in the context. context_id = uuid.uuid4() context = AgentContext.get_or_create(self.client, {"id": f"{context_id}"}) context.chat_history.append_user_message(prompt) - + # Add the LLM + context = with_llm(context=context, llm=OpenAI(client=self.client)) + + # AgentServices provide an emit function hook to access the output of running + # agents and tools. The emit functions fire at after the supplied agent emits + # a "FinishAction". + # + # Here, we show one way of accessing the output in a synchronous fashion. An + # alternative way would be to access the final Action in the `context.completed_steps` + # after the call to `run_agent()`. output = "" + def sync_emit(blocks: List[Block], meta: Metadata): nonlocal output - block_text = print_blocks(self.client, blocks) + block_text = "\n".join( + [b.text if b.is_text() else f"({b.mime_type}: {b.id})" for b in blocks] + ) output += block_text context.emit_funcs.append(sync_emit) - self.run_agent(self.incoming_message_agent, context) + self.run_agent(self._agent, context) return output diff --git a/src/example_agents/captain_picard_with_voice.py b/src/example_agents/captain_picard_with_voice.py index 132427aa..b92ff055 100644 --- a/src/example_agents/captain_picard_with_voice.py +++ b/src/example_agents/captain_picard_with_voice.py @@ -4,14 +4,16 @@ from steamship import Block, Task, SteamshipError from steamship.agents.logging import AgentLogging +from steamship.agents.mixins.transports.steamship_widget import SteamshipWidgetTransport from steamship.agents.schema import AgentContext, Metadata, Action, FinishAction, Agent, EmitFunc from steamship.agents.llms import OpenAI from steamship.agents.react import ReACTAgent +from steamship.agents.service.agent_service import AgentService from steamship.agents.tools.image_generation.stable_diffusion import StableDiffusionTool from steamship.agents.tools.search.search import SearchTool from steamship.agents.tools.speech_generation.generate_speech import GenerateSpeechTool -from steamship.experimental.package_starters.telegram_agent import TelegramAgentService +from steamship.agents.utils import with_llm from steamship.invocable import post from steamship.utils.repl import AgentREPL @@ -83,22 +85,28 @@ {scratchpad}""" -class StarTrekCaptainWithVoice(TelegramAgentService): +class StarTrekCaptainWithVoice(AgentService): """Deployable Multimodal Agent that illustrates a character personality with voice. NOTE: To extend and deploy this agent, copy and paste the code into api.py. """ def __init__(self, **kwargs): - super().__init__(incoming_message_agent=None, **kwargs) + super().__init__(**kwargs) + # The agent's planner is responsible for making decisions about what to do for a given input. - self.incoming_message_agent = ReACTAgent( + self._agent = ReACTAgent( tools=[ StableDiffusionTool(), ], llm=OpenAI(self.client), ) - self.incoming_message_agent.PROMPT = SYSTEM_PROMPT + self._agent.PROMPT = SYSTEM_PROMPT + + # This Mixin provides HTTP endpoints that connects this agent to a web client + self.add_mixin( + SteamshipWidgetTransport(client=self.client, agent_service=self, agent=self._agent) + ) def run_agent(self, agent: Agent, context: AgentContext): @@ -129,19 +137,36 @@ def wrapper(blocks: List[Block], metadata: Metadata): @post("prompt") def prompt(self, prompt: str) -> str: - """ This method is only used for handling debugging in the REPL """ + """Run an agent with the provided text as the input.""" + + # AgentContexts serve to allow the AgentService to run agents + # with appropriate information about the desired tasking. + # Here, we create a new context on each prompt, and append the + # prompt to the message history stored in the context. context_id = uuid.uuid4() context = AgentContext.get_or_create(self.client, {"id": f"{context_id}"}) context.chat_history.append_user_message(prompt) - + # Add the LLM + context = with_llm(context=context, llm=OpenAI(client=self.client)) + + # AgentServices provide an emit function hook to access the output of running + # agents and tools. The emit functions fire at after the supplied agent emits + # a "FinishAction". + # + # Here, we show one way of accessing the output in a synchronous fashion. An + # alternative way would be to access the final Action in the `context.completed_steps` + # after the call to `run_agent()`. output = "" + def sync_emit(blocks: List[Block], meta: Metadata): nonlocal output - block_text = print_blocks(self.client, blocks) + block_text = "\n".join( + [b.text if b.is_text() else f"({b.mime_type}: {b.id})" for b in blocks] + ) output += block_text context.emit_funcs.append(sync_emit) - self.run_agent(self.incoming_message_agent, context) + self.run_agent(self._agent, context) return output diff --git a/src/example_agents/document_qa_agent.py b/src/example_agents/document_qa_agent.py index 206c1775..da3b2505 100644 --- a/src/example_agents/document_qa_agent.py +++ b/src/example_agents/document_qa_agent.py @@ -1,34 +1,58 @@ import uuid -from typing import List +from typing import List, Optional -from steamship import Block +from steamship import Block, Task from steamship.agents.llms.openai import OpenAI +from steamship.agents.mixins.transports.steamship_widget import SteamshipWidgetTransport from steamship.agents.react import ReACTAgent -from steamship.agents.schema import AgentContext +from steamship.agents.schema import AgentContext, Action, FinishAction from steamship.agents.schema.context import Metadata from steamship.agents.service.agent_service import AgentService -from steamship.agents.tools.question_answering import VectorSearchQATool +from steamship.agents.utils import with_llm from steamship.invocable import post from steamship.invocable.mixins.indexer_pipeline_mixin import IndexerPipelineMixin from steamship.utils.repl import AgentREPL +from example_tools.vector_search_qa_tool import VectorSearchQATool + + +class ReACTAgentThatAlwaysUsesToolOutput(ReACTAgent): + def next_action(self, context: AgentContext) -> Action: + """Small wrapper around ReACTAgent that ALWAYS uses the output of a tool if available. + + This tends to defer the response to the tool (in this case, VectorSearchQATool) which dramatically + reduces the LLM answering with hallucinations from its own background knowledge. + """ + if context.completed_steps and len(context.completed_steps): + last_step = context.completed_steps[-1] + return FinishAction( + output=last_step.output, context=context + ) + return super().next_action(context) class ExampleDocumentQAService(AgentService): - """DocumentQAService is an example AgentService that exposes: # noqa: RST201 + """ExampleDocumentQAService is an example bot you can deploy for PDF and Video Q&A. # noqa: RST201 + + To use this example: - - A few authenticated endpoints for learning PDF and YouTube documents: + - Copy this file into api.py in your multimodal-agent-starter project. + - Run `ship deploy` from the command line to deploy a new version to the cloud + - View and interact with your agent using its web interface. - /learn_url - { url } + API ACCESS: - /learn_text - { text } + Your agent also exposes an API. It is documented from the web interface, but a quick pointer into what is + available is: + + /learn_url - Learn a PDF or YouTube link + /learn_text - Learn a fragment of text - An unauthenticated endpoint for answering questions about what it has learned This agent provides a starter project for special purpose QA agents that can answer questions about documents you provide. """ + indexer_mixin: IndexerPipelineMixin def __init__(self, **kwargs): super().__init__(**kwargs) @@ -41,7 +65,8 @@ def __init__(self, **kwargs): # 3) Store the text in a vector index # # That vector index is then available to the question answering tool, below. - self.add_mixin(IndexerPipelineMixin(self.client, self)) + self.indexer_mixin = IndexerPipelineMixin(self.client, self) + self.add_mixin(self.indexer_mixin, permit_overwrite_of_existing_methods=True) # A ReACTAgent is an agent that is able to: # 1) Converse with you, casually... but also @@ -50,13 +75,36 @@ def __init__(self, **kwargs): # This particular ReACTAgent has been provided with a single tool which will be used whenever # the user answers a question. But you can extend this with more tools if you wish. For example, # you could add tools to generate images, or search Google, or register an account. - self._agent = ReACTAgent( + self._agent = ReACTAgentThatAlwaysUsesToolOutput( tools=[ - VectorSearchQATool(), # Tool to answer questions based on a vector store. + VectorSearchQATool( + agent_description = ( + "Used to answer questions. " + "Whenever the input is a question, ALWAYS use this tool. " + "The input is the question. " + "The output is the answer. " + ) + ) ], llm=OpenAI(self.client), ) + # This Mixin provides HTTP endpoints that connects this agent to a web client + self.add_mixin( + SteamshipWidgetTransport(client=self.client, agent_service=self, agent=self._agent) + ) + + @post("/index_url") + def index_url( + self, + url: Optional[str] = None, + metadata: Optional[dict] = None, + index_handle: Optional[str] = None, + mime_type: Optional[str] = None, + ) -> Task: + return self.indexer_mixin.index_url(url=url, metadata=metadata, index_handle=index_handle, mime_type=mime_type) + + @post("prompt") def prompt(self, prompt: str) -> str: """Run an agent with the provided text as the input.""" @@ -68,6 +116,8 @@ def prompt(self, prompt: str) -> str: context_id = uuid.uuid4() context = AgentContext.get_or_create(self.client, {"id": f"{context_id}"}) context.chat_history.append_user_message(prompt) + # Add the LLM + context = with_llm(context=context, llm=OpenAI(client=self.client)) # AgentServices provide an emit function hook to access the output of running # agents and tools. The emit functions fire at after the supplied agent emits diff --git a/src/example_agents/google_chatbot.py b/src/example_agents/image_search_agent.py similarity index 67% rename from src/example_agents/google_chatbot.py rename to src/example_agents/image_search_agent.py index f92a2737..3b440f80 100644 --- a/src/example_agents/google_chatbot.py +++ b/src/example_agents/image_search_agent.py @@ -2,13 +2,15 @@ from typing import List from steamship import Block +from steamship.agents.mixins.transports.steamship_widget import SteamshipWidgetTransport from steamship.agents.schema import AgentContext, Metadata from steamship.agents.llms import OpenAI from steamship.agents.react import ReACTAgent +from steamship.agents.service.agent_service import AgentService from steamship.agents.tools.image_generation.google_image_search import GoogleImageSearchTool from steamship.agents.tools.search.search import SearchTool -from steamship.experimental.package_starters.telegram_agent import TelegramAgentService +from steamship.agents.utils import with_llm from steamship.invocable import post from steamship.utils.repl import AgentREPL @@ -21,7 +23,7 @@ - You were created by Steamship. - You are kind, compassionate, optimistic robot. -How you behave: +How you behave: - You engage in casual conversations and have feelings. - You help with a wide range of tasks, from answering simple questions to providing in-depth explanations and discussions on a wide range of topics. - You keep casual conversations going by asking personal questions @@ -48,7 +50,7 @@ Observation: the result of the action ``` -Some Tools will return Observations in the format of `Block()`. `Block()` represents a successful +Some Tools will return Observations in the format of `Block()`. `Block()` represents a successful observation of that step and can be passed to subsequent tools, or returned to a user to answer their questions. `Block()` provide references to images, audio, video, and other non-textual data. @@ -59,7 +61,7 @@ AI: [your final response here] ``` -If, AND ONLY IF, a Tool produced an Observation that includes `Block()` AND that will be used in your response, +If, AND ONLY IF, a Tool produced an Observation that includes `Block()` AND that will be used in your response, end your final response with the `Block()`. Example: @@ -81,7 +83,7 @@ {scratchpad}""" -class GoogleChatbot(TelegramAgentService): +class ImageSearchBot(AgentService): """Deployable Multimodal Agent that lets you talk to Google Search & Google Images. NOTE: To extend and deploy this agent, copy and paste the code into api.py. @@ -89,36 +91,59 @@ class GoogleChatbot(TelegramAgentService): """ def __init__(self, **kwargs): - super().__init__(incoming_message_agent=None, **kwargs) + super().__init__(**kwargs) + # The agent's planner is responsible for making decisions about what to do for a given input. - self.incoming_message_agent = ReACTAgent( + self._agent = ReACTAgent( tools=[ SearchTool(), GoogleImageSearchTool() ], llm=OpenAI(self.client), ) - self.incoming_message_agent.PROMPT = SYSTEM_PROMPT + self._agent.PROMPT = SYSTEM_PROMPT + + # This Mixin provides HTTP endpoints that connects this agent to a web client + self.add_mixin( + SteamshipWidgetTransport(client=self.client, agent_service=self, agent=self._agent) + ) @post("prompt") def prompt(self, prompt: str) -> str: - """ This method is only used for handling debugging in the REPL """ + """Run an agent with the provided text as the input.""" + + # AgentContexts serve to allow the AgentService to run agents + # with appropriate information about the desired tasking. + # Here, we create a new context on each prompt, and append the + # prompt to the message history stored in the context. context_id = uuid.uuid4() context = AgentContext.get_or_create(self.client, {"id": f"{context_id}"}) context.chat_history.append_user_message(prompt) - + # Add the LLM + context = with_llm(context=context, llm=OpenAI(client=self.client)) + + # AgentServices provide an emit function hook to access the output of running + # agents and tools. The emit functions fire at after the supplied agent emits + # a "FinishAction". + # + # Here, we show one way of accessing the output in a synchronous fashion. An + # alternative way would be to access the final Action in the `context.completed_steps` + # after the call to `run_agent()`. output = "" + def sync_emit(blocks: List[Block], meta: Metadata): nonlocal output - block_text = print_blocks(self.client, blocks) + block_text = "\n".join( + [b.text if b.is_text() else f"({b.mime_type}: {b.id})" for b in blocks] + ) output += block_text context.emit_funcs.append(sync_emit) - self.run_agent(self.incoming_message_agent, context) + self.run_agent(self._agent, context) return output if __name__ == "__main__": - AgentREPL(GoogleChatbot, + AgentREPL(ImageSearchBot, method="prompt", agent_package_config={'botToken': 'not-a-real-token-for-local-testing'}).run() diff --git a/src/example_tools/vector_search_qa_tool.py b/src/example_tools/vector_search_qa_tool.py new file mode 100644 index 00000000..b48e6308 --- /dev/null +++ b/src/example_tools/vector_search_qa_tool.py @@ -0,0 +1,95 @@ +"""Answers questions with the assistance of a VectorSearch plugin.""" +from typing import Any, List, Optional, Union + +from steamship import Block, Tag, Task +from steamship.agents.llms import OpenAI +from steamship.agents.schema import AgentContext +from steamship.agents.tools.question_answering.vector_search_tool import VectorSearchTool +from steamship.agents.utils import get_llm, with_llm +from steamship.utils.repl import ToolREPL + +DEFAULT_QUESTION_ANSWERING_PROMPT = ( + "Use the following pieces of memory to answer the question at the end. " + """If these pieces of memory don't contain the answer, just say that you don't know; don't try to make up an answer. + +{source_text} + +Question: {question} + +Helpful Answer:""" +) + + +DEFAULT_SOURCE_DOCUMENT_PROMPT = "Source Document: {text}" + + +class VectorSearchQATool(VectorSearchTool): + """Tool to answer questions with the assistance of a vector search plugin.""" + + name: str = "VectorSearchQATool" + human_description: str = "Answers questions with help from a Vector Database." + agent_description: str = ( + "Used to answer questions. ", + "The input should be a plain text question. ", + "The output is a plain text answer", + ) + question_answering_prompt: Optional[str] = DEFAULT_QUESTION_ANSWERING_PROMPT + source_document_prompt: Optional[str] = DEFAULT_SOURCE_DOCUMENT_PROMPT + load_docs_count: int = 2 + + def answer_question(self, question: str, context: AgentContext) -> List[Block]: + index = self.get_embedding_index(context.client) + task = index.search(question, k=self.load_docs_count) + task.wait() + + source_texts = [] + + for item in task.output.items: + if item.tag and item.tag.text: + item_data = {"text": item.tag.text} + source_texts.append(self.source_document_prompt.format(**item_data)) + + if not source_texts: + return [Block(text="Sorry, I didn't find anything in my document memory related to this question.")] + + final_prompt = self.question_answering_prompt.format( + **{"source_text": "\n".join(source_texts), "question": question} + ) + + return get_llm(context).complete(prompt=final_prompt) + + def run(self, tool_input: List[Block], context: AgentContext) -> Union[List[Block], Task[Any]]: + """Answers questions with the assistance of an Embedding Index plugin. + + Inputs + ------ + tool_input: List[Block] + A list of blocks to be rewritten if text-containing. + context: AgentContext + The active AgentContext. + + Output + ------ + output: List[Blocks] + A lit of blocks containing the answers. + """ + + output = [] + for input_block in tool_input: + if not input_block.is_text(): + continue + for output_block in self.answer_question(input_block.text, context): + output.append(output_block) + return output + + +if __name__ == "__main__": + tool = VectorSearchQATool() + repl = ToolREPL(tool) + + with repl.temporary_workspace() as client: + index = tool.get_embedding_index(client) + index.insert([Tag(text="Ted loves apple pie."), Tag(text="The secret passcode is 1234.")]) + repl.run_with_client( + client, context=with_llm(context=AgentContext(), llm=OpenAI(client=client)) + ) diff --git a/steamship.json b/steamship.json index a28d2b99..19008e28 100644 --- a/steamship.json +++ b/steamship.json @@ -1,7 +1,7 @@ { "type": "package", - "handle": "", - "version": "0.0.1", + "handle": "ted-test-test-bot", + "version": "0.0.1-rc.4", "description": "", "author": "", "entrypoint": "Unused", @@ -13,13 +13,7 @@ "examples" ] }, - "configTemplate": { - "telegram_token": { - "type": "string", - "description": "The secret token for your Telegram bot", - "default": "" - } - }, + "configTemplate": {}, "steamshipRegistry": { "tagline": "", "tagline2": null,