Python: [BREAKING] Types API Review improvements (#3647)

* Replace Role and FinishReason classes with NewType + Literal

- Remove EnumLike metaclass from _types.py
- Replace Role class with NewType('Role', str) + RoleLiteral
- Replace FinishReason class with NewType('FinishReason', str) + FinishReasonLiteral
- Update all usages across codebase to use string literals
- Remove .value access patterns (direct string comparison now works)
- Add backward compatibility for legacy dict serialization format
- Update tests to reflect new string-based types

Addresses #3591, #3615

* Simplify ChatResponse and AgentResponse type hints (#3592)

- Remove overloads from ChatResponse.__init__
- Remove text parameter from ChatResponse.__init__
- Remove | dict[str, Any] from finish_reason and usage_details params
- Remove **kwargs from AgentResponse.__init__
- Both now accept ChatMessage | Sequence[ChatMessage] | None for messages
- Update docstrings and examples to reflect changes
- Fix tests that were using removed kwargs
- Fix Role type hint usage in ag-ui utils

* Remove text parameter from ChatResponseUpdate and AgentResponseUpdate (#3597)

- Remove text parameter from ChatResponseUpdate.__init__
- Remove text parameter from AgentResponseUpdate.__init__
- Remove **kwargs from both update classes
- Simplify contents parameter type to Sequence[Content] | None
- Update all usages to use contents=[Content.from_text(...)] pattern
- Fix imports in test files
- Update docstrings and examples

* Rename from_chat_response_updates to from_updates (#3593)

- ChatResponse.from_chat_response_updates → ChatResponse.from_updates
- ChatResponse.from_chat_response_generator → ChatResponse.from_update_generator
- AgentResponse.from_agent_run_response_updates → AgentResponse.from_updates

* Remove try_parse_value method from ChatResponse and AgentResponse (#3595)

- Remove try_parse_value method from ChatResponse
- Remove try_parse_value method from AgentResponse
- Remove try_parse_value calls from from_updates and from_update_generator methods
- Update samples to use try/except with response.value instead
- Update tests to use response.value pattern
- Users should now use response.value with try/except for safe parsing

* Add agent_id to AgentResponse and clarify author_name documentation (#3596)

- Add agent_id parameter to AgentResponse class
- Document that author_name is on ChatMessage objects, not responses
- Update ChatResponse docstring with author_name note
- Update AgentResponse docstring with author_name note

* Simplify ChatMessage.__init__ signature (#3618)

- Make contents a positional argument accepting Sequence[Content | str]
- Auto-convert strings in contents to TextContent
- Remove overloads, keep text kwarg for backward compatibility with serialization
- Update _parse_content_list to handle string items
- Update all usages across codebase to use new format: ChatMessage("role", ["text"])

* Allow Content as input on run and get_response

- Update prepare_messages and normalize_messages to accept Content
- Update type signatures in _agents.py and _clients.py
- Add tests for Content input handling

* Fix ChatMessage usage across packages and samples

Update all remaining ChatMessage(role=..., text=...) to use new
ChatMessage('role', ['text']) signature.

* Fix Role string usage and response format parsing

- Fix redis provider: remove .value access on string literals
- Fix durabletask ensure_response_format: set _response_format before accessing .value

* Fix ollama .value and ai_model_id issues, handle None in content list

- Fix ollama _chat_client: remove .value on string literals
- Fix ollama _chat_client: rename ai_model_id to model_id
- Fix _parse_content_list: skip None values gracefully

* Fix A2AAgent type signature to include Content

* Fix Role/FinishReason NewType dict annotations and improve test coverage to 95%

* Fix mypy errors for Role/FinishReason NewType usage

* Fix Role.TOOL and Role.ASSISTANT usage in _orchestrator_helpers.py

* Fix Role NewType usage in durabletask _models.py
This commit is contained in:
Eduard van Valkenburg
2026-02-04 11:13:23 +01:00
committed by GitHub
Unverified
parent ef798629e5
commit 838a7fd61d
341 changed files with 3766 additions and 3228 deletions
@@ -1,12 +1,17 @@
# Copyright (c) Microsoft. All rights reserved.
# type: ignore
import argparse
import asyncio
import os
import time
import argparse
import pandas as pd
import openai
from typing import Any
import openai
import pandas as pd
from agent_framework import ChatAgent, ChatMessage
from agent_framework.azure import AzureOpenAIChatClient
from azure.ai.projects import AIProjectClient
from azure.identity import AzureCliCredential
from dotenv import load_dotenv
from openai.types.eval_create_params import DataSourceConfigCustom
from openai.types.evals.create_eval_jsonl_run_data_source_param import (
@@ -15,11 +20,6 @@ from openai.types.evals.create_eval_jsonl_run_data_source_param import (
SourceFileContentContent,
)
from agent_framework import ChatAgent, ChatMessage
from agent_framework.azure import AzureOpenAIChatClient
from azure.ai.projects import AIProjectClient
from azure.identity import AzureCliCredential
"""
Self-Reflection LLM Runner
@@ -122,7 +122,7 @@ def run_eval(
if run.status == "failed":
print(f"Eval run failed. Run ID: {run.id}, Status: {run.status}, Error: {getattr(run, 'error', 'Unknown error')}")
continue
elif run.status == "completed":
if run.status == "completed":
output_items = list(client.evals.runs.output_items.list(run_id=run.id, eval_id=eval_object.id))
return output_items
time.sleep(5)
@@ -162,7 +162,7 @@ async def execute_query_with_self_reflection(
- total_groundedness_eval_time: Time spent on evaluations (seconds)
- total_end_to_end_time: Total execution time (seconds)
"""
messages = [ChatMessage(role="user", text=full_user_query)]
messages = [ChatMessage("user", [full_user_query])]
best_score = 0
max_score = 5
@@ -174,8 +174,8 @@ async def execute_query_with_self_reflection(
iteration_scores = [] # Store all iteration scores in structured format
for i in range(max_self_reflections):
print(f" Self-reflection iteration {i+1}/{max_self_reflections}...")
print(f" Self-reflection iteration {i + 1}/{max_self_reflections}...")
raw_response = await agent.run(messages=messages)
agent_response = raw_response.text
@@ -189,7 +189,7 @@ async def execute_query_with_self_reflection(
context=context,
)
if eval_run_output_items is None:
print(f" ⚠️ Groundedness evaluation failed (timeout or error) for iteration {i+1}.")
print(f" ⚠️ Groundedness evaluation failed (timeout or error) for iteration {i + 1}.")
continue
score = eval_run_output_items[0].results[0].score
end_time_eval = time.time()
@@ -209,21 +209,21 @@ async def execute_query_with_self_reflection(
best_response = agent_response
best_iteration = i + 1
if score == max_score:
print(f" ✓ Perfect groundedness score achieved!")
print(" ✓ Perfect groundedness score achieved!")
break
else:
print(f" → No improvement (score: {score}/{max_score}). Trying again...")
# Add to conversation history
messages.append(ChatMessage(role="assistant", text=agent_response))
messages.append(ChatMessage("assistant", [agent_response]))
# Request improvement
reflection_prompt = (
f"The groundedness score of your response is {score}/{max_score}. "
f"Reflect on your answer and improve it to get the maximum score of {max_score} "
)
messages.append(ChatMessage(role="user", text=reflection_prompt))
messages.append(ChatMessage("user", [reflection_prompt]))
end_time = time.time()
latency = end_time - start_time
@@ -290,46 +290,46 @@ async def run_self_reflection_batch(
print(f"Processing first {len(df)} prompts (limited by -n {limit})")
# Validate required columns
required_columns = ['system_instruction', 'user_request', 'context_document',
'full_prompt', 'domain', 'type', 'high_level_type']
required_columns = ["system_instruction", "user_request", "context_document",
"full_prompt", "domain", "type", "high_level_type"]
missing_columns = [col for col in required_columns if col not in df.columns]
if missing_columns:
raise ValueError(f"Input file missing required columns: {missing_columns}")
# Configure clients
print(f"Configuring Azure OpenAI client...")
print("Configuring Azure OpenAI client...")
client = create_openai_client()
# Create Eval
eval_object = create_eval(client=client, judge_model=judge_model)
# Process each prompt
print(f"Max self-reflections: {max_self_reflections}\n")
results = []
for counter, (idx, row) in enumerate(df.iterrows(), start=1):
print(f"[{counter}/{len(df)}] Processing prompt {row.get('original_index', idx)}...")
try:
result = await execute_query_with_self_reflection(
client=client,
agent=agent,
eval_object=eval_object,
full_user_query=row['full_prompt'],
context=row['context_document'],
full_user_query=row["full_prompt"],
context=row["context_document"],
max_self_reflections=max_self_reflections,
)
# Prepare result data
result_data = {
"original_index": row.get('original_index', idx),
"domain": row['domain'],
"question_type": row['type'],
"high_level_type": row['high_level_type'],
"full_prompt": row['full_prompt'],
"system_prompt": row['system_instruction'],
"user_request": row['user_request'],
"context_document": row['context_document'],
"original_index": row.get("original_index", idx),
"domain": row["domain"],
"question_type": row["type"],
"high_level_type": row["high_level_type"],
"full_prompt": row["full_prompt"],
"system_prompt": row["system_instruction"],
"user_request": row["user_request"],
"context_document": row["context_document"],
"agent_response_model": agent_model,
"agent_response": result,
"error": None,
@@ -346,14 +346,14 @@ async def run_self_reflection_batch(
# Save error information
error_data = {
"original_index": row.get('original_index', idx),
"domain": row['domain'],
"question_type": row['type'],
"high_level_type": row['high_level_type'],
"full_prompt": row['full_prompt'],
"system_prompt": row['system_instruction'],
"user_request": row['user_request'],
"context_document": row['context_document'],
"original_index": row.get("original_index", idx),
"domain": row["domain"],
"question_type": row["type"],
"high_level_type": row["high_level_type"],
"full_prompt": row["full_prompt"],
"system_prompt": row["system_instruction"],
"user_request": row["user_request"],
"context_document": row["context_document"],
"agent_response_model": agent_model,
"agent_response": None,
"error": str(e),
@@ -361,36 +361,36 @@ async def run_self_reflection_batch(
}
results.append(error_data)
continue
# Create DataFrame and save
results_df = pd.DataFrame(results)
print(f"\nSaving results to: {output_file}")
results_df.to_json(output_file, orient='records', lines=True)
results_df.to_json(output_file, orient="records", lines=True)
# Generate detailed summary
successful_runs = results_df[results_df['error'].isna()]
failed_runs = results_df[results_df['error'].notna()]
successful_runs = results_df[results_df["error"].isna()]
failed_runs = results_df[results_df["error"].notna()]
print("\n" + "="*60)
print("\n" + "=" * 60)
print("SUMMARY")
print("="*60)
print("=" * 60)
print(f"Total prompts processed: {len(results_df)}")
print(f" ✓ Successful: {len(successful_runs)}")
print(f" ✗ Failed: {len(failed_runs)}")
if len(successful_runs) > 0:
# Extract scores and iteration data from nested agent_response dict
best_scores = [r['best_response_score'] for r in successful_runs['agent_response'] if r is not None]
iterations = [r['best_iteration'] for r in successful_runs['agent_response'] if r is not None]
iteration_scores_list = [r['iteration_scores'] for r in successful_runs['agent_response'] if r is not None and 'iteration_scores' in r]
best_scores = [r["best_response_score"] for r in successful_runs["agent_response"] if r is not None]
iterations = [r["best_iteration"] for r in successful_runs["agent_response"] if r is not None]
iteration_scores_list = [r["iteration_scores"] for r in successful_runs["agent_response"] if r is not None and "iteration_scores" in r]
if best_scores:
avg_score = sum(best_scores) / len(best_scores)
perfect_scores = sum(1 for s in best_scores if s == 5)
print(f"\nGroundedness Scores:")
print("\nGroundedness Scores:")
print(f" Average best score: {avg_score:.2f}/5")
print(f" Perfect scores (5/5): {perfect_scores}/{len(best_scores)} ({100*perfect_scores/len(best_scores):.1f}%)")
print(f" Perfect scores (5/5): {perfect_scores}/{len(best_scores)} ({100 * perfect_scores / len(best_scores):.1f}%)")
# Calculate improvement metrics
if iteration_scores_list:
@@ -404,33 +404,33 @@ async def run_self_reflection_batch(
avg_last_score = sum(last_scores) / len(last_scores)
avg_improvement = sum(improvements) / len(improvements)
print(f"\nImprovement Analysis:")
print("\nImprovement Analysis:")
print(f" Average first score: {avg_first_score:.2f}/5")
print(f" Average final score: {avg_last_score:.2f}/5")
print(f" Average improvement: +{avg_improvement:.2f}")
print(f" Responses that improved: {improved_count}/{len(improvements)} ({100*improved_count/len(improvements):.1f}%)")
print(f" Responses that improved: {improved_count}/{len(improvements)} ({100 * improved_count / len(improvements):.1f}%)")
# Show iteration statistics
if iterations:
avg_iteration = sum(iterations) / len(iterations)
first_try = sum(1 for it in iterations if it == 1)
print(f"\nIteration Statistics:")
print("\nIteration Statistics:")
print(f" Average best iteration: {avg_iteration:.2f}")
print(f" Best on first try: {first_try}/{len(iterations)} ({100*first_try/len(iterations):.1f}%)")
print(f" Best on first try: {first_try}/{len(iterations)} ({100 * first_try / len(iterations):.1f}%)")
print("="*60)
print("=" * 60)
async def main():
"""CLI entry point."""
parser = argparse.ArgumentParser(description="Run self-reflection loop on LLM prompts with groundedness evaluation")
parser.add_argument('--input', '-i', default="resources/suboptimal_groundedness_prompts.jsonl", help='Input JSONL file with prompts')
parser.add_argument('--output', '-o', default="resources/results.jsonl", help='Output JSONL file for results')
parser.add_argument('--agent-model', '-m', default=DEFAULT_AGENT_MODEL, help=f'Agent model deployment name (default: {DEFAULT_AGENT_MODEL})')
parser.add_argument('--judge-model', '-e', default=DEFAULT_JUDGE_MODEL, help=f'Judge model deployment name (default: {DEFAULT_JUDGE_MODEL})')
parser.add_argument('--max-reflections', type=int, default=3, help='Maximum number of self-reflection iterations (default: 3)')
parser.add_argument('--env-file', help='Path to .env file with Azure OpenAI credentials')
parser.add_argument('--limit', '-n', type=int, default=None, help='Process only the first N prompts from the input file')
parser.add_argument("--input", "-i", default="resources/suboptimal_groundedness_prompts.jsonl", help="Input JSONL file with prompts")
parser.add_argument("--output", "-o", default="resources/results.jsonl", help="Output JSONL file for results")
parser.add_argument("--agent-model", "-m", default=DEFAULT_AGENT_MODEL, help=f"Agent model deployment name (default: {DEFAULT_AGENT_MODEL})")
parser.add_argument("--judge-model", "-e", default=DEFAULT_JUDGE_MODEL, help=f"Judge model deployment name (default: {DEFAULT_JUDGE_MODEL})")
parser.add_argument("--max-reflections", type=int, default=3, help="Maximum number of self-reflection iterations (default: 3)")
parser.add_argument("--env-file", help="Path to .env file with Azure OpenAI credentials")
parser.add_argument("--limit", "-n", type=int, default=None, help="Process only the first N prompts from the input file")
args = parser.parse_args()