Skip to content

Commit d8c1fed

Browse files
authored
LLM Ouptut Parsing back into Blocks (#387)
This allows Telegram (and other transports) to appropriately send them
1 parent 3cb597a commit d8c1fed

5 files changed

Lines changed: 70 additions & 8 deletions

File tree

src/steamship/agents/examples/my_assistant.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,9 @@ def prompt(self, prompt: str) -> str:
3535

3636
def sync_emit(blocks: List[Block], meta: Metadata):
3737
nonlocal output
38-
block_text = "\n".join([b.text for b in blocks if b.is_text()])
38+
block_text = "\n".join(
39+
[b.text if b.is_text() else f"({b.mime_type}: {b.id})" for b in blocks]
40+
)
3941
output += block_text
4042

4143
context.emit_funcs.append(sync_emit)
@@ -44,4 +46,4 @@ def sync_emit(blocks: List[Block], meta: Metadata):
4446

4547

4648
if __name__ == "__main__":
47-
AgentREPL(MyAssistant, "prompt").run()
49+
AgentREPL(MyAssistant, "prompt", agent_package_config={}).run()

src/steamship/agents/react/output_parser.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import re
2-
from typing import Dict, Optional
2+
from typing import Dict, List, Optional
33

4-
from steamship import Block
4+
from steamship import Block, Steamship
55
from steamship.agents.schema import Action, AgentContext, FinishAction, OutputParser, Tool
66

77

@@ -19,7 +19,9 @@ def parse(self, text: str, context: AgentContext) -> Action:
1919
raise RuntimeError(f"Could not parse LLM output: `{text}`")
2020

2121
if "AI:" in text:
22-
return FinishAction(output=[Block(text=text.split("AI:")[-1].strip())], context=context)
22+
return FinishAction(
23+
output=ReACTOutputParser._blocks_from_text(context.client, text), context=context
24+
)
2325

2426
regex = r"Action: (.*?)[\n]*Action Input: (.*)"
2527
match = re.search(regex, text)
@@ -35,3 +37,22 @@ def parse(self, text: str, context: AgentContext) -> Action:
3537
input=[Block(text=action_input)],
3638
context=context,
3739
)
40+
41+
@staticmethod
42+
def _blocks_from_text(client: Steamship, text: str) -> List[Block]:
43+
last_response = text.split("AI:")[-1].strip()
44+
45+
block_id_regex = r"(?:\[Block)?\(?([A-F0-9]{8}\-[A-F0-9]{4}\-[A-F0-9]{4}\-[A-F0-9]{4}\-[A-F0-9]{12})\)?\]?"
46+
remaining_text = last_response
47+
result_blocks: List[Block] = []
48+
while remaining_text is not None and len(remaining_text) > 0:
49+
match = re.search(block_id_regex, remaining_text)
50+
if match:
51+
result_blocks.append(Block(text=remaining_text[0 : match.start()]))
52+
result_blocks.append(Block.get(client, _id=match.group(1)))
53+
remaining_text = remaining_text[match.end() :]
54+
else:
55+
result_blocks.append(Block(text=remaining_text))
56+
remaining_text = ""
57+
58+
return result_blocks

src/steamship/experimental/package_starters/telegram_agent.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pydantic import Field
77

88
from steamship import Block
9-
from steamship.agents.schema import AgentContext, EmitFunc, Metadata
9+
from steamship.agents.schema import Agent, AgentContext, EmitFunc, Metadata
1010
from steamship.experimental.package_starters.web_agent import SteamshipWidgetAgentService
1111
from steamship.experimental.package_starters.web_bot import response_for_exception
1212
from steamship.experimental.transports import TelegramTransport
@@ -20,18 +20,20 @@ class TelegramBotConfig(Config):
2020
class TelegramAgentService(SteamshipWidgetAgentService, ABC):
2121
config: TelegramBotConfig
2222
telegram_transport: TelegramTransport
23+
incoming_message_agent: Agent
2324

2425
@classmethod
2526
def config_cls(cls) -> Type[Config]:
2627
"""Return the Configuration class."""
2728
return TelegramBotConfig
2829

29-
def __init__(self, **kwargs):
30+
def __init__(self, incoming_message_agent: Agent, **kwargs):
3031
super().__init__(**kwargs)
3132
self.api_root = f"https://api.telegram.org/bot{self.config.bot_token}"
3233
self.telegram_transport = TelegramTransport(
3334
bot_token=self.config.bot_token, client=self.client
3435
)
36+
self.incoming_message_agent = incoming_message_agent
3537

3638
def instance_init(self):
3739
"""This instance init method is called automatically when an instance of this package is created. It registers the URL of the instance as the Telegram webhook for messages."""
@@ -62,7 +64,7 @@ def respond(self, **kwargs) -> InvocableResponse[str]:
6264
if len(context.emit_funcs) == 0:
6365
context.emit_funcs.append(self.build_emit_func(chat_id=chat_id))
6466

65-
response = self.run_agent(context)
67+
response = self.run_agent(self.incoming_message_agent, context)
6668
if response is not None:
6769
self.telegram_transport.send(response, metadata={})
6870
else:

tests/steamship_tests/agents/__init__.py

Whitespace-only changes.
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import pytest
2+
3+
from steamship import Block, File, Steamship
4+
from steamship.agents.react import ReACTOutputParser
5+
6+
7+
@pytest.mark.usefixtures("client")
8+
def test_parse_output(client: Steamship):
9+
10+
file = File.create(client, blocks=[Block(text="test"), Block(text="Another test")])
11+
block_id = file.blocks[0].id
12+
block_2_id = file.blocks[1].id
13+
14+
example1 = f"some text [Block({block_id})] some more text"
15+
16+
parsed_blocks1 = ReACTOutputParser._blocks_from_text(client, example1)
17+
assert len(parsed_blocks1) == 3
18+
assert parsed_blocks1[0].text == "some text "
19+
assert parsed_blocks1[1].id == block_id
20+
assert parsed_blocks1[2].text == " some more text"
21+
22+
example2 = f"some text [Block({block_id})] some more text {block_2_id} even more text"
23+
parsed_blocks2 = ReACTOutputParser._blocks_from_text(client, example2)
24+
assert len(parsed_blocks2) == 5
25+
assert parsed_blocks2[0].text == "some text "
26+
assert parsed_blocks2[1].id == block_id
27+
assert parsed_blocks2[2].text == " some more text "
28+
assert parsed_blocks2[3].id == block_2_id
29+
assert parsed_blocks2[4].text == " even more text"
30+
31+
example3 = f"some text [Block({block_id})] some more text {block_2_id}"
32+
parsed_blocks3 = ReACTOutputParser._blocks_from_text(client, example3)
33+
assert len(parsed_blocks3) == 4
34+
assert parsed_blocks3[0].text == "some text "
35+
assert parsed_blocks3[1].id == block_id
36+
assert parsed_blocks3[2].text == " some more text "
37+
assert parsed_blocks3[3].id == block_2_id

0 commit comments

Comments
 (0)