feat!: migrate to OpenAI native tool calls and fix circular deps (#fuck-gemini)
- Fix circular dependencies in agent/tools - Migrate from custom JSON to OpenAI tool calls format - Add async streaming (step_stream, complete_stream) - Simplify prompt system and remove token counting - Add 5 new API endpoints (/health, /v1/models, /api/memory/*) - Add 3 new tools (get_torrent_by_index, add_torrent_by_index, set_language) - Fix all 500 tests and add coverage config (80% threshold) - Add comprehensive docs (README, pytest guide) BREAKING: LLM interface changed, memory injection via get_memory()
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"""Shared kernel - Common domain concepts used across subdomains."""
|
||||
|
||||
from .exceptions import DomainException, ValidationError
|
||||
from .value_objects import ImdbId, FilePath, FileSize
|
||||
from .value_objects import FilePath, FileSize, ImdbId
|
||||
|
||||
__all__ = [
|
||||
"DomainException",
|
||||
|
||||
@@ -3,19 +3,23 @@
|
||||
|
||||
class DomainException(Exception):
|
||||
"""Base exception for all domain-related errors."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class ValidationError(DomainException):
|
||||
"""Raised when domain validation fails."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(DomainException):
|
||||
"""Raised when a domain entity is not found."""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class AlreadyExistsError(DomainException):
|
||||
"""Raised when trying to create an entity that already exists."""
|
||||
|
||||
pass
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"""Shared value objects used across multiple domains."""
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
import re
|
||||
|
||||
from .exceptions import ValidationError
|
||||
|
||||
@@ -11,30 +11,31 @@ from .exceptions import ValidationError
|
||||
class ImdbId:
|
||||
"""
|
||||
Value object representing an IMDb ID.
|
||||
|
||||
|
||||
IMDb IDs follow the format: tt followed by 7-8 digits (e.g., tt1375666)
|
||||
"""
|
||||
|
||||
value: str
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate IMDb ID format."""
|
||||
if not self.value:
|
||||
raise ValidationError("IMDb ID cannot be empty")
|
||||
|
||||
|
||||
if not isinstance(self.value, str):
|
||||
raise ValidationError(f"IMDb ID must be a string, got {type(self.value)}")
|
||||
|
||||
|
||||
# IMDb ID format: tt + 7-8 digits
|
||||
pattern = r'^tt\d{7,8}$'
|
||||
pattern = r"^tt\d{7,8}$"
|
||||
if not re.match(pattern, self.value):
|
||||
raise ValidationError(
|
||||
f"Invalid IMDb ID format: {self.value}. "
|
||||
"Expected format: tt followed by 7-8 digits (e.g., tt1375666)"
|
||||
)
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.value
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"ImdbId('{self.value}')"
|
||||
|
||||
@@ -43,15 +44,16 @@ class ImdbId:
|
||||
class FilePath:
|
||||
"""
|
||||
Value object representing a file path with validation.
|
||||
|
||||
|
||||
Ensures the path is valid and optionally checks existence.
|
||||
"""
|
||||
|
||||
value: Path
|
||||
|
||||
def __init__(self, path: Union[str, Path]):
|
||||
|
||||
def __init__(self, path: str | Path):
|
||||
"""
|
||||
Initialize FilePath.
|
||||
|
||||
|
||||
Args:
|
||||
path: String or Path object representing the file path
|
||||
"""
|
||||
@@ -61,25 +63,25 @@ class FilePath:
|
||||
path_obj = path
|
||||
else:
|
||||
raise ValidationError(f"Path must be str or Path, got {type(path)}")
|
||||
|
||||
|
||||
# Use object.__setattr__ because dataclass is frozen
|
||||
object.__setattr__(self, 'value', path_obj)
|
||||
|
||||
object.__setattr__(self, "value", path_obj)
|
||||
|
||||
def exists(self) -> bool:
|
||||
"""Check if the path exists."""
|
||||
return self.value.exists()
|
||||
|
||||
|
||||
def is_file(self) -> bool:
|
||||
"""Check if the path is a file."""
|
||||
return self.value.is_file()
|
||||
|
||||
|
||||
def is_dir(self) -> bool:
|
||||
"""Check if the path is a directory."""
|
||||
return self.value.is_dir()
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"FilePath('{self.value}')"
|
||||
|
||||
@@ -88,41 +90,44 @@ class FilePath:
|
||||
class FileSize:
|
||||
"""
|
||||
Value object representing a file size in bytes.
|
||||
|
||||
|
||||
Provides human-readable formatting.
|
||||
"""
|
||||
|
||||
bytes: int
|
||||
|
||||
|
||||
def __post_init__(self):
|
||||
"""Validate file size."""
|
||||
if not isinstance(self.bytes, int):
|
||||
raise ValidationError(f"File size must be an integer, got {type(self.bytes)}")
|
||||
|
||||
raise ValidationError(
|
||||
f"File size must be an integer, got {type(self.bytes)}"
|
||||
)
|
||||
|
||||
if self.bytes < 0:
|
||||
raise ValidationError(f"File size cannot be negative: {self.bytes}")
|
||||
|
||||
|
||||
def to_human_readable(self) -> str:
|
||||
"""
|
||||
Convert bytes to human-readable format.
|
||||
|
||||
|
||||
Returns:
|
||||
String like "1.5 GB", "500 MB", etc.
|
||||
"""
|
||||
units = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
units = ["B", "KB", "MB", "GB", "TB"]
|
||||
size = float(self.bytes)
|
||||
unit_index = 0
|
||||
|
||||
|
||||
while size >= 1024 and unit_index < len(units) - 1:
|
||||
size /= 1024
|
||||
unit_index += 1
|
||||
|
||||
|
||||
if unit_index == 0:
|
||||
return f"{int(size)} {units[unit_index]}"
|
||||
else:
|
||||
return f"{size:.2f} {units[unit_index]}"
|
||||
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.to_human_readable()
|
||||
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"FileSize({self.bytes})"
|
||||
|
||||
Reference in New Issue
Block a user