Coverage for agentos/protocols/output.py: 38%
64 statements
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
« prev ^ index » next coverage.py v7.14.3, created at 2026-07-02 09:59 +0800
1"""
2Structured output validation for NexusAgent.
4Provides Pydantic-style output validation for agents.
5When Agent[Deps, Out] has Out as a Pydantic BaseModel,
6the output is automatically validated.
7"""
9from __future__ import annotations
11from typing import Any, TypeVar, Generic, get_type_hints, get_origin, get_args
12from dataclasses import dataclass
14try:
15 from pydantic import BaseModel, ValidationError
16 PYDANTIC_AVAILABLE = True
17except ImportError:
18 BaseModel = None
19 ValidationError = Exception
20 PYDANTIC_AVAILABLE = False
22T = TypeVar("T")
25if PYDANTIC_AVAILABLE:
26 from pydantic import ConfigDict
29class StructuredOutput(BaseModel if PYDANTIC_AVAILABLE else object):
31 """Agent 结构化输出。"""
33 """
34 Base class for structured outputs.
36 Usage:
37 class MyOutput(StructuredOutput):
38 answer: str
39 confidence: float
40 sources: list[str]
41 """
42 if PYDANTIC_AVAILABLE:
43 model_config = ConfigDict(extra="forbid")
46@dataclass
47class ValidationResult(Generic[T]):
48 """
49 Result of output validation.
51 Attributes:
52 success: Whether validation passed
53 output: Validated output (if success)
54 error: Validation error (if failed)
55 """
56 success: bool
57 output: T | None = None
58 error: str | None = None
61class OutputValidator(Generic[T]):
62 """
63 Validator for structured outputs.
65 Usage:
66 validator = OutputValidator(MyOutput)
67 result = validator.validate({"answer": "42", "confidence": 0.9})
68 if result.success:
69 output = result.output # MyOutput instance
70 """
72 def __init__(self, output_type: type[T]):
73 """
74 Initialize validator.
76 Args:
77 output_type: Expected output type
78 """
79 self.output_type = output_type
80 self._is_pydantic = (
81 PYDANTIC_AVAILABLE and
82 isinstance(output_type, type) and
83 issubclass(output_type, BaseModel)
84 )
86 def validate(self, data: Any) -> ValidationResult[T]:
87 """
88 Validate data against output type.
90 Args:
91 data: Data to validate
93 Returns:
94 ValidationResult with success/error info
95 """
96 # If not Pydantic, just check type
97 if not self._is_pydantic:
98 if isinstance(data, self.output_type):
99 return ValidationResult(success=True, output=data)
100 else:
101 return ValidationResult(
102 success=False,
103 error=f"Expected {self.output_type}, got {type(data)}"
104 )
106 # Pydantic validation
107 try:
108 if isinstance(data, self.output_type):
109 # Already correct type
110 return ValidationResult(success=True, output=data)
111 elif isinstance(data, dict):
112 # Try to construct from dict
113 output = self.output_type(**data)
114 return ValidationResult(success=True, output=output)
115 else:
116 # Try model_validate
117 output = self.output_type.model_validate(data)
118 return ValidationResult(success=True, output=output)
119 except ValidationError as e:
120 return ValidationResult(success=False, error=str(e))
121 except Exception as e:
122 return ValidationResult(success=False, error=str(e))
124 def validate_or_raise(self, data: Any) -> T:
125 """
126 Validate data, raise on failure.
128 Args:
129 data: Data to validate
131 Returns:
132 Validated output
134 Raises:
135 ValueError: If validation fails
136 """
137 result = self.validate(data)
138 if not result.success:
139 raise ValueError(f"Output validation failed: {result.error}")
140 return result.output
143def validate_output(output_type: type[T], data: Any) -> ValidationResult[T]:
144 """
145 Validate data against output type.
147 Convenience function wrapping OutputValidator.
149 Args:
150 output_type: Expected output type
151 data: Data to validate
153 Returns:
154 ValidationResult with success/error info
156 Usage:
157 result = validate_output(MyOutput, {"answer": "42"})
158 if result.success:
159 output = result.output
160 """
161 validator = OutputValidator(output_type)
162 return validator.validate(data)
165def get_output_type(agent_class: type) -> type | None:
166 """
167 Extract output type from Agent class.
169 Args:
170 agent_class: Agent subclass
172 Returns:
173 Output type if found, None otherwise
174 """
175 # Check type hints
176 hints = get_type_hints(agent_class)
177 if 'Out' in hints:
178 return hints['Out']
180 # Check generic base
181 for base in agent_class.__mro__:
182 origin = get_origin(base)
183 if origin is not None:
184 # Check if it's Agent
185 from agentos.core.di import Agent
186 if issubclass(origin, Agent):
187 args = get_args(base)
188 if len(args) >= 2:
189 return args[1]
191 return None