diff --git a/a2a-samples b/a2a-samples new file mode 160000 index 00000000..834c1e7b --- /dev/null +++ b/a2a-samples @@ -0,0 +1 @@ +Subproject commit 834c1e7b843115d94722668b1ce27c0584dfda6e diff --git a/agentstack_readme.md b/agentstack_readme.md new file mode 100644 index 00000000..7b710179 --- /dev/null +++ b/agentstack_readme.md @@ -0,0 +1,68 @@ +# Mellea-Agentstack + +Mellea is a library for writing generative programs. +Agentstack is an open-source framework for building production-grade multi-agent systems. +This example serves to merge both libraries with a simple module that will allow users to transform +their Mellea programs into Agentstack agentic interfaces with structured (form) inputs. + +We provide the example of an email writer. Only text inputs are currently supported. + +# Installing Agentstack + +To install Agentstack, follow the instructions at this page: https://agentstack.beeai.dev/introduction/welcome + + +# Running the example + +Then, in order to run the example email writer, run the code below in the root of the directory: +```bash +uv run --with agentstack-sdk docs/examples/agentstack_agent.py +``` + +In a separate terminal, either run +```bash +agentstack run mellea_agent +``` + +OR open the UI and select the **mellea-agent**. + +```bash +agentstack ui +``` + +# Tutorial + +To create your own Agentstack agent with Mellea, write a traditional program with Mellea, as shown below. We provide the source code of the email writer. + +```bash +@agentstack_app +def mellea_func(m: MelleaSession, sender: str, recipient, subject: str, topic: str) -> tuple[ModelOutputThunk, Context] | SamplingResult: + """ + Example email writing module that utilizes an IVR loop in Mellea to generate an email with a specific list of requirements. + Inputs: + sender: str + recipient: str + subject: str + topic: str + Output: + sampling: tuple[ModelOutputThunk, Context] | SamplingResult + """ + requirements = [ + req("Be formal."), + req("Be funny."), + req(f"Make sure that the email is from {sender}, is towards {recipient}, has {subject} as the subject, and is focused on {topic} as a topic"), + Requirement("Use less than 100 words.", + validation_fn=simple_validate(lambda o: len(o.split()) < 100)) + ] + description = f"Write an email from {sender}. Subject of email is {subject}. Name of recipient is {recipient}. Topic of email should be {topic}." + sampling = m.instruct(description=description, requirements=requirements, strategy=RejectionSamplingStrategy(loop_budget=3), return_sampling_results=True) + return sampling +``` + +Adjust ```requirements``` and ```prompt``` as necessary. + +As shown above, note that the first parameter should be an **m** object. + +Then, to deploy your Mellea program to Agentstack, wrap with the ```@agentstack_app``` decorator, as shown above. + +Place your code in the ```docs/examples/``` folder. diff --git a/cli/serve/bee_playform/types.py b/cli/serve/bee_playform/types.py new file mode 100644 index 00000000..42dea6b9 --- /dev/null +++ b/cli/serve/bee_playform/types.py @@ -0,0 +1,37 @@ +class RangeType: + """A custom range class that mimics the built-in range() behavior.""" + + def __init__(self, start, stop=None, step=1): + if step == 0: + raise ValueError("RangeType() arg 3 must not be zero") + + if stop is None: + self.start = 0 + self.stop = start + else: + self.start = start + self.stop = stop + + self.step = step + self.current = self.start + + def __iter__(self): + """Returns the iterator object.""" + self.current = self.start # Reset iterator for each new loop + return self + + def __next__(self): + """Returns the next value in the sequence.""" + if self.step > 0 and self.current >= self.stop: + raise StopIteration + if self.step < 0 and self.current <= self.stop: + raise StopIteration + + value = self.current + self.current += self.step + return value + + def __repr__(self): + """Official string representation of the object.""" + return f"RangeType({self.start}, {self.stop}, {self.step})" + diff --git a/docs/examples/agentstack_agent.py b/docs/examples/agentstack_agent.py new file mode 100644 index 00000000..7f1d60b1 --- /dev/null +++ b/docs/examples/agentstack_agent.py @@ -0,0 +1,116 @@ +""" +Example use case for Agentstack integration: utilizing a Mellea program to write an email with an IVF loop. +""" +import os +import asyncio +import sys +import mellea +import inspect +from typing import Annotated, Callable + +from mellea.stdlib.requirement import req, check, simple_validate +from mellea import MelleaSession, start_session +from mellea.stdlib.base import ChatContext, ModelOutputThunk + +from mellea.stdlib.sampling import RejectionSamplingStrategy +from mellea.stdlib.sampling.types import SamplingResult +from mellea.stdlib.sampling.base import Context +from mellea.stdlib.requirement import req, Requirement, simple_validate +from mellea.backends.openai import OpenAIBackend + +#from agentstack_platform.agentstack_platform import agentstack_app + +from a2a.types import Message +from a2a.utils.message import get_message_text +from agentstack_sdk.a2a.types import AgentMessage +from agentstack_sdk.server import Server +from agentstack_sdk.a2a.extensions import ( + LLMServiceExtensionServer, LLMServiceExtensionSpec, + TrajectoryExtensionServer, TrajectoryExtensionSpec, + AgentDetail +) +from agentstack_sdk.a2a.extensions.ui.form import ( + FormExtensionServer, FormExtensionSpec, FormRender, TextField +) + +def agentstack_app(func: Callable) -> Callable: + """Serves as a wrapper that takes any Mellea program and converts it to a BeeAI Agent. This is an example for an email writer.""" + server = Server() + + params : dict = inspect.signature(func).parameters # Mapping params from Mellea function onto form inputs + form_fields : list[str] = list(params.keys())[1:] + all_fields : list[TextField] = [] + + for field in form_fields: + all_fields.append(TextField(id=field, label=field, col_span=2)) #Maps all input params from Mellea agent into BeeAI Forms + + + form_render = FormRender( + id="input_form", + title="Please provide your information", + columns=2, + fields=all_fields + ) + form_extension_spec = FormExtensionSpec(form_render) + + + @server.agent(name="mellea_agent") + + async def wrapper(input: Message, + llm: Annotated[LLMServiceExtensionServer, LLMServiceExtensionSpec.single_demand()], + trajectory: Annotated[TrajectoryExtensionServer, TrajectoryExtensionSpec()], + form: Annotated[FormExtensionServer, + form_extension_spec]): + """Sample email writer with Mellea backend.""" + + form_data = form.parse_form_response(message=input) + inputs = [form_data.values[key].value for key in form_data.values] # Extracting all of the user inputs from the form + llm_config = llm.data.llm_fulfillments.get("default") + #loop = asyncio.get_running_loop() + + backend = OpenAIBackend(model_id=llm_config.api_model, api_key=llm_config.api_key, base_url=llm_config.api_base, stream=True) + m = MelleaSession(backend) + + + sampling = await asyncio.to_thread(func, m, *inputs) + if sampling.success: + yield AgentMessage(text=sampling.value) + return + + #for idx in range(len(sampling.sample_generations)): + # yield trajectory.trajectory_metadata( + # title=f"Attempt {idx + 1}", content=f"Generating message...") + # validations = sampling.sample_validations[idx] # Print out validations + # status = "\n".join(f"{'✓' if bool(v) else '✗'} {getattr(r, 'description', str(r))}" for r, v in validations) + # yield trajectory.trajectory_metadata(title=f"✗ Attempt {idx + 1} failed", content=status) + + yield AgentMessage(text=sampling.value) + + server.run(host=os.getenv("HOST", "127.0.0.1"), port=int(os.getenv("PORT", 8000))) + + return wrapper + +@agentstack_app +def mellea_func(m: MelleaSession, sender: str, recipient, subject: str, topic: str) -> tuple[ModelOutputThunk, Context] | SamplingResult: + """ + Example email writing module that utilizes an IVR loop in Mellea to generate an email with a specific list of requirements. + Inputs: + sender: str + recipient: str + subject: str + topic: str + Output: + sampling: tuple[ModelOutputThunk, Context] | SamplingResult + """ + requirements = [ + req("Be formal."), + req("Be funny."), + req(f"Make sure that the email is from {sender}, is towards {recipient}, has {subject} as the subject, and is focused on {topic} as a topic"), + Requirement("Use less than 100 words.", + validation_fn=simple_validate(lambda o: len(o.split()) < 100)) + ] + description = f"Write an email from {sender}. Subject of email is {subject}. Name of recipient is {recipient}. Topic of email should be {topic}." + sampling = m.instruct(description=description, requirements=requirements, strategy=RejectionSamplingStrategy(loop_budget=3), return_sampling_results=True) + return sampling + + diff --git a/docs/examples/bee_agent.py b/docs/examples/bee_agent.py new file mode 100644 index 00000000..44e601d4 --- /dev/null +++ b/docs/examples/bee_agent.py @@ -0,0 +1,47 @@ +""" +Example use case for BeeAI integration: utilizing a Mellea program to write an email with an IVF loop. +Also demo of RangeType to demonstrate random selection of a integer from a given range +""" +import os +import asyncio +import sys +from typing import Annotated + +from mellea import MelleaSession, start_session +from mellea.stdlib.base import ChatContext, ModelOutputThunk + +from mellea.stdlib.sampling import RejectionSamplingStrategy +from mellea.stdlib.sampling.types import SamplingResult +from mellea.stdlib.sampling.base import Context +from mellea.stdlib.requirement import req, Requirement, simple_validate +#from cli.serve.bee_playform.types import RangeType +from bee_platform.bee_platform import bee_app + + +@bee_app +def mellea_func(m: MelleaSession, sender: str, recipient, subject: str, topic: str, sampling_iters : int = 3) -> tuple[ModelOutputThunk, Context] | SamplingResult: + """ + Example email writing module that utilizes an IVR loop in Mellea to generate an email with a specific list of requirements. + Inputs: + sender: str + recipient: str + subject: str + topic: str + Output: + sampling: tuple[ModelOutputThunk, Context] | SamplingResult + """ + requirements = [ + req("Be formal."), + req("Be funny."), + req(f"Make sure that the email is from {sender}, is towards {recipient}, has {subject} as the subject, and is focused on {topic} as a topic"), + Requirement("Use less than 100 words.", + validation_fn=simple_validate(lambda o: len(o.split()) < 100)) + ] + sampling = m.instruct(f"Write an email from {sender}. Subject of email is {subject}. Name of recipient is {recipient}. Topic of email should be {topic}.", requirements=requirements, strategy=RejectionSamplingStrategy(loop_budget=1), return_sampling_results=True) + + return sampling + + + + + diff --git a/mellea/stdlib/base.py b/mellea/stdlib/base.py index 111d44f6..de866764 100644 --- a/mellea/stdlib/base.py +++ b/mellea/stdlib/base.py @@ -18,7 +18,11 @@ from mellea.helpers.fancy_logger import FancyLogger - +def callback(input): + yield trajectory.trajectory_metadata( + title="Execution", + content=input + ) class CBlock: """A `CBlock` is a block of content that can serve as input to or output from an LLM.""" @@ -341,8 +345,13 @@ async def astream(self) -> str: if self._computed: assert self._post_process is not None await self._post_process(self) + + display_string = self._underlying_value - return self._underlying_value # type: ignore + if "yes" in display_string.lower() or "no" in display_string.lower(): + display_string = "Requirement result: " + display_string + print(display_string) + return self._underlying_value def __repr__(self): """Provides a python-parsable representation (usually). diff --git a/new_types.py b/new_types.py new file mode 100644 index 00000000..98395721 --- /dev/null +++ b/new_types.py @@ -0,0 +1,40 @@ +import random + +class RangeType: + """A custom range class that mimics the built-in range() behavior.""" + + def __init__(self, start, stop=None, step=1): + if step == 0: + raise ValueError("RangeType() arg 3 must not be zero") + + if stop is None: + self.start = 0 + self.stop = start + else: + self.start = start + self.stop = stop + + self.step = step + self.current = self.start + + def __iter__(self): + """Returns the iterator object.""" + self.current = self.start # Reset iterator for each new loop + return self + + def __next__(self): + """Returns the next value in the sequence.""" + if self.step > 0 and self.current >= self.stop: + raise StopIteration + if self.step < 0 and self.current <= self.stop: + raise StopIteration + + value = self.current + self.current += self.step + return value + + def __repr__(self): + """Official string representation of the object. Randomly selected age from a given interval.""" + + return str(random.choice(list(self))) +