Skip to content
Draft
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
209 changes: 209 additions & 0 deletions analyze_dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
#!/usr/bin/env python3
"""
Script to analyze model dependencies in the azure-ai-agentserver-core package
and identify which models can be safely removed.
"""

import re
import ast
from typing import Set, Dict, List, Tuple
from pathlib import Path

# Models that are directly imported by the other packages
REQUIRED_MODELS = {
# From base models
'CreateResponse', # From _create_response.py, but needs AgentReference
'Response',
'ResponseStreamEvent',

# From projects models - specifically imported
'AgentId',
'ItemContentOutputText', # inherits from ItemContent
'ResponsesAssistantMessageItemResource', # inherits from ResponsesMessageItemResource -> ItemResource
'FunctionToolCallItemResource', # inherits from ItemResource
'FunctionToolCallOutputItemResource', # inherits from ItemResource
'ResponseCompletedEvent', # inherits from ResponseStreamEvent
'ResponseContentPartAddedEvent', # inherits from ResponseStreamEvent
'ResponseContentPartDoneEvent', # inherits from ResponseStreamEvent
'ResponseCreatedEvent', # inherits from ResponseStreamEvent
'ResponseErrorEvent', # inherits from ResponseStreamEvent
'ResponseFunctionCallArgumentsDeltaEvent', # inherits from ResponseStreamEvent
'ResponseFunctionCallArgumentsDoneEvent', # inherits from ResponseStreamEvent
'ResponseInProgressEvent', # inherits from ResponseStreamEvent
'ResponseOutputItemAddedEvent', # inherits from ResponseStreamEvent
'ResponseOutputItemDoneEvent', # inherits from ResponseStreamEvent
'ResponseTextDeltaEvent', # inherits from ResponseStreamEvent
'ResponseTextDoneEvent', # inherits from ResponseStreamEvent

# From CreateResponse dependency
'AgentReference',

# Additional dependencies that are needed
'ItemContent', # base class for ItemContentOutputText
'ItemResource', # base class for function tool call resources and messages
'ResponsesMessageItemResource', # parent of ResponsesAssistantMessageItemResource
'CreatedBy', # referenced by ItemResource
}

# Enums that are referenced
REQUIRED_ENUMS = {
'ResponsesMessageRole',
}

def parse_class_definition(line: str) -> Tuple[str, List[str]]:
"""Parse a class definition line to extract class name and parent classes."""
# Match class definition with optional inheritance
match = re.match(r'class\s+(\w+)(\([^)]*\))?\s*:', line)
if not match:
return '', []

class_name = match.group(1)
parents_str = match.group(2)

parents = []
if parents_str:
# Remove parentheses and split by comma
parents_str = parents_str.strip('()')
for parent in parents_str.split(','):
parent = parent.strip()
# Handle discriminator parameters
if '=' not in parent:
parents.append(parent)

return class_name, parents

def extract_type_references(class_content: str) -> Set[str]:
"""Extract all type references from class content."""
references = set()

# Find type annotations
type_annotations = re.findall(r':\s*([^=\n]+)', class_content)
for annotation in type_annotations:
# Extract model names from type annotations
model_refs = re.findall(r'"_models\.(\w+)"', annotation)
references.update(model_refs)

# Also check for direct references
model_refs = re.findall(r'_models\.(\w+)', annotation)
references.update(model_refs)

# Check for Union types
union_refs = re.findall(r'Union\[[^\]]*"_models\.(\w+)"[^\]]*\]', annotation)
references.update(union_refs)

return references

def analyze_models_file(file_path: Path) -> Dict[str, Dict]:
"""Analyze the _models.py file to extract all class definitions and their dependencies."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

lines = content.split('\n')
models = {}
current_class = None
current_content = []

for i, line in enumerate(lines):
# Check if this is a class definition
if line.strip().startswith('class '):
# Save previous class if exists
if current_class:
class_content = '\n'.join(current_content)
type_refs = extract_type_references(class_content)
models[current_class]['type_references'] = type_refs

# Parse new class
class_name, parents = parse_class_definition(line)
if class_name:
current_class = class_name
models[class_name] = {
'line_number': i + 1,
'parents': parents,
'type_references': set()
}
current_content = [line]
elif current_class:
current_content.append(line)
# Stop collecting when we hit the next class or end
if line.strip() and not line.startswith(' ') and not line.startswith('\t') and not line.startswith('#'):
if not line.strip().startswith('class '):
current_content.pop() # Remove the non-class line
break

# Handle last class
if current_class:
class_content = '\n'.join(current_content)
type_refs = extract_type_references(class_content)
models[current_class]['type_references'] = type_refs

return models

def find_dependencies(models: Dict[str, Dict], required_set: Set[str]) -> Set[str]:
"""Find all dependencies of the required models."""
all_required = set(required_set)
to_process = list(required_set)

# Add known base classes that are definitely needed
base_classes = {'_Model', 'Model', 'ItemResource', 'ItemParam', 'Tool', 'ItemContent', 'CreatedBy'}
for base in base_classes:
if base in models:
all_required.add(base)
to_process.append(base)

while to_process:
current = to_process.pop()
if current not in models:
continue # Skip if not in our models (might be from base library)

model_info = models[current]

# Add parent classes
for parent in model_info['parents']:
# Clean up parent name (remove _Model, etc.)
clean_parent = parent.strip()
if clean_parent == '_Model':
clean_parent = 'Model' # This is just the base model class
continue # Skip _Model as it's imported from utils

if clean_parent not in all_required and clean_parent in models:
all_required.add(clean_parent)
to_process.append(clean_parent)

# Add type references
for ref in model_info['type_references']:
if ref not in all_required and ref in models:
all_required.add(ref)
to_process.append(ref)

# Remove _Model if it was added, as it's from utils
all_required.discard('_Model')
all_required.discard('Model')

return all_required

def main():
models_file = Path("c:/Users/llawrence/Desktop/Repo/azure-sdk-for-python/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/models/projects/_models.py")

print("Analyzing models file...")
models = analyze_models_file(models_file)
print(f"Found {len(models)} model classes")

print("\nFinding dependencies...")
all_required = find_dependencies(models, REQUIRED_MODELS)

print(f"\nRequired models (including dependencies): {len(all_required)}")
for model in sorted(all_required):
print(f" - {model}")

print(f"\nModels that can be removed: {len(models) - len(all_required)}")
removable = set(models.keys()) - all_required
for model in sorted(removable):
print(f" - {model}")

print(f"\nSummary:")
print(f"Total models: {len(models)}")
print(f"Required models: {len(all_required)}")
print(f"Removable models: {len(removable)}")

if __name__ == "__main__":
main()
2 changes: 1 addition & 1 deletion eng/tools/azure-sdk-tools/azpysdk/mypy.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from ci_tools.environment_exclusions import is_check_enabled, is_typing_ignored
from ci_tools.logging import logger

PYTHON_VERSION = "3.9"
PYTHON_VERSION = "3.10"
MYPY_VERSION = "1.14.1"
ADDITIONAL_LOCKED_DEPENDENCIES = [
"types-chardet==5.0.4.6",
Expand Down
122 changes: 122 additions & 0 deletions remove_unused_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""
Script to remove unused model classes from azure-ai-agentserver-core _models.py
"""

import re
from typing import Set, List
from pathlib import Path

# Keep only these models (from the dependency analysis)
KEEP_MODELS = {
'AgentId', 'AgentReference', 'CreateResponse', 'CreatedBy',
'FunctionToolCallItemResource', 'FunctionToolCallOutputItemResource',
'ItemContent', 'ItemContentOutputText', 'ItemParam', 'ItemResource',
'Response', 'ResponseCompletedEvent', 'ResponseContentPartAddedEvent',
'ResponseContentPartDoneEvent', 'ResponseCreatedEvent', 'ResponseErrorEvent',
'ResponseFunctionCallArgumentsDeltaEvent', 'ResponseFunctionCallArgumentsDoneEvent',
'ResponseInProgressEvent', 'ResponseOutputItemAddedEvent', 'ResponseOutputItemDoneEvent',
'ResponseStreamEvent', 'ResponseTextDeltaEvent', 'ResponseTextDoneEvent',
'ResponsesAssistantMessageItemResource', 'ResponsesMessageItemResource', 'Tool'
}

def extract_class_blocks(content: str) -> List[tuple[str, int, int, str]]:
"""Extract all class definitions and their line ranges."""
lines = content.split('\n')
classes = []
current_class = None
current_start = None

for i, line in enumerate(lines):
if line.strip().startswith('class '):
# Save previous class if exists
if current_class and current_start is not None:
classes.append((current_class, current_start, i - 1, '\n'.join(lines[current_start:i])))

# Parse new class
match = re.match(r'class\s+(\w+)', line)
if match:
current_class = match.group(1)
current_start = i
elif current_class and line.strip() and not line.startswith((' ', '\t', '#')):
# This is the start of something new (not a class member)
if not line.strip().startswith('class '):
# Save current class and reset
if current_start is not None:
classes.append((current_class, current_start, i - 1, '\n'.join(lines[current_start:i])))
current_class = None
current_start = None

# Handle last class
if current_class and current_start is not None:
classes.append((current_class, current_start, len(lines) - 1, '\n'.join(lines[current_start:])))

return classes

def remove_unused_models(file_path: Path, keep_models: Set[str]) -> str:
"""Remove unused model classes from the file."""
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()

lines = content.split('\n')
classes = extract_class_blocks(content)

print(f"Found {len(classes)} class definitions")

# Mark lines to remove
lines_to_remove = set()
removed_classes = []

for class_name, start_line, end_line, class_content in classes:
if class_name not in keep_models:
print(f"Removing class {class_name} (lines {start_line + 1}-{end_line + 1})")
removed_classes.append(class_name)
for line_num in range(start_line, end_line + 1):
lines_to_remove.add(line_num)

# Create new content without removed lines
new_lines = []
for i, line in enumerate(lines):
if i not in lines_to_remove:
new_lines.append(line)

new_content = '\n'.join(new_lines)

# Clean up any double newlines that might have been created
new_content = re.sub(r'\n\n\n+', '\n\n', new_content)

print(f"Removed {len(removed_classes)} classes:")
for cls in sorted(removed_classes):
print(f" - {cls}")

return new_content

def main():
models_file = Path("c:/Users/llawrence/Desktop/Repo/azure-sdk-for-python/sdk/agentserver/azure-ai-agentserver-core/azure/ai/agentserver/core/models/projects/_models.py")
backup_file = models_file.with_suffix('.py.backup')

print(f"Creating backup at {backup_file}")
with open(models_file, 'r', encoding='utf-8') as f:
original_content = f.read()

with open(backup_file, 'w', encoding='utf-8') as f:
f.write(original_content)

print(f"Processing {models_file}")
new_content = remove_unused_models(models_file, KEEP_MODELS)

print(f"Writing updated content to {models_file}")
with open(models_file, 'w', encoding='utf-8') as f:
f.write(new_content)

print("Done! File has been updated.")

# Show stats
original_lines = len(original_content.split('\n'))
new_lines = len(new_content.split('\n'))
print(f"Original: {original_lines} lines")
print(f"New: {new_lines} lines")
print(f"Reduced by: {original_lines - new_lines} lines ({((original_lines - new_lines) / original_lines * 100):.1f}%)")

if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,12 @@ def _ensure_response_started(self) -> None:
self._response_created_at = int(datetime.datetime.now(datetime.timezone.utc).timestamp()) # type: ignore

def _build_item_content_output_text(self, text: str) -> ItemContentOutputText:
return ItemContentOutputText(text=text, annotations=[])
return ItemContentOutputText(type="output_text", text=text, annotations=[])

def _new_assistant_message_item(self, message_text: str) -> ResponsesAssistantMessageItemResource:
item_content = self._build_item_content_output_text(message_text)
return ResponsesAssistantMessageItemResource(
id=self._context.id_generator.generate_message_id(), status="completed", content=[item_content]
role="assistant", type="message", id=self._context.id_generator.generate_message_id(), status="completed", content=[item_content]
)

def transform_output_for_response(self, response: AgentRunResponse) -> OpenAIResponse:
Expand Down Expand Up @@ -77,7 +77,7 @@ def transform_output_for_response(self, response: AgentRunResponse) -> OpenAIRes
self._append_content_item(content, completed_items)

response_data = self._construct_response_data(completed_items)
openai_response = OpenAIResponse(response_data)
openai_response = OpenAIResponse(**response_data)
logger.info(
"OpenAIResponse built (id=%s, items=%d)",
self._response_id,
Expand Down
Loading
Loading