e768098d0e
Flake8 Lint / flake8 (push) Waiting to run
Spell check CI / Spell_Check (push) Waiting to run
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
215 lines
8.7 KiB
Python
215 lines
8.7 KiB
Python
"""Tests for question-simulation MAF workflow.
|
|
|
|
Runs the workflow end-to-end against Azure OpenAI and verifies output format
|
|
and behaviour for both STOP and CONTINUE conversation scenarios.
|
|
|
|
Requires AZURE_OPENAI_ENDPOINT, AZURE_OPENAI_API_KEY, AZURE_OPENAI_DEPLOYMENT
|
|
to be set (via .env or environment).
|
|
"""
|
|
|
|
import asyncio
|
|
|
|
from workflow import QuestionSimInput, create_workflow
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Sample inputs and expected outputs
|
|
# ---------------------------------------------------------------------------
|
|
|
|
# Case 1: Multi-turn conversation that should CONTINUE — the human keeps asking
|
|
# substantive follow-up questions, the bot gives rich answers with more to explore.
|
|
CONTINUE_INPUT = QuestionSimInput(
|
|
chat_history=[
|
|
{
|
|
"inputs": {"question": "Can you introduce something about large language model?"},
|
|
"outputs": {
|
|
"answer": (
|
|
"A large language model (LLM) is a type of language model that is distinguished "
|
|
"by its ability to perform general-purpose language generation and understanding. "
|
|
"These models learn statistical relationships from text documents through a "
|
|
"self-supervised and semi-supervised training process that is computationally "
|
|
"intensive. LLMs are artificial neural networks, the largest and most capable "
|
|
"of which are built with a transformer-based architecture. Some recent "
|
|
"implementations are based on other architectures, such as recurrent neural "
|
|
"network variants and Mamba. LLMs can be used for text generation, a form of "
|
|
"generative AI, by taking an input text and repeatedly predicting the next "
|
|
"token or word. Notable examples include OpenAI's GPT series, Google's PaLM "
|
|
"and Gemini, Meta's LLaMA family, and Anthropic's Claude models."
|
|
),
|
|
},
|
|
},
|
|
{
|
|
"inputs": {"question": "What is the transformer architecture you mentioned?"},
|
|
"outputs": {
|
|
"answer": (
|
|
"The transformer is a deep learning architecture introduced in the 2017 "
|
|
"paper 'Attention Is All You Need' by Google researchers. It uses self-attention "
|
|
"mechanisms to process sequences in parallel, making it much faster to train "
|
|
"than previous recurrent models. The key innovation is multi-head attention, "
|
|
"which allows the model to focus on different parts of the input simultaneously."
|
|
),
|
|
},
|
|
},
|
|
],
|
|
question_count=3,
|
|
)
|
|
|
|
# Case 2: Conversation that should STOP — the human said thanks with no question,
|
|
# the bot replied with just a polite closing.
|
|
STOP_INPUT = QuestionSimInput(
|
|
chat_history=[
|
|
{
|
|
"inputs": {"question": "Thanks for the info. I'll look into it."},
|
|
"outputs": {
|
|
"answer": "You're welcome! If you need anything else, feel free to ask.",
|
|
},
|
|
}
|
|
],
|
|
question_count=3,
|
|
)
|
|
|
|
# Case 3: Multi-turn conversation that should CONTINUE.
|
|
MULTI_TURN_INPUT = QuestionSimInput(
|
|
chat_history=[
|
|
{
|
|
"inputs": {"question": "What is machine learning?"},
|
|
"outputs": {
|
|
"answer": (
|
|
"Machine learning is a subset of artificial intelligence that enables "
|
|
"systems to learn and improve from experience without being explicitly "
|
|
"programmed. It focuses on developing algorithms that can access data "
|
|
"and use it to learn for themselves."
|
|
),
|
|
},
|
|
},
|
|
{
|
|
"inputs": {"question": "What are the main types of machine learning?"},
|
|
"outputs": {
|
|
"answer": (
|
|
"There are three main types: supervised learning, unsupervised learning, "
|
|
"and reinforcement learning. Supervised learning uses labeled data, "
|
|
"unsupervised learning finds patterns in unlabeled data, and reinforcement "
|
|
"learning learns through trial and error with rewards."
|
|
),
|
|
},
|
|
},
|
|
],
|
|
question_count=2,
|
|
)
|
|
|
|
# Case 4: Single question requested — multi-turn to ensure CONTINUE.
|
|
SINGLE_Q_INPUT = QuestionSimInput(
|
|
chat_history=[
|
|
{
|
|
"inputs": {"question": "What is the transformer architecture?"},
|
|
"outputs": {
|
|
"answer": (
|
|
"The transformer is a deep learning architecture introduced in the 2017 "
|
|
"paper 'Attention Is All You Need' by Google researchers at NeurIPS. It "
|
|
"uses self-attention mechanisms to process sequences in parallel, making "
|
|
"it much faster to train than previous recurrent models like LSTMs."
|
|
),
|
|
},
|
|
},
|
|
{
|
|
"inputs": {"question": "How does self-attention work in transformers?"},
|
|
"outputs": {
|
|
"answer": (
|
|
"Self-attention computes a weighted sum of all positions in a sequence. "
|
|
"For each token, it creates query, key, and value vectors. The attention "
|
|
"score between two tokens is the dot product of the query of one with the "
|
|
"key of the other. These scores determine how much each token attends to "
|
|
"every other token, enabling the model to capture long-range dependencies."
|
|
),
|
|
},
|
|
},
|
|
],
|
|
question_count=1,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper: run a single test case
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def run_test(name: str, sim_input: QuestionSimInput, expect_stop: bool):
|
|
"""Run the workflow and verify the output."""
|
|
print(f"\n{'='*60}")
|
|
print(f"TEST: {name}")
|
|
print(f" chat_history turns: {len(sim_input.chat_history)}")
|
|
print(f" question_count: {sim_input.question_count}")
|
|
print(f" expect_stop: {expect_stop}")
|
|
print(f"{'='*60}")
|
|
|
|
workflow = create_workflow()
|
|
result = await workflow.run(sim_input)
|
|
output = result.get_outputs()[0]
|
|
print(f" Output:\n {output!r}")
|
|
|
|
errors = []
|
|
|
|
# Basic type check
|
|
if not isinstance(output, str):
|
|
errors.append(f"Expected str output, got {type(output).__name__}")
|
|
|
|
if expect_stop:
|
|
# STOP case: output must be exactly "[STOP]"
|
|
if output != "[STOP]":
|
|
errors.append(f"Expected '[STOP]', got: {output!r}")
|
|
else:
|
|
# CONTINUE case: output must NOT be "[STOP]"
|
|
if output == "[STOP]":
|
|
errors.append("Expected generated questions but got '[STOP]'")
|
|
# Should contain at least one non-empty line
|
|
lines = [line.strip() for line in output.split("\n") if line.strip()]
|
|
if len(lines) < 1:
|
|
errors.append("Expected at least 1 generated question, got empty output")
|
|
# Number of questions should match question_count
|
|
if len(lines) != sim_input.question_count:
|
|
# This is a soft check — LLM may occasionally return fewer
|
|
print(f" WARNING: Expected {sim_input.question_count} questions, got {len(lines)}")
|
|
|
|
if errors:
|
|
for e in errors:
|
|
print(f" FAIL: {e}")
|
|
return False
|
|
else:
|
|
print(" PASS")
|
|
return True
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Main
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def main():
|
|
test_cases = [
|
|
("continue_single_turn", CONTINUE_INPUT, False),
|
|
("stop_polite_close", STOP_INPUT, True),
|
|
("continue_multi_turn", MULTI_TURN_INPUT, False),
|
|
("continue_single_question", SINGLE_Q_INPUT, False),
|
|
]
|
|
|
|
results = {}
|
|
for name, sim_input, expect_stop in test_cases:
|
|
passed = await run_test(name, sim_input, expect_stop)
|
|
results[name] = passed
|
|
|
|
# Summary
|
|
print(f"\n{'='*60}")
|
|
print("SUMMARY")
|
|
print(f"{'='*60}")
|
|
total = len(results)
|
|
passed = sum(1 for v in results.values() if v)
|
|
for name, ok in results.items():
|
|
status = "PASS" if ok else "FAIL"
|
|
print(f" [{status}] {name}")
|
|
print(f"\n {passed}/{total} passed")
|
|
|
|
if passed < total:
|
|
raise SystemExit(1)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|