Skip to content

Commit 1ed953a

Browse files
committed
Add spanish translations
1 parent d906984 commit 1ed953a

10 files changed

Lines changed: 1365 additions & 4 deletions

examples/spanish/workflow_agents.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
# igual que las subclases de Executor en workflow_rag_ingest.py.
4949
writer = Agent(
5050
client=client,
51-
name="Escritor",
51+
name="Writer",
5252
instructions=(
5353
"Eres un escritor de contenido conciso. "
5454
"Escribe un artículo corto (2-3 párrafos) claro y atractivo sobre el tema del usuario. "
@@ -58,7 +58,7 @@
5858

5959
reviewer = Agent(
6060
client=client,
61-
name="Revisor",
61+
name="Reviewer",
6262
instructions=(
6363
"Eres un revisor de contenido reflexivo. "
6464
"Lee el borrador del escritor y ofrece retroalimentación específica y constructiva. "
@@ -76,7 +76,10 @@ async def main():
7676
prompt = "Escribe una publicación de LinkedIn de 2 frases: \"Por qué tu piloto de IA se ve bien, pero falla en producción.\""
7777
print(f"Prompt: {prompt}\n")
7878
events = await workflow.run(prompt)
79-
print(events.get_outputs())
79+
80+
for output in events.get_outputs():
81+
print("===== Salida =====")
82+
print(output)
8083

8184
if async_credential:
8285
await async_credential.close()
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
"""Fan-out/fan-in con agregación de ranking usando LLM como juez.
2+
3+
Tres agentes creativos con diferentes personalidades (audaz, minimalista,
4+
emocional) proponen cada uno un eslogan de marketing. Un ejecutor ranker
5+
recopila las opciones, las formatea y usa un agente juez interno para
6+
calificarlas y ordenarlas — dejando que el LLM evalúe creatividad,
7+
memorabilidad y encaje con la marca.
8+
9+
Técnica de agregación: LLM como juez (generar N candidatos y rankear el mejor).
10+
11+
Ejecutar:
12+
uv run examples/spanish/workflow_aggregator_ranked.py
13+
uv run examples/spanish/workflow_aggregator_ranked.py --devui (abre DevUI en http://localhost:8104)
14+
"""
15+
16+
import asyncio
17+
import os
18+
import sys
19+
20+
from typing import Never
21+
22+
from agent_framework import Agent, AgentExecutorResponse, Executor, Message, WorkflowBuilder, WorkflowContext, handler
23+
from agent_framework.openai import OpenAIChatClient
24+
from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider
25+
from dotenv import load_dotenv
26+
from pydantic import BaseModel, Field
27+
28+
29+
load_dotenv(override=True)
30+
API_HOST = os.getenv("API_HOST", "github")
31+
32+
# Configura el cliente de chat según el proveedor de API
33+
async_credential = None
34+
if API_HOST == "azure":
35+
async_credential = DefaultAzureCredential()
36+
token_provider = get_bearer_token_provider(async_credential, "https://cognitiveservices.azure.com/.default")
37+
client = OpenAIChatClient(
38+
base_url=f"{os.environ['AZURE_OPENAI_ENDPOINT']}/openai/v1/",
39+
api_key=token_provider,
40+
model_id=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT"],
41+
)
42+
elif API_HOST == "github":
43+
client = OpenAIChatClient(
44+
base_url="https://models.github.ai/inference",
45+
api_key=os.environ["GITHUB_TOKEN"],
46+
model_id=os.getenv("GITHUB_MODEL", "openai/gpt-5-mini"),
47+
)
48+
else:
49+
client = OpenAIChatClient(
50+
api_key=os.environ["OPENAI_API_KEY"], model_id=os.environ.get("OPENAI_MODEL", "gpt-5-mini")
51+
)
52+
53+
54+
class RankedSlogan(BaseModel):
55+
"""Una sola entrada de eslogan rankeada."""
56+
57+
rank: int = Field(description="Rank position, 1 = best.")
58+
agent_name: str = Field(description="Name of the agent that produced the slogan.")
59+
slogan: str = Field(description="The marketing slogan text.")
60+
score: int = Field(description="Score from 1 to 10.")
61+
justification: str = Field(description="One-sentence justification for the score.")
62+
63+
64+
class RankedSlogans(BaseModel):
65+
"""Salida tipada: una lista de eslóganes rankeados."""
66+
67+
rankings: list[RankedSlogan] = Field(description="Slogans ranked from best to worst.")
68+
69+
70+
class DispatchPrompt(Executor):
71+
"""Emite el brief del producto hacia abajo para el broadcast de fan-out."""
72+
73+
@handler
74+
async def dispatch(self, prompt: str, ctx: WorkflowContext[str]) -> None:
75+
await ctx.send_message(prompt)
76+
77+
78+
class RankerExecutor(Executor):
79+
"""Agregador fan-in que formatea eslóganes candidatos y los rankea vía el cliente LLM."""
80+
81+
def __init__(self, *, client: OpenAIChatClient, id: str = "Ranker") -> None:
82+
super().__init__(id=id)
83+
self._client = client
84+
85+
@handler
86+
async def run(
87+
self,
88+
results: list[AgentExecutorResponse],
89+
ctx: WorkflowContext[Never, RankedSlogans],
90+
) -> None:
91+
"""Recopila eslóganes, los formatea y le pide al LLM que los rankee."""
92+
lines = []
93+
for result in results:
94+
slogan = result.agent_response.text.strip().strip("\"'").split("\n")[0].strip().strip("\"'")
95+
lines.append(f"- [{result.executor_id}]: \"{slogan}\"")
96+
97+
messages = [
98+
Message(
99+
role="system",
100+
text=(
101+
"Eres un director creativo senior evaluando eslóganes de marketing. "
102+
"Dada una lista de eslóganes candidatos, ordénalos del mejor al peor. "
103+
"Para cada eslogan, da una puntuación de 1 a 10 y una justificación de una sola oración "
104+
"evaluando creatividad, memorabilidad, claridad y encaje con la marca."
105+
),
106+
),
107+
Message(role="user", text="Eslóganes candidatos:\n" + "\n".join(lines)),
108+
]
109+
response = await self._client.get_response(messages, options={"response_format": RankedSlogans})
110+
await ctx.yield_output(response.value)
111+
112+
113+
dispatcher = DispatchPrompt(id="dispatcher")
114+
115+
bold_writer = Agent(
116+
client=client,
117+
name="BoldWriter",
118+
instructions=(
119+
"Eres un copywriter audaz y dramático. "
120+
"Dado el brief del producto, propone UN eslogan de marketing contundente (máx. 10 palabras). "
121+
"Hazlo llamativo y con mucha confianza. Responde SOLO con el eslogan."
122+
),
123+
)
124+
125+
minimalist_writer = Agent(
126+
client=client,
127+
name="MinimalistWriter",
128+
instructions=(
129+
"Eres un copywriter minimalista que valora la brevedad por encima de todo. "
130+
"Dado el brief del producto, propone UN eslogan de marketing ultra-corto (máx. 6 palabras). "
131+
"Menos es más. Responde SOLO con el eslogan."
132+
),
133+
)
134+
135+
emotional_writer = Agent(
136+
client=client,
137+
name="EmotionalWriter",
138+
instructions=(
139+
"Eres un copywriter con enfoque empático. "
140+
"Dado el brief del producto, propone UN eslogan de marketing (máx. 10 palabras) "
141+
"que conecte emocionalmente con la audiencia. Responde SOLO con el eslogan."
142+
),
143+
)
144+
145+
# El ejecutor ranker llama directamente al cliente LLM para manejar el fan-in —
146+
# formatea los eslóganes recopilados y hace que el LLM los rankee.
147+
ranker = RankerExecutor(client=client)
148+
149+
workflow = (
150+
WorkflowBuilder(
151+
name="FanOutFanInRanked",
152+
description="Generate slogans in parallel, then LLM-judge ranks them.",
153+
start_executor=dispatcher,
154+
output_executors=[ranker],
155+
)
156+
.add_fan_out_edges(dispatcher, [bold_writer, minimalist_writer, emotional_writer])
157+
.add_fan_in_edges([bold_writer, minimalist_writer, emotional_writer], ranker)
158+
.build()
159+
)
160+
161+
162+
async def main() -> None:
163+
"""Ejecuta el pipeline de eslóganes e imprime los resultados rankeados."""
164+
prompt = "Bicicleta eléctrica económica para commuters urbanos. Confiable, accesible y verde."
165+
print(f"Brief del producto: {prompt}\n")
166+
167+
events = await workflow.run(prompt)
168+
for output in events.get_outputs():
169+
for entry in output.rankings:
170+
print(f"#{entry.rank} (puntaje {entry.score}) [{entry.agent_name}]: \"{entry.slogan}\"")
171+
print(f" {entry.justification}\n")
172+
173+
if async_credential:
174+
await async_credential.close()
175+
176+
177+
if __name__ == "__main__":
178+
if "--devui" in sys.argv:
179+
from agent_framework.devui import serve
180+
181+
serve(entities=[workflow], port=8104, auto_open=True)
182+
else:
183+
asyncio.run(main())
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
"""Fan-out/fan-in con agregación usando extracción estructurada.
2+
3+
Tres agentes entrevistadores (técnico, conductual, cultura) evalúan a un
4+
candidato. El ejecutor fan-in recopila sus evaluaciones, llama al LLM con
5+
response_format=CandidateReview y produce un modelo tipado de Pydantic —
6+
listo para código downstream, no prosa.
7+
8+
Técnica de agregación: extracción estructurada del LLM hacia un modelo tipado.
9+
10+
Ejecutar:
11+
uv run examples/spanish/workflow_aggregator_structured.py
12+
uv run examples/spanish/workflow_aggregator_structured.py --devui (abre DevUI en http://localhost:8102)
13+
"""
14+
15+
import asyncio
16+
import os
17+
import sys
18+
19+
from agent_framework import Agent, AgentExecutorResponse, Executor, Message, WorkflowBuilder, WorkflowContext, handler
20+
from agent_framework.openai import OpenAIChatClient
21+
from azure.identity.aio import DefaultAzureCredential, get_bearer_token_provider
22+
from dotenv import load_dotenv
23+
from pydantic import BaseModel, Field
24+
from typing_extensions import Literal, Never
25+
26+
load_dotenv(override=True)
27+
API_HOST = os.getenv("API_HOST", "github")
28+
29+
# Configura el cliente de chat según el proveedor de API
30+
async_credential = None
31+
if API_HOST == "azure":
32+
async_credential = DefaultAzureCredential()
33+
token_provider = get_bearer_token_provider(async_credential, "https://cognitiveservices.azure.com/.default")
34+
client = OpenAIChatClient(
35+
base_url=f"{os.environ['AZURE_OPENAI_ENDPOINT']}/openai/v1/",
36+
api_key=token_provider,
37+
model_id=os.environ["AZURE_OPENAI_CHAT_DEPLOYMENT"],
38+
)
39+
elif API_HOST == "github":
40+
client = OpenAIChatClient(
41+
base_url="https://models.github.ai/inference",
42+
api_key=os.environ["GITHUB_TOKEN"],
43+
model_id=os.getenv("GITHUB_MODEL", "openai/gpt-5-mini"),
44+
)
45+
else:
46+
client = OpenAIChatClient(
47+
api_key=os.environ["OPENAI_API_KEY"], model_id=os.environ.get("OPENAI_MODEL", "gpt-5-mini")
48+
)
49+
50+
51+
class CandidateReview(BaseModel):
52+
"""Salida tipada producida por el revisor — útil para APIs, bases de datos o motores de scoring."""
53+
54+
technical_score: int = Field(description="Technical skills score from 1 to 10.")
55+
technical_reason: str = Field(description="Justification for the technical score.")
56+
behavioral_score: int = Field(description="Behavioral skills score from 1 to 10.")
57+
behavioral_reason: str = Field(description="Justification for the behavioral score.")
58+
recommendation: Literal["strong hire", "hire with reservations", "no hire"] = Field(
59+
description="Final hiring recommendation."
60+
)
61+
62+
63+
class DispatchPrompt(Executor):
64+
"""Emite la descripción del candidato hacia abajo para el broadcast de fan-out."""
65+
66+
@handler
67+
async def dispatch(self, prompt: str, ctx: WorkflowContext[str]) -> None:
68+
await ctx.send_message(prompt)
69+
70+
71+
class ExtractReview(Executor):
72+
"""Agregador fan-in que llama al LLM con response_format para producir un CandidateReview tipado."""
73+
74+
def __init__(self, *, client: OpenAIChatClient, **kwargs: object) -> None:
75+
super().__init__(**kwargs)
76+
self._client = client
77+
78+
@handler
79+
async def extract(
80+
self,
81+
results: list[AgentExecutorResponse],
82+
ctx: WorkflowContext[Never, CandidateReview],
83+
) -> None:
84+
"""Recopila evaluaciones de entrevistadores y le pide al LLM una revisión estructurada."""
85+
sections = []
86+
for result in results:
87+
label = result.executor_id.replace("_", " ").title()
88+
sections.append(f"[{label}]\n{result.agent_response.text}")
89+
combined = "\n\n".join(sections)
90+
91+
messages = [
92+
Message(
93+
role="system",
94+
text=(
95+
"Eres un revisor de un comité de contratación. "
96+
"Con base en las siguientes evaluaciones de entrevistadores, produce una revisión estructurada del candidato."
97+
),
98+
),
99+
Message(role="user", text=combined),
100+
]
101+
response = await self._client.get_response(messages, options={"response_format": CandidateReview})
102+
review: CandidateReview = response.value
103+
await ctx.yield_output(review)
104+
105+
106+
dispatcher = DispatchPrompt(id="dispatcher")
107+
108+
technical_interviewer = Agent(
109+
client=client,
110+
name="TechnicalInterviewer",
111+
instructions=(
112+
"Eres un ingeniero senior haciendo una entrevista técnica. "
113+
"Evalúa las habilidades técnicas del candidato, su conocimiento de arquitectura y su capacidad de programar. "
114+
"Sé específico sobre fortalezas y brechas. Usa viñetas cortas."
115+
),
116+
)
117+
118+
behavioral_interviewer = Agent(
119+
client=client,
120+
name="BehavioralInterviewer",
121+
instructions=(
122+
"Eres un especialista de RR.HH. haciendo una entrevista conductual. "
123+
"Evalúa comunicación, trabajo en equipo, resolución de conflictos y liderazgo. "
124+
"Sé específico sobre fortalezas y brechas. Usa viñetas cortas."
125+
),
126+
)
127+
128+
cultural_interviewer = Agent(
129+
client=client,
130+
name="CulturalInterviewer",
131+
instructions=(
132+
"Eres un líder de equipo evaluando encaje cultural. "
133+
"Evalúa si el candidato se alinea con una cultura startup colaborativa y de ritmo rápido. "
134+
"Sé específico sobre fortalezas y brechas. Usa viñetas cortas."
135+
),
136+
)
137+
138+
extractor = ExtractReview(client=client, id="extractor")
139+
140+
workflow = (
141+
WorkflowBuilder(
142+
name="FanOutFanInStructured",
143+
description="Fan-out/fan-in with Pydantic structured extraction.",
144+
start_executor=dispatcher,
145+
output_executors=[extractor],
146+
)
147+
.add_fan_out_edges(dispatcher, [technical_interviewer, behavioral_interviewer, cultural_interviewer])
148+
.add_fan_in_edges([technical_interviewer, behavioral_interviewer, cultural_interviewer], extractor)
149+
.build()
150+
)
151+
152+
153+
async def main() -> None:
154+
"""Ejecuta el pipeline de entrevistas e imprime la revisión tipada."""
155+
prompt = (
156+
"Candidato aplicando a Senior Software Engineer. "
157+
"5 años de experiencia en Python y sistemas distribuidos. "
158+
"Gran comunicador pero con experiencia limitada en cloud."
159+
)
160+
print(f"Brief del candidato: {prompt}\n")
161+
162+
events = await workflow.run(prompt)
163+
for output in events.get_outputs():
164+
print(f"Recomendación: {output.recommendation}\n")
165+
print(f"Técnico: {output.technical_score}/10 — {output.technical_reason}\n")
166+
print(f"Conductual: {output.behavioral_score}/10 — {output.behavioral_reason}")
167+
168+
if async_credential:
169+
await async_credential.close()
170+
171+
172+
if __name__ == "__main__":
173+
if "--devui" in sys.argv:
174+
from agent_framework.devui import serve
175+
176+
serve(entities=[workflow], port=8102, auto_open=True)
177+
else:
178+
asyncio.run(main())

0 commit comments

Comments
 (0)