mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
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:
committed by
GitHub
Unverified
parent
ef798629e5
commit
838a7fd61d
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user