85742ab165
Deploy Documentation / deploy (push) Has been cancelled
CPU Test / Test (Utilities, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (LLM proxy, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Others, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, latest, Python 3.13) (push) Has been cancelled
Dashboard / Chromatic (push) Has been cancelled
CPU Test / Lint - fast (push) Has been cancelled
CPU Test / Lint - next (push) Has been cancelled
CPU Test / Lint - slow (push) Has been cancelled
CPU Test / Lint - JavaScript (push) Has been cancelled
CPU Test / Build documentation (push) Has been cancelled
CPU Test / Test (AgentOps, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (LLM proxy, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Others, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Store, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (Weave, legacy, Python 3.10) (push) Has been cancelled
CPU Test / Test (AgentOps, stable, Python 3.11) (push) Has been cancelled
CPU Test / Test (Store, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Utilities, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (Weave, stable, Python 3.12) (push) Has been cancelled
CPU Test / Test (AgentOps, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (LLM proxy, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Others, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Store, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (Utilities, latest, Python 3.13) (push) Has been cancelled
CPU Test / Test (JavaScript) (push) Has been cancelled
186 lines
6.4 KiB
Python
186 lines
6.4 KiB
Python
# Copyright (c) Microsoft. All rights reserved.
|
|
|
|
"""This sample code shows how to run a custom algorithm and rollout runner separately.
|
|
|
|
You can run this in two modes:
|
|
|
|
1. Algorithm mode - runs the optimization algorithm:
|
|
```bash
|
|
python apo_custom_algorithm.py algo
|
|
```
|
|
|
|
2. Runner mode - runs the rollout runner:
|
|
```bash
|
|
python apo_custom_algorithm.py runner
|
|
```
|
|
|
|
To use both together, you need to run them in parallel along with the store:
|
|
```bash
|
|
agl store
|
|
python apo_custom_algorithm.py algo
|
|
python apo_custom_algorithm.py runner
|
|
```
|
|
|
|
Or use the integrated version in `apo_custom_algorithm_trainer.py`:
|
|
```bash
|
|
python apo_custom_algorithm_trainer.py
|
|
```
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
from typing import Optional, Sequence
|
|
|
|
from openai import AsyncOpenAI
|
|
from rich.console import Console
|
|
|
|
import agentlightning as agl
|
|
|
|
console = Console()
|
|
|
|
|
|
async def apo_algorithm(*, store: agl.LightningStore):
|
|
"""
|
|
An example of how a prompt optimization works.
|
|
"""
|
|
prompt_candidates = [
|
|
"You are a helpful assistant. {any_question}",
|
|
"You are a knowledgeable AI. {any_question}",
|
|
"You are a friendly chatbot. {any_question}",
|
|
]
|
|
|
|
prompt_and_rewards: list[tuple[str, float]] = []
|
|
|
|
algo_marker = "[bold red][Algo][/bold red]"
|
|
|
|
for prompt in prompt_candidates:
|
|
# 1. The optimization algorithm updates the prompt template
|
|
console.print(f"\n{algo_marker} Updating prompt template to: '{prompt}'")
|
|
resources: agl.NamedResources = {
|
|
# The "main_prompt" can be replaced with any name you like
|
|
# As long as the PromptTemplate type is used, the rollout function will recognize it
|
|
"main_prompt": agl.PromptTemplate(template=prompt, engine="f-string")
|
|
}
|
|
# How the resource is used fully depends on the client implementation.
|
|
await store.add_resources(resources)
|
|
|
|
# 2. The algorithm queues up a task from a dataset
|
|
console.print(f"{algo_marker} Queuing task for clients...")
|
|
rollout = await store.enqueue_rollout(
|
|
input="Explain why the sky appears blue using principles of light scattering in 100 words.", mode="train"
|
|
)
|
|
console.print(f"{algo_marker} Task '{rollout.rollout_id}' is now available for clients.")
|
|
|
|
# 3. The algorithm waits for clients to process the task
|
|
for _ in range(30): # Wait for at most 30 seconds
|
|
rollouts = await store.wait_for_rollouts(rollout_ids=[rollout.rollout_id], timeout=0.01)
|
|
if rollouts:
|
|
break
|
|
await asyncio.sleep(1.0)
|
|
else:
|
|
raise RuntimeError("Expected a completed rollout from the client, but got none.")
|
|
|
|
console.print(f"{algo_marker} Received Result: {rollouts[0]}")
|
|
if rollouts[0].status != "succeeded":
|
|
raise RuntimeError(f"Rollout {rollout.rollout_id} did not succeed. Status: {rollouts[0].status}")
|
|
spans = await store.query_spans(rollout.rollout_id)
|
|
|
|
# Logs LLM spans for debugging and inspection here
|
|
await log_llm_span(spans)
|
|
|
|
# 4. The algorithm records the final reward for sorting
|
|
final_reward = agl.find_final_reward(spans)
|
|
assert final_reward is not None, "Expected a final reward from the client."
|
|
console.print(f"{algo_marker} Final reward: {final_reward}")
|
|
prompt_and_rewards.append((prompt, final_reward))
|
|
|
|
console.print(f"\n[bold red][Algo][/bold red] All prompts and their rewards: {prompt_and_rewards}")
|
|
best_prompt = max(prompt_and_rewards, key=lambda x: x[1])
|
|
console.print(f"[bold red][Algo][/bold red] Best prompt found: '{best_prompt[0]}' with reward {best_prompt[1]}")
|
|
|
|
|
|
@agl.rollout
|
|
async def apo_rollout(task: str, prompt_template: agl.PromptTemplate) -> float:
|
|
# This relies on a public OpenAI service
|
|
client = AsyncOpenAI()
|
|
|
|
result = await client.chat.completions.create(
|
|
model="gpt-4.1-nano",
|
|
messages=[
|
|
{"role": "user", "content": prompt_template.format(any_question=task)},
|
|
],
|
|
)
|
|
|
|
text = result.choices[0].message.content
|
|
console.print(f"[bold yellow][Rollout][/bold yellow] LLM returned: {text}")
|
|
|
|
return await llm_judge(task, text)
|
|
|
|
|
|
async def log_llm_span(spans: Sequence[agl.Span]) -> None:
|
|
"""Logs the LLM related spans that records prompts and responses."""
|
|
for span in spans:
|
|
if "chat.completion" in span.name:
|
|
console.print(f"[bold green][LLM][/bold green] Span {span.span_id} ({span.name}): {span.attributes}")
|
|
|
|
|
|
async def llm_judge(task: str, output: Optional[str]) -> float:
|
|
client = AsyncOpenAI()
|
|
judge_prompt = f"""Evaluate how well the output fulfills the task.
|
|
Task: {task}
|
|
Output: {output}
|
|
You must be very critical and strict in your evaluation.
|
|
Return only a number between 0 and 1. No text, punctuation, or explanation."""
|
|
result = await client.chat.completions.create(
|
|
model="gpt-4.1-nano",
|
|
messages=[
|
|
{"role": "user", "content": judge_prompt},
|
|
],
|
|
temperature=0.0,
|
|
)
|
|
try:
|
|
content = result.choices[0].message.content
|
|
if content is None:
|
|
console.print(f"[bold blue][Judge][/bold blue] Judge returned no content: {result}")
|
|
return 0.0
|
|
score = float(content)
|
|
console.print(f"[bold blue][Judge][/bold blue] Judge returned score: {score}")
|
|
return score
|
|
except ValueError:
|
|
console.print(f"[bold blue][Judge][/bold blue] Error evaluating output: {result}")
|
|
return 0.0
|
|
|
|
|
|
async def apo_runner(*, store: agl.LightningStore):
|
|
"""
|
|
A runner that iteratively receives new rollout tasks from the store and executes them.
|
|
"""
|
|
|
|
runner = agl.LitAgentRunner[str](tracer=agl.AgentOpsTracer())
|
|
with runner.run_context(agent=apo_rollout, store=store):
|
|
await runner.iter()
|
|
|
|
|
|
async def main():
|
|
store = agl.LightningStoreClient("http://localhost:4747")
|
|
parser = argparse.ArgumentParser(description="Run APO custom algorithm in different modes")
|
|
parser.add_argument(
|
|
"mode", choices=["algo", "runner"], help="Mode to run: 'algo' for algorithm or 'runner' for rollout runner"
|
|
)
|
|
args = parser.parse_args()
|
|
|
|
try:
|
|
if args.mode == "algo":
|
|
# Run the algorithm mode
|
|
await apo_algorithm(store=store)
|
|
elif args.mode == "runner":
|
|
# Run the runner mode
|
|
await apo_runner(store=store)
|
|
finally:
|
|
await store.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
agl.setup_logging()
|
|
asyncio.run(main())
|