|
1 | 1 | """Fan-out/fan-in with LLM-as-judge ranking aggregation. |
2 | 2 |
|
3 | 3 | Three creative agents with different personas (bold, minimalist, |
4 | | -emotional) each propose a marketing slogan. A formatter collects the |
5 | | -candidates, then a judge Agent scores and ranks them — letting the LLM |
6 | | -evaluate creativity, memorability, and brand fit. |
| 4 | +emotional) each propose a marketing slogan. A ranker Executor collects |
| 5 | +the candidates, formats them, and uses an internal judge Agent to score |
| 6 | +and rank them — letting the LLM evaluate creativity, memorability, and |
| 7 | +brand fit. |
7 | 8 |
|
8 | 9 | Aggregation technique: LLM-as-judge (generate N candidates, rank the best). |
9 | 10 |
|
|
16 | 17 | import os |
17 | 18 | import sys |
18 | 19 |
|
19 | | -from agent_framework import Agent, AgentExecutorResponse, Executor, WorkflowBuilder, WorkflowContext, handler |
| 20 | +from typing import Never |
| 21 | + |
| 22 | +from agent_framework import Agent, AgentExecutorResponse, Executor, Message, WorkflowBuilder, WorkflowContext, handler |
20 | 23 | from agent_framework.openai import OpenAIChatClient |
21 | 24 | from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider |
22 | 25 | from dotenv import load_dotenv |
23 | | -from typing_extensions import Never |
| 26 | + |
24 | 27 |
|
25 | 28 | load_dotenv(override=True) |
26 | 29 | API_HOST = os.getenv("API_HOST", "github") |
@@ -55,22 +58,37 @@ async def dispatch(self, prompt: str, ctx: WorkflowContext[str]) -> None: |
55 | 58 | await ctx.send_message(prompt) |
56 | 59 |
|
57 | 60 |
|
58 | | -class FormatCandidates(Executor): |
59 | | - """Fan-in aggregator that formats candidate slogans for the judge.""" |
| 61 | +class RankerExecutor(Executor): |
| 62 | + """Fan-in aggregator that formats candidate slogans and ranks them via the LLM client directly.""" |
| 63 | + |
| 64 | + def __init__(self, *, client: OpenAIChatClient, id: str = "Ranker") -> None: |
| 65 | + super().__init__(id=id) |
| 66 | + self._client = client |
60 | 67 |
|
61 | 68 | @handler |
62 | | - async def format( |
| 69 | + async def run( |
63 | 70 | self, |
64 | 71 | results: list[AgentExecutorResponse], |
65 | 72 | ctx: WorkflowContext[Never, str], |
66 | 73 | ) -> None: |
67 | | - """Collect slogans into a labeled list for the judge Agent.""" |
| 74 | + """Collect slogans, format them, and ask the LLM to rank them.""" |
68 | 75 | lines = [] |
69 | 76 | for result in results: |
70 | 77 | slogan = result.agent_response.text.strip().strip("\"'").split("\n")[0].strip().strip("\"'") |
71 | 78 | lines.append(f"- [{result.executor_id}]: \"{slogan}\"") |
72 | | - await ctx.send_message("Candidate slogans:\n" + "\n".join(lines)) |
73 | 79 |
|
| 80 | + messages = [ |
| 81 | + Message(role="system", text=( |
| 82 | + "You are a senior creative director judging marketing slogans. " |
| 83 | + "Given a list of candidate slogans, rank them from best to worst. " |
| 84 | + "For each slogan, give a 1-10 score and a one-sentence justification " |
| 85 | + "evaluating creativity, memorability, clarity, and brand fit. " |
| 86 | + 'Format: #1 (score X) [AgentName]: "slogan" — justification' |
| 87 | + )), |
| 88 | + Message(role="user", text="Candidate slogans:\n" + "\n".join(lines)), |
| 89 | + ] |
| 90 | + response = await self._client.get_response(messages) |
| 91 | + await ctx.yield_output(response.text) |
74 | 92 |
|
75 | 93 | dispatcher = DispatchPrompt(id="dispatcher") |
76 | 94 |
|
@@ -104,30 +122,19 @@ async def format( |
104 | 122 | ), |
105 | 123 | ) |
106 | 124 |
|
107 | | -ranker = FormatCandidates(id="ranker") |
108 | | - |
109 | | -judge = Agent( |
110 | | - client=client, |
111 | | - name="Judge", |
112 | | - instructions=( |
113 | | - "You are a senior creative director judging marketing slogans. " |
114 | | - "Given a list of candidate slogans, rank them from best to worst. " |
115 | | - "For each slogan, give a 1-10 score and a one-sentence justification " |
116 | | - "evaluating creativity, memorability, clarity, and brand fit. " |
117 | | - "Format: #1 (score X) [AgentName]: \"slogan\" — justification" |
118 | | - ), |
119 | | -) |
| 125 | +# The ranker Executor calls the LLM client directly to handle fan-in — |
| 126 | +# it formats the collected slogans and has the LLM rank them. |
| 127 | +ranker = RankerExecutor(client=client) |
120 | 128 |
|
121 | 129 | workflow = ( |
122 | 130 | WorkflowBuilder( |
123 | 131 | name="FanOutFanInRanked", |
124 | 132 | description="Generate slogans in parallel, then LLM-judge ranks them.", |
125 | 133 | start_executor=dispatcher, |
126 | | - output_executors=[judge], |
| 134 | + output_executors=[ranker], |
127 | 135 | ) |
128 | 136 | .add_fan_out_edges(dispatcher, [bold_writer, minimalist_writer, emotional_writer]) |
129 | 137 | .add_fan_in_edges([bold_writer, minimalist_writer, emotional_writer], ranker) |
130 | | - .add_edge(ranker, judge) |
131 | 138 | .build() |
132 | 139 | ) |
133 | 140 |
|
|
0 commit comments