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/bee_platform/__init__.py b/bee_platform/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bee_platform/bee_examples.md b/bee_platform/bee_examples.md new file mode 100644 index 00000000..2bfe5d8a --- /dev/null +++ b/bee_platform/bee_examples.md @@ -0,0 +1,48 @@ +# Mellea-BeeAI + +Mellea is a library for writing generative programs. +BeeAI Framework 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 BeeAI agentic interfaces with structured (form) inputs. + +We provide the example of an email writer. Only text inputs are currently supported. + + +# Initialization + +First, install BeeAI, instructions available here: https://framework.beeai.dev/introduction/quickstart +Then, add the BeeAI-sdk to your local environment. +```bash +uv add beeai-sdk +``` + +# Running the example + +Then, in order to run the example email writer, run: +```bash +uv run --with mellea docs/examples/bee_agent.py +``` + +In a separate terminal, either run +```bash +beeai run mellea_agent +``` + +OR open the UI and select the **mellea-agent**. + +```bash +beeai ui +``` + +# Creating your own examples + +To create your own BeeAI agent with Mellea, write a traditional program with Mellea. + +Ensure that the first parameter is an **m** object. + +Wrap your Mellea program with ```@bee_app```. + +Place your example in the ```docs/examples/``` folder. + + + diff --git a/bee_platform/bee_platform.py b/bee_platform/bee_platform.py new file mode 100644 index 00000000..6dfb306f --- /dev/null +++ b/bee_platform/bee_platform.py @@ -0,0 +1,88 @@ +import os +import asyncio +from typing import Annotated, Callable + +from a2a.types import Message +from a2a.utils.message import get_message_text +from beeai_sdk.server import Server +from beeai_sdk.a2a.types import AgentMessage +from beeai_sdk.a2a.extensions import ( + LLMServiceExtensionServer, LLMServiceExtensionSpec, + TrajectoryExtensionServer, TrajectoryExtensionSpec, + AgentDetail +) +from beeai_sdk.a2a.extensions.ui.form import ( + FormExtensionServer, FormExtensionSpec, FormRender, TextField +) +from mellea import MelleaSession, start_session +from mellea.backends.openai import OpenAIBackend +from mellea.stdlib.requirement import req, Requirement, simple_validate +from mellea.stdlib.sampling import RejectionSamplingStrategy +from mellea.stdlib.base import ChatContext +import inspect + + +def bee_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:-1] + print(params) + 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", detail=AgentDetail(interaction_mode="single-turn")) + + async def wrapper(input: Message, + llm: Annotated[LLMServiceExtensionServer, LLMServiceExtensionSpec.single_demand()], + trajectory: Annotated[TrajectoryExtensionServer, TrajectoryExtensionSpec()], + form: Annotated[FormExtensionServer, + form_extension_spec]): + + + 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") + + for i in range(2): #Fixed loop budget to two iterations + yield trajectory.trajectory_metadata( + title=f"Attempt {i + 1}/2", + content=f"Generating message...") + m = MelleaSession(OpenAIBackend( + model_id=llm_config.api_model, + api_key=llm_config.api_key, + base_url=llm_config.api_base + )) + + sampling = await asyncio.to_thread(func, m, *inputs) + + validations = sampling.sample_validations[0] + all_passed = all(bool(val_result) for _, val_result in validations) + + if all_passed: + yield trajectory.trajectory_metadata(title=f"✓ Attempt {i + 1} succeeded!") + yield AgentMessage(text=sampling.value) + return + + 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 {i + 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 + + 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/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/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))) +