e768098d0e
tools_continuous_delivery / Private PyPI non-main branch release (push) Has been skipped
tools_continuous_delivery / Private PyPI main branch release (push) Failing after 2m42s
Publish Promptflow Doc / Build (push) Has been cancelled
Publish Promptflow Doc / Deploy (push) Has been cancelled
Flake8 Lint / flake8 (push) Has been cancelled
Spell check CI / Spell_Check (push) Has been cancelled
202 lines
7.8 KiB
Python
202 lines
7.8 KiB
Python
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
from dataclasses import dataclass, field
|
|
|
|
from dotenv import load_dotenv
|
|
from typing_extensions import Never
|
|
|
|
from agent_framework import Agent, Executor, WorkflowBuilder, WorkflowContext, handler
|
|
from agent_framework.openai import OpenAIChatClient
|
|
|
|
load_dotenv()
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Variant system prompts (converted from .jinja2 templates)
|
|
# ---------------------------------------------------------------------------
|
|
|
|
VARIANT_0_INSTRUCTIONS = (
|
|
"You are an assistant to calculate the answer to the provided math problems.\n"
|
|
"Please return the final numerical answer only, without any accompanying reasoning or explanation."
|
|
)
|
|
|
|
VARIANT_1_INSTRUCTIONS = """\
|
|
You are an assistant to calculate the answer to the provided math problems.
|
|
Please think step by step.
|
|
Return the final numerical answer only and any accompanying reasoning or explanation seperately as json format.
|
|
|
|
Here are some examples:
|
|
|
|
User: A jar contains two red marbles, three green marbles, ten white marbles \
|
|
and no other marbles. Two marbles are randomly drawn from this jar without \
|
|
replacement. What is the probability that these two marbles drawn will both \
|
|
be red? Express your answer as a common fraction.
|
|
Assistant: {"Chain of thought": "The total number of marbles is 2+3+10=15. \
|
|
The probability that the first marble drawn will be red is 2/15. Then, there \
|
|
will be one red left, out of 14. Therefore, the probability of drawing out \
|
|
two red marbles will be: (2/15)*(1/14) = 1/105.", "answer": "1/105"}
|
|
|
|
User: Find the greatest common divisor of 7! and (5!)^2.
|
|
Assistant: {"Chain of thought": "7! = 5040 = 2^4 * 3^2 * 5 * 7. \
|
|
(5!)^2 = 14400 = 2^6 * 3^2 * 5^2. gcd = 2^4 * 3^2 * 5 = 720.", \
|
|
"answer": "720"}"""
|
|
|
|
VARIANT_2_INSTRUCTIONS = """\
|
|
You are an assistant to calculate the answer to the provided math problems.
|
|
Please think step by step.
|
|
Return the final numerical answer only and any accompanying reasoning or explanation seperately as json format.
|
|
|
|
Here are some examples:
|
|
|
|
User: A jar contains two red marbles, three green marbles, ten white marbles \
|
|
and no other marbles. Two marbles are randomly drawn from this jar without \
|
|
replacement. What is the probability that these two marbles drawn will both \
|
|
be red? Express your answer as a common fraction.
|
|
Assistant: {"Chain of thought": "The total number of marbles is 2+3+10=15. \
|
|
The probability that the first marble drawn will be red is 2/15. Then, there \
|
|
will be one red left, out of 14. Therefore, the probability of drawing out \
|
|
two red marbles will be: (2/15)*(1/14) = 1/105.", "answer": "1/105"}
|
|
|
|
User: Find the greatest common divisor of 7! and (5!)^2.
|
|
Assistant: {"Chain of thought": "7! = 5040 = 2^4 * 3^2 * 5 * 7. \
|
|
(5!)^2 = 14400 = 2^6 * 3^2 * 5^2. gcd = 2^4 * 3^2 * 5 = 720.", \
|
|
"answer": "720"}
|
|
|
|
User: A club has 10 members, 5 boys and 5 girls. Two of the members \
|
|
are chosen at random. What is the probability that they are both girls?
|
|
Assistant: {"Chain of thought": "There are C(10,2) = 45 ways to choose \
|
|
two members, and C(5,2) = 10 ways to choose two girls. Therefore, \
|
|
the probability is 10/45 = 2/9.", "answer": "2/9"}
|
|
|
|
User: Allison, Brian and Noah each have a 6-sided cube. All of the faces \
|
|
on Allison's cube have a 5. The faces on Brian's cube are numbered \
|
|
1, 2, 3, 4, 5 and 6. Three of the faces on Noah's cube have a 2 and \
|
|
three of the faces have a 6. All three cubes are rolled. What is the \
|
|
probability that Allison's roll is greater than each of Brian's and Noah's?
|
|
Assistant: {"Chain of thought": "Allison always rolls 5. Brian rolls 4 \
|
|
or lower with probability 4/6 = 2/3. Noah rolls 2 (less than 5) with \
|
|
probability 3/6 = 1/2. So the probability is 2/3 * 1/2 = 1/3.", \
|
|
"answer": "1/3"}
|
|
|
|
User: Compute C(50,2).
|
|
Assistant: {"Chain of thought": "C(50,2) = 50!/(2!48!) = (50*49)/(2*1) \
|
|
= 1225.", "answer": "1225"}
|
|
|
|
User: The set S = {1, 2, 3, ..., 49, 50} contains the first 50 positive \
|
|
integers. After the multiples of 2 and the multiples of 3 are removed, \
|
|
how many integers remain in the set S?
|
|
Assistant: {"Chain of thought": "25 even numbers removed, leaving 25 odd \
|
|
numbers. Remove odd multiples of 3: 3,9,15,21,27,33,39,45 = 8 numbers. \
|
|
25 - 8 = 17.", "answer": "17"}"""
|
|
|
|
VARIANT_INSTRUCTIONS = {
|
|
"variant_0": VARIANT_0_INSTRUCTIONS,
|
|
"variant_1": VARIANT_1_INSTRUCTIONS,
|
|
"variant_2": VARIANT_2_INSTRUCTIONS,
|
|
}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Data classes
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
@dataclass
|
|
class ChatInput:
|
|
question: str
|
|
chat_history: list = field(default_factory=list)
|
|
variant: str = "variant_0"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Executors
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class InputExecutor(Executor):
|
|
"""Formats the chat history and question into a single prompt string."""
|
|
|
|
@handler
|
|
async def receive(self, chat_input: ChatInput, ctx: WorkflowContext[str]) -> None:
|
|
parts: list[str] = []
|
|
if chat_input.chat_history:
|
|
for turn in chat_input.chat_history:
|
|
parts.append(f"User: {turn['inputs']['question']}")
|
|
parts.append(f"Assistant: {turn['outputs']['answer']}")
|
|
parts.append(chat_input.question)
|
|
await ctx.send_message("\n".join(parts))
|
|
|
|
|
|
class ChatExecutor(Executor):
|
|
"""Calls the LLM with the selected variant instructions."""
|
|
|
|
def __init__(self, variant: str = "variant_0", **kwargs):
|
|
super().__init__(**kwargs)
|
|
client = OpenAIChatClient(
|
|
azure_endpoint=os.environ["AZURE_OPENAI_ENDPOINT"],
|
|
model=os.environ.get("AZURE_OPENAI_DEPLOYMENT", "gpt-4"),
|
|
api_key=os.environ["AZURE_OPENAI_API_KEY"],
|
|
)
|
|
self._agent = Agent(
|
|
client=client,
|
|
name="ChatMathAgent",
|
|
instructions=VARIANT_INSTRUCTIONS[variant],
|
|
)
|
|
|
|
@handler
|
|
async def call_llm(self, prompt: str, ctx: WorkflowContext[str]) -> None:
|
|
response = await self._agent.run(prompt)
|
|
await ctx.send_message(response.text)
|
|
|
|
|
|
class ExtractResultExecutor(Executor):
|
|
"""Extracts the final answer from the LLM response (mirrors extract_result.py)."""
|
|
|
|
@handler
|
|
async def extract(self, llm_output: str, ctx: WorkflowContext[Never, str]) -> None:
|
|
cleaned = re.sub(r'[$\\!]', '', llm_output)
|
|
try:
|
|
json_answer = json.loads(cleaned)
|
|
answer = json_answer["answer"]
|
|
except Exception:
|
|
answer = cleaned
|
|
await ctx.yield_output(answer)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Workflow builder
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def build_workflow(variant: str = "variant_0"):
|
|
"""Build the chat-math workflow for the given variant."""
|
|
_input = InputExecutor(id="input")
|
|
_chat = ChatExecutor(id="chat", variant=variant)
|
|
_extract = ExtractResultExecutor(id="extract_result")
|
|
|
|
return (
|
|
WorkflowBuilder(name="ChatMathVariantWorkflow", start_executor=_input)
|
|
.add_edge(_input, _chat)
|
|
.add_edge(_chat, _extract)
|
|
.build()
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
async def main():
|
|
variant = os.environ.get("CHAT_VARIANT", "variant_0")
|
|
workflow = build_workflow(variant)
|
|
|
|
result = await workflow.run(
|
|
ChatInput(question="1+1=?", variant=variant)
|
|
)
|
|
print("Answer:", result.get_outputs()[0])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|