types.py 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. from __future__ import annotations
  2. from collections.abc import Callable, Sequence
  3. from enum import StrEnum
  4. from typing import Any, TypeAlias
  5. from pydantic import BaseModel, Field
  6. class Role(StrEnum):
  7. SYSTEM = "system"
  8. USER = "user"
  9. ASSISTANT = "assistant"
  10. TOOL = "tool"
  11. class Message(BaseModel):
  12. """A single message in the conversation."""
  13. role: Role
  14. content: str | None = None
  15. tool_calls: list[ToolCall] | None = None
  16. tool_call_id: str | None = None
  17. name: str | None = None
  18. # Model reasoning / chain-of-thought text (OpenRouter); not sent back in API history.
  19. reasoning: str | None = None
  20. def to_api_dict(self) -> dict[str, Any]:
  21. """Convert to OpenAI-compatible API format."""
  22. data: dict[str, Any] = {"role": self.role.value}
  23. if self.content is not None:
  24. data["content"] = self.content
  25. if self.tool_calls:
  26. data["tool_calls"] = [tc.to_api_dict() for tc in self.tool_calls]
  27. if self.tool_call_id:
  28. data["tool_call_id"] = self.tool_call_id
  29. if self.name:
  30. data["name"] = self.name
  31. return data
  32. CompletionGuard: TypeAlias = Callable[
  33. [Message, Sequence[Message]],
  34. str | None,
  35. ]
  36. """Completion callback.
  37. Return ``None`` to accept an assistant response without tool calls as the final
  38. answer. Return feedback text to reject completion and continue the agent loop;
  39. the feedback is appended as a user message before the next iteration. The guard
  40. does not run for the forced final answer after ``max_iterations`` is reached.
  41. """
  42. class ToolCall(BaseModel):
  43. """A tool invocation requested by the model."""
  44. id: str
  45. name: str
  46. arguments: str # JSON string
  47. def to_api_dict(self) -> dict[str, Any]:
  48. return {
  49. "id": self.id,
  50. "type": "function",
  51. "function": {
  52. "name": self.name,
  53. "arguments": self.arguments,
  54. },
  55. }
  56. class ToolDefinition(BaseModel):
  57. """OpenAI-compatible tool schema."""
  58. name: str
  59. description: str
  60. parameters: dict[str, Any] = Field(default_factory=lambda: {
  61. "type": "object",
  62. "properties": {},
  63. "required": [],
  64. })
  65. def to_api_dict(self) -> dict[str, Any]:
  66. return {
  67. "type": "function",
  68. "function": {
  69. "name": self.name,
  70. "description": self.description,
  71. "parameters": self.parameters,
  72. },
  73. }
  74. class ToolResult(BaseModel):
  75. """Result of executing a tool."""
  76. tool_call_id: str
  77. name: str
  78. content: str
  79. is_error: bool = False
  80. class SkillInfo(BaseModel):
  81. """Metadata and content for a loaded skill."""
  82. name: str
  83. description: str
  84. content: str
  85. path: str
  86. class AgentEventType(StrEnum):
  87. """Events emitted during agent execution."""
  88. THINKING = "thinking"
  89. TOOL_CALL = "tool_call"
  90. TOOL_RESULT = "tool_result"
  91. MESSAGE = "message"
  92. SKILL_LOADED = "skill_loaded"
  93. DONE = "done"
  94. ERROR = "error"
  95. class AgentEvent(BaseModel):
  96. """A streaming event from the agent loop."""
  97. type: AgentEventType
  98. data: dict[str, Any] = Field(default_factory=dict)
  99. class AgentResult(BaseModel):
  100. """Final result of an agent run."""
  101. content: str
  102. messages: list[Message]
  103. iterations: int
  104. tool_calls_made: int
  105. skills_used: list[str] = Field(default_factory=list)