idun-agent-schema 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of idun-agent-schema might be problematic. Click here for more details.

@@ -0,0 +1,201 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+ # Database files
177
+ *.db
178
+ *.db-shm
179
+ *.db-wai
180
+ *.db-journal*
181
+ *.sqlite
182
+ *.sqlite3
183
+ agents.db
184
+ checkpoint_async.db
185
+
186
+ # Python virtual environment
187
+ .venv/
188
+ venv/
189
+ ENV/
190
+
191
+ # Python cache
192
+ __pycache__/
193
+ *.pyc
194
+ *.pyo
195
+ *.pyd
196
+
197
+ # Poetry
198
+ poetry.lock
199
+
200
+ # Environment variables
201
+ .env
@@ -0,0 +1,40 @@
1
+ Metadata-Version: 2.4
2
+ Name: idun-agent-schema
3
+ Version: 0.1.0
4
+ Summary: Centralized Pydantic schema library for Idun Agent Engine and Manager
5
+ Project-URL: Homepage, https://github.com/geoffreyharrazi/idun-agent-platform
6
+ Project-URL: Repository, https://github.com/geoffreyharrazi/idun-agent-platform
7
+ Project-URL: Issues, https://github.com/geoffreyharrazi/idun-agent-platform/issues
8
+ Author-email: Idun Team <team@idun.com>
9
+ License-Expression: MIT
10
+ Keywords: fastapi,idun,langgraph,pydantic,schemas
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3 :: Only
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Typing :: Typed
16
+ Requires-Python: <3.14,>=3.13
17
+ Requires-Dist: pydantic-settings<3.0.0,>=2.7.0
18
+ Requires-Dist: pydantic<3.0.0,>=2.11.7
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Idun Agent Schema
22
+
23
+ Centralized Pydantic schema library shared by Idun Agent Engine and Idun Agent Manager.
24
+
25
+ ## Install
26
+
27
+ ```bash
28
+ pip install idun-agent-schema
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ ```python
34
+ from idun_agent_schema.engine import EngineConfig
35
+ from idun_agent_schema.manager.api import AgentCreateRequest
36
+ ```
37
+
38
+ This package re-exports stable schema namespaces to avoid breaking existing imports. Prefer importing from this package directly going forward.
39
+
40
+
@@ -0,0 +1,20 @@
1
+ # Idun Agent Schema
2
+
3
+ Centralized Pydantic schema library shared by Idun Agent Engine and Idun Agent Manager.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install idun-agent-schema
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```python
14
+ from idun_agent_schema.engine import EngineConfig
15
+ from idun_agent_schema.manager.api import AgentCreateRequest
16
+ ```
17
+
18
+ This package re-exports stable schema namespaces to avoid breaking existing imports. Prefer importing from this package directly going forward.
19
+
20
+
@@ -0,0 +1,76 @@
1
+ [project]
2
+ name = "idun-agent-schema"
3
+ version = "0.1.0"
4
+ description = "Centralized Pydantic schema library for Idun Agent Engine and Manager"
5
+ authors = [{ name = "Idun Team", email = "team@idun.com" }]
6
+ readme = "README.md"
7
+ license = "MIT"
8
+ requires-python = ">=3.13,<3.14"
9
+ keywords = ["pydantic", "schemas", "fastapi", "langgraph", "idun"]
10
+ classifiers = [
11
+ "License :: OSI Approved :: MIT License",
12
+ "Programming Language :: Python :: 3",
13
+ "Programming Language :: Python :: 3 :: Only",
14
+ "Programming Language :: Python :: 3.13",
15
+ "Typing :: Typed",
16
+ ]
17
+ dependencies = [
18
+ "pydantic>=2.11.7,<3.0.0",
19
+ "pydantic-settings>=2.7.0,<3.0.0",
20
+ ]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/geoffreyharrazi/idun-agent-platform"
24
+ Repository = "https://github.com/geoffreyharrazi/idun-agent-platform"
25
+ Issues = "https://github.com/geoffreyharrazi/idun-agent-platform/issues"
26
+
27
+ [build-system]
28
+ requires = ["hatchling>=1.25.0"]
29
+ build-backend = "hatchling.build"
30
+
31
+ [tool.hatch.build.targets.sdist]
32
+ include = [
33
+ "src/idun_agent_schema",
34
+ "src/idun_agent_schema/py.typed",
35
+ ]
36
+
37
+ [tool.hatch.build.targets.wheel]
38
+ include = [
39
+ "src/idun_agent_schema",
40
+ "src/idun_agent_schema/py.typed",
41
+ ]
42
+
43
+ [tool.hatch.build.targets.wheel.sources]
44
+ "src/idun_agent_schema" = "idun_agent_schema"
45
+
46
+ [tool.ruff]
47
+ target-version = "py313"
48
+ line-length = 88
49
+
50
+ [tool.ruff.lint]
51
+ select = [
52
+ "E", "F", "W", "I", "B", "UP", "N", "SIM", "D"
53
+ ]
54
+ extend-ignore = ["E501"]
55
+
56
+ [tool.black]
57
+ line-length = 88
58
+ target-version = ["py313"]
59
+ include = "\\.pyi?$"
60
+
61
+ [tool.mypy]
62
+ python_version = "3.13"
63
+ warn_return_any = true
64
+ warn_unused_ignores = true
65
+ ignore_missing_imports = true
66
+ strict_optional = true
67
+ no_implicit_optional = true
68
+ check_untyped_defs = true
69
+ pretty = true
70
+ show_error_codes = true
71
+
72
+ [tool.pytest.ini_options]
73
+ addopts = "-ra -q --strict-markers --disable-warnings"
74
+ testpaths = ["tests"]
75
+
76
+
@@ -0,0 +1,12 @@
1
+ """Idun Agent Schema - Centralized Pydantic schemas.
2
+
3
+ Public namespaces:
4
+ - idun_agent_schema.engine: Engine-related schemas
5
+ - idun_agent_schema.manager: Manager-related schemas
6
+ - idun_agent_schema.shared: Shared cross-cutting schemas
7
+ """
8
+
9
+ # Re-export key types for convenience
10
+ from .shared.observability import ObservabilityConfig # noqa: F401
11
+
12
+
@@ -0,0 +1,13 @@
1
+ """Engine-related schemas."""
2
+
3
+ from .agent import BaseAgentConfig # noqa: F401
4
+ from .api import ChatRequest, ChatResponse # noqa: F401
5
+ from .config import AgentConfig, EngineConfig # noqa: F401
6
+ from .langgraph import ( # noqa: F401
7
+ CheckpointConfig,
8
+ LangGraphAgentConfig,
9
+ SqliteCheckpointConfig,
10
+ )
11
+ from .server import ServerAPIConfig, ServerConfig # noqa: F401
12
+
13
+
@@ -0,0 +1,18 @@
1
+ """Common agent model definitions (engine)."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from idun_agent_schema.shared import ObservabilityConfig
8
+
9
+
10
+ class BaseAgentConfig(BaseModel):
11
+ """Base model for agent configurations. Extend for specific frameworks."""
12
+
13
+ name: str | None = Field(default="Unnamed Agent")
14
+ input_schema_definition: dict[str, Any] | None = Field(default_factory=dict)
15
+ output_schema_definition: dict[str, Any] | None = Field(default_factory=dict)
16
+ observability: ObservabilityConfig | None = Field(default=None)
17
+
18
+
@@ -0,0 +1,15 @@
1
+ """Schemas for engine HTTP API request/response payloads."""
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class ChatRequest(BaseModel):
7
+ session_id: str
8
+ query: str
9
+
10
+
11
+ class ChatResponse(BaseModel):
12
+ session_id: str
13
+ response: str
14
+
15
+
@@ -0,0 +1,27 @@
1
+ """Top-level engine configuration models."""
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+ from .agent import BaseAgentConfig
8
+ from .langgraph import LangGraphAgentConfig
9
+ from .server import ServerConfig
10
+
11
+
12
+ class AgentConfig(BaseModel):
13
+ """Configuration for agent specification and settings."""
14
+
15
+ type: Literal["langgraph", "ADK", "CREWAI"] = Field(default="langgraph")
16
+ config: BaseAgentConfig | LangGraphAgentConfig = Field(
17
+ default_factory=BaseAgentConfig
18
+ )
19
+
20
+
21
+ class EngineConfig(BaseModel):
22
+ """Main engine configuration model for the entire Idun Agent Engine."""
23
+
24
+ server: ServerConfig = Field(default_factory=ServerConfig)
25
+ agent: AgentConfig
26
+
27
+
@@ -0,0 +1,42 @@
1
+ """Configuration models for LangGraph agents (engine)."""
2
+
3
+ from typing import Any, Literal
4
+ from urllib.parse import urlparse
5
+
6
+ from pydantic import BaseModel, field_validator
7
+
8
+ from .agent import BaseAgentConfig
9
+
10
+
11
+ class SqliteCheckpointConfig(BaseModel):
12
+ """Configuration for SQLite checkpointer."""
13
+
14
+ type: Literal["sqlite"]
15
+ db_url: str
16
+
17
+ @field_validator("db_url")
18
+ @classmethod
19
+ def db_url_must_be_sqlite(cls, v: str) -> str:
20
+ if not v.startswith("sqlite:///"):
21
+ raise ValueError("SQLite DB URL must start with 'sqlite:///'")
22
+ return v
23
+
24
+ @property
25
+ def db_path(self) -> str:
26
+ path = urlparse(self.db_url).path
27
+ if self.db_url.startswith("sqlite:///"):
28
+ return path.lstrip("/")
29
+ return path
30
+
31
+
32
+ CheckpointConfig = SqliteCheckpointConfig
33
+
34
+
35
+ class LangGraphAgentConfig(BaseAgentConfig):
36
+ """Configuration model for LangGraph agents."""
37
+
38
+ graph_definition: str
39
+ checkpointer: CheckpointConfig | None = None
40
+ store: dict[str, Any] | None = None
41
+
42
+
@@ -0,0 +1,17 @@
1
+ """Server configuration models (engine)."""
2
+
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class ServerAPIConfig(BaseModel):
7
+ """API server configuration."""
8
+
9
+ port: int = 8000
10
+
11
+
12
+ class ServerConfig(BaseModel):
13
+ """Configuration for the Engine's universal settings."""
14
+
15
+ api: ServerAPIConfig = Field(default_factory=ServerAPIConfig)
16
+
17
+
@@ -0,0 +1,52 @@
1
+ """Manager-related schemas."""
2
+
3
+ from .api import ( # noqa: F401
4
+ AgentCreateRequest,
5
+ AgentResponse,
6
+ AgentRunRequest,
7
+ AgentRunResponse,
8
+ AgentRunSummaryResponse,
9
+ AgentStatsResponse,
10
+ AgentSummaryResponse,
11
+ AgentUpdateRequest,
12
+ PaginatedAgentsResponse,
13
+ PaginatedResponse,
14
+ PaginatedRunsResponse,
15
+ )
16
+ from .deps import Principal # noqa: F401
17
+ from .domain import ( # noqa: F401
18
+ AgentEntity,
19
+ AgentFramework,
20
+ AgentRunEntity,
21
+ AgentStatus,
22
+ TenantEntity,
23
+ TenantPlan,
24
+ TenantStatus,
25
+ TenantUserEntity,
26
+ )
27
+ from .dto import ( # noqa: F401
28
+ AgentCreateDTO,
29
+ AgentDeploymentDTO,
30
+ AgentHealthDTO,
31
+ AgentMetricsDTO,
32
+ AgentRunCreateDTO,
33
+ AgentUpdateDTO,
34
+ TenantCreateDTO,
35
+ TenantQuotaDTO,
36
+ TenantUpdateDTO,
37
+ TenantUsageDTO,
38
+ TenantUserCreateDTO,
39
+ TenantUserUpdateDTO,
40
+ )
41
+ from .errors import ProblemDetail # noqa: F401
42
+ from .settings import ( # noqa: F401
43
+ APISettings,
44
+ AuthSettings,
45
+ CelerySettings,
46
+ DatabaseSettings,
47
+ ObservabilitySettings,
48
+ RedisSettings,
49
+ Settings,
50
+ )
51
+
52
+
@@ -0,0 +1,130 @@
1
+ """Pydantic schemas for Agent Manager API I/O."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from .domain import AgentFramework, AgentStatus
10
+
11
+
12
+ class AgentCreateRequest(BaseModel):
13
+ name: str = Field(..., min_length=1, max_length=255)
14
+ description: str | None = Field(None, max_length=1000)
15
+ framework: AgentFramework
16
+ config: dict[str, Any] = Field(default_factory=dict)
17
+ environment_variables: dict[str, str] = Field(default_factory=dict)
18
+ tags: list[str] = Field(default_factory=list)
19
+
20
+
21
+ class AgentUpdateRequest(BaseModel):
22
+ name: str | None = Field(None, min_length=1, max_length=255)
23
+ description: str | None = Field(None, max_length=1000)
24
+ config: dict[str, Any] | None = None
25
+ environment_variables: dict[str, str] | None = None
26
+ tags: list[str] | None = None
27
+
28
+
29
+ class AgentRunRequest(BaseModel):
30
+ input_data: dict[str, Any]
31
+ trace_id: str | None = Field(None, max_length=100)
32
+
33
+
34
+ class AgentResponse(BaseModel):
35
+ id: UUID
36
+ name: str
37
+ description: str | None
38
+ framework: AgentFramework
39
+ status: AgentStatus
40
+ config: dict[str, Any]
41
+ environment_variables: dict[str, str]
42
+ version: str
43
+ tags: list[str]
44
+ tenant_id: UUID
45
+ created_at: datetime
46
+ updated_at: datetime
47
+ deployed_at: datetime | None
48
+ total_runs: int
49
+ success_rate: float | None
50
+ avg_response_time_ms: float | None
51
+
52
+ class Config:
53
+ from_attributes = True
54
+
55
+
56
+ class AgentSummaryResponse(BaseModel):
57
+ id: UUID
58
+ name: str
59
+ description: str | None
60
+ framework: AgentFramework
61
+ status: AgentStatus
62
+ version: str
63
+ tags: list[str]
64
+ created_at: datetime
65
+ updated_at: datetime
66
+ total_runs: int
67
+ success_rate: float | None
68
+
69
+ class Config:
70
+ from_attributes = True
71
+
72
+
73
+ class AgentRunResponse(BaseModel):
74
+ id: UUID
75
+ agent_id: UUID
76
+ tenant_id: UUID
77
+ input_data: dict[str, Any]
78
+ output_data: dict[str, Any] | None
79
+ status: str
80
+ started_at: datetime
81
+ completed_at: datetime | None
82
+ error_message: str | None
83
+ response_time_ms: float | None
84
+ tokens_used: int | None
85
+ cost_usd: float | None
86
+ trace_id: str | None
87
+ span_id: str | None
88
+
89
+ class Config:
90
+ from_attributes = True
91
+
92
+
93
+ class AgentRunSummaryResponse(BaseModel):
94
+ id: UUID
95
+ agent_id: UUID
96
+ status: str
97
+ started_at: datetime
98
+ completed_at: datetime | None
99
+ response_time_ms: float | None
100
+ tokens_used: int | None
101
+ cost_usd: float | None
102
+
103
+ class Config:
104
+ from_attributes = True
105
+
106
+
107
+ class PaginatedResponse(BaseModel):
108
+ total: int
109
+ limit: int
110
+ offset: int
111
+ has_more: bool
112
+
113
+
114
+ class PaginatedAgentsResponse(PaginatedResponse):
115
+ items: list[AgentSummaryResponse]
116
+
117
+
118
+ class PaginatedRunsResponse(PaginatedResponse):
119
+ items: list[AgentRunSummaryResponse]
120
+
121
+
122
+ class AgentStatsResponse(BaseModel):
123
+ total_agents: int
124
+ active_agents: int
125
+ total_runs_today: int
126
+ total_runs_this_month: int
127
+ avg_success_rate: float | None
128
+ avg_response_time_ms: float | None
129
+
130
+
@@ -0,0 +1,14 @@
1
+ """Deployment-related enums and schemas."""
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class DeploymentMode(str, Enum):
7
+ """Supported deployment modes."""
8
+
9
+ LOCAL = "local"
10
+ AWS = "aws"
11
+ AZURE = "azure"
12
+ GCP = "gcp"
13
+
14
+
@@ -0,0 +1,16 @@
1
+ """Shared dependency models used by FastAPI deps in Manager."""
2
+
3
+ from uuid import UUID
4
+
5
+ from pydantic import BaseModel
6
+
7
+
8
+ class Principal(BaseModel):
9
+ """Authenticated caller identity and authorization context."""
10
+
11
+ user_id: str
12
+ tenant_id: UUID
13
+ roles: list[str] = []
14
+ workspace_ids: list[UUID] = []
15
+
16
+
@@ -0,0 +1,261 @@
1
+ """Domain entities and enums for Agents and Tenants."""
2
+
3
+ from datetime import UTC, datetime
4
+ from enum import Enum
5
+ from typing import Any
6
+ from uuid import UUID, uuid4
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ class AgentStatus(str, Enum):
12
+ """Agent status enumeration."""
13
+
14
+ DRAFT = "draft"
15
+ ACTIVE = "active"
16
+ INACTIVE = "inactive"
17
+ DEPRECATED = "deprecated"
18
+ ERROR = "error"
19
+
20
+
21
+ class AgentFramework(str, Enum):
22
+ """Supported agent frameworks."""
23
+
24
+ LANGGRAPH = "langgraph"
25
+ CREWAI = "crewai"
26
+ AUTOGEN = "autogen"
27
+ CUSTOM = "custom"
28
+
29
+
30
+ class AgentEntity(BaseModel):
31
+ """Agent domain entity."""
32
+
33
+ id: UUID = Field(default_factory=uuid4)
34
+ name: str = Field(..., min_length=1, max_length=255)
35
+ description: str | None = Field(None, max_length=1000)
36
+ framework: AgentFramework
37
+ status: AgentStatus = Field(default=AgentStatus.DRAFT)
38
+
39
+ # Configuration
40
+ config: dict[str, Any] = Field(default_factory=dict)
41
+ environment_variables: dict[str, str] = Field(default_factory=dict)
42
+
43
+ # Metadata
44
+ version: str = Field(default="1.0.0")
45
+ tags: list[str] = Field(default_factory=list)
46
+
47
+ # Tenant isolation
48
+ tenant_id: UUID
49
+
50
+ # Timestamps
51
+ created_at: datetime
52
+ updated_at: datetime
53
+ deployed_at: datetime | None = None
54
+
55
+ # Performance metrics
56
+ total_runs: int = Field(default=0)
57
+ success_rate: float | None = Field(None, ge=0.0, le=1.0)
58
+ avg_response_time_ms: float | None = Field(None, ge=0)
59
+
60
+ def activate(self) -> None:
61
+ if self.status == AgentStatus.DRAFT:
62
+ self.status = AgentStatus.ACTIVE
63
+ self.deployed_at = datetime.now(UTC)
64
+ else:
65
+ raise ValueError(f"Cannot activate agent in {self.status} status")
66
+
67
+ def deactivate(self) -> None:
68
+ if self.status == AgentStatus.ACTIVE:
69
+ self.status = AgentStatus.INACTIVE
70
+ else:
71
+ raise ValueError(f"Cannot deactivate agent in {self.status} status")
72
+
73
+ def update_metrics(self, success: bool, response_time_ms: float) -> None:
74
+ self.total_runs += 1
75
+
76
+ if self.success_rate is None:
77
+ self.success_rate = 1.0 if success else 0.0
78
+ else:
79
+ current_successes = self.success_rate * (self.total_runs - 1)
80
+ if success:
81
+ current_successes += 1
82
+ self.success_rate = current_successes / self.total_runs
83
+
84
+ if self.avg_response_time_ms is None:
85
+ self.avg_response_time_ms = response_time_ms
86
+ else:
87
+ total_time = self.avg_response_time_ms * (self.total_runs - 1)
88
+ self.avg_response_time_ms = (total_time + response_time_ms) / self.total_runs
89
+
90
+ def can_be_deployed(self) -> bool:
91
+ return (
92
+ self.status in [AgentStatus.DRAFT, AgentStatus.INACTIVE]
93
+ and bool(self.name)
94
+ and bool(self.config)
95
+ )
96
+
97
+
98
+ class AgentRunEntity(BaseModel):
99
+ """Agent run domain entity."""
100
+
101
+ id: UUID = Field(default_factory=uuid4)
102
+ agent_id: UUID
103
+ tenant_id: UUID
104
+
105
+ # Input/Output
106
+ input_data: dict[str, Any]
107
+ output_data: dict[str, Any] | None = None
108
+
109
+ # Execution details
110
+ status: str # running, completed, failed
111
+ started_at: datetime
112
+ completed_at: datetime | None = None
113
+ error_message: str | None = None
114
+
115
+ # Performance
116
+ response_time_ms: float | None = None
117
+ tokens_used: int | None = None
118
+ cost_usd: float | None = None
119
+
120
+ # Tracing
121
+ trace_id: str | None = None
122
+ span_id: str | None = None
123
+
124
+ def complete(self, output_data: dict[str, Any], response_time_ms: float) -> None:
125
+ self.status = "completed"
126
+ self.output_data = output_data
127
+ self.completed_at = datetime.now(UTC)
128
+ self.response_time_ms = response_time_ms
129
+
130
+ def fail(self, error_message: str) -> None:
131
+ self.status = "failed"
132
+ self.error_message = error_message
133
+ self.completed_at = datetime.now(UTC)
134
+
135
+ @property
136
+ def is_completed(self) -> bool:
137
+ return self.status in ["completed", "failed"]
138
+
139
+ @property
140
+ def was_successful(self) -> bool:
141
+ return self.status == "completed"
142
+
143
+
144
+ class TenantStatus(str, Enum):
145
+ """Tenant status enumeration."""
146
+
147
+ ACTIVE = "active"
148
+ SUSPENDED = "suspended"
149
+ DEACTIVATED = "deactivated"
150
+
151
+
152
+ class TenantPlan(str, Enum):
153
+ """Tenant subscription plan."""
154
+
155
+ FREE = "free"
156
+ STARTER = "starter"
157
+ PROFESSIONAL = "professional"
158
+ ENTERPRISE = "enterprise"
159
+
160
+
161
+ class TenantEntity(BaseModel):
162
+ """Tenant domain entity."""
163
+
164
+ id: UUID = Field(default_factory=uuid4)
165
+ name: str = Field(..., min_length=1, max_length=255)
166
+ slug: str = Field(..., min_length=1, max_length=100)
167
+
168
+ # Contact information
169
+ email: str = Field(..., description="Primary contact email")
170
+ website: str | None = Field(None)
171
+
172
+ # Status and plan
173
+ status: TenantStatus = Field(default=TenantStatus.ACTIVE)
174
+ plan: TenantPlan = Field(default=TenantPlan.FREE)
175
+
176
+ # Settings
177
+ settings: dict[str, Any] = Field(default_factory=dict)
178
+
179
+ # Quotas and limits
180
+ max_agents: int = Field(default=5)
181
+ max_runs_per_month: int = Field(default=1000)
182
+ max_storage_mb: int = Field(default=100)
183
+
184
+ # Usage tracking
185
+ current_agents: int = Field(default=0)
186
+ current_runs_this_month: int = Field(default=0)
187
+ current_storage_mb: float = Field(default=0.0)
188
+
189
+ # Timestamps
190
+ created_at: datetime
191
+ updated_at: datetime
192
+ suspended_at: datetime | None = None
193
+
194
+ def can_create_agent(self) -> bool:
195
+ return self.status == TenantStatus.ACTIVE and self.current_agents < self.max_agents
196
+
197
+ def can_run_agent(self) -> bool:
198
+ return self.status == TenantStatus.ACTIVE and self.current_runs_this_month < self.max_runs_per_month
199
+
200
+ def suspend(self, reason: str) -> None:
201
+ self.status = TenantStatus.SUSPENDED
202
+ self.suspended_at = datetime.utcnow()
203
+ self.settings["suspension_reason"] = reason
204
+
205
+ def reactivate(self) -> None:
206
+ if self.status == TenantStatus.SUSPENDED:
207
+ self.status = TenantStatus.ACTIVE
208
+ self.suspended_at = None
209
+ if "suspension_reason" in self.settings:
210
+ del self.settings["suspension_reason"]
211
+
212
+ def upgrade_plan(self, new_plan: TenantPlan) -> None:
213
+ self.plan = new_plan
214
+ if new_plan == TenantPlan.STARTER:
215
+ self.max_agents = 20
216
+ self.max_runs_per_month = 10000
217
+ self.max_storage_mb = 1000
218
+ elif new_plan == TenantPlan.PROFESSIONAL:
219
+ self.max_agents = 100
220
+ self.max_runs_per_month = 100000
221
+ self.max_storage_mb = 10000
222
+ elif new_plan == TenantPlan.ENTERPRISE:
223
+ self.max_agents = 1000
224
+ self.max_runs_per_month = 1000000
225
+ self.max_storage_mb = 100000
226
+
227
+ def increment_usage(self, agents: int = 0, runs: int = 0, storage_mb: float = 0) -> None:
228
+ self.current_agents += agents
229
+ self.current_runs_this_month += runs
230
+ self.current_storage_mb += storage_mb
231
+
232
+ def reset_monthly_usage(self) -> None:
233
+ self.current_runs_this_month = 0
234
+
235
+
236
+ class TenantUserEntity(BaseModel):
237
+ """Tenant user domain entity for multi-user tenants."""
238
+
239
+ id: UUID = Field(default_factory=uuid4)
240
+ tenant_id: UUID
241
+ user_id: str
242
+ email: str
243
+
244
+ # Role and permissions
245
+ role: str = Field(default="member")
246
+ permissions: list[str] = Field(default_factory=list)
247
+
248
+ # Status
249
+ is_active: bool = Field(default=True)
250
+
251
+ # Timestamps
252
+ joined_at: datetime
253
+ last_active_at: datetime | None = None
254
+
255
+ def has_permission(self, permission: str) -> bool:
256
+ return permission in self.permissions or self.role == "owner"
257
+
258
+ def is_admin(self) -> bool:
259
+ return self.role in ["owner", "admin"]
260
+
261
+
@@ -0,0 +1,109 @@
1
+ """DTOs for Manager operations."""
2
+
3
+ from datetime import datetime
4
+ from typing import Any
5
+ from uuid import UUID
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from .domain import AgentFramework, TenantPlan
10
+
11
+
12
+ class AgentCreateDTO(BaseModel):
13
+ name: str = Field(..., min_length=1, max_length=255)
14
+ description: str | None = Field(None, max_length=1000)
15
+ framework: AgentFramework
16
+ config: dict[str, Any] = Field(default_factory=dict)
17
+ environment_variables: dict[str, str] = Field(default_factory=dict)
18
+ tags: list[str] = Field(default_factory=list)
19
+ tenant_id: UUID
20
+
21
+
22
+ class AgentUpdateDTO(BaseModel):
23
+ name: str | None = Field(None, min_length=1, max_length=255)
24
+ description: str | None = Field(None, max_length=1000)
25
+ config: dict[str, Any] | None = None
26
+ environment_variables: dict[str, str] | None = None
27
+ tags: list[str] | None = None
28
+
29
+
30
+ class AgentDeploymentDTO(BaseModel):
31
+ agent_id: UUID
32
+ container_id: str
33
+ endpoint: str
34
+ status: str
35
+ framework: str
36
+ deployed_at: datetime | None = None
37
+
38
+
39
+ class AgentHealthDTO(BaseModel):
40
+ agent_id: UUID
41
+ status: str
42
+ uptime: str | None = None
43
+ cpu_usage: str | None = None
44
+ memory_usage: str | None = None
45
+ last_activity: str | None = None
46
+ error: str | None = None
47
+
48
+
49
+ class AgentRunCreateDTO(BaseModel):
50
+ agent_id: UUID
51
+ tenant_id: UUID
52
+ input_data: dict[str, Any]
53
+ trace_id: str | None = None
54
+
55
+
56
+ class AgentMetricsDTO(BaseModel):
57
+ agent_id: UUID
58
+ total_runs: int
59
+ success_rate: float | None = Field(None, ge=0.0, le=1.0)
60
+ avg_response_time_ms: float | None = Field(None, ge=0)
61
+ last_run_at: datetime | None = None
62
+
63
+
64
+ class TenantCreateDTO(BaseModel):
65
+ name: str = Field(..., min_length=1, max_length=255)
66
+ slug: str = Field(..., min_length=1, max_length=100)
67
+ email: str = Field(..., description="Primary contact email")
68
+ website: str | None = None
69
+ plan: TenantPlan = Field(default=TenantPlan.FREE)
70
+
71
+
72
+ class TenantUpdateDTO(BaseModel):
73
+ name: str | None = Field(None, min_length=1, max_length=255)
74
+ email: str | None = None
75
+ website: str | None = None
76
+ settings: dict[str, Any] | None = None
77
+
78
+
79
+ class TenantUsageDTO(BaseModel):
80
+ tenant_id: UUID
81
+ current_agents: int
82
+ max_agents: int
83
+ current_runs_this_month: int
84
+ max_runs_per_month: int
85
+ current_storage_mb: float
86
+ max_storage_mb: int
87
+ usage_percentage: dict[str, float]
88
+
89
+
90
+ class TenantQuotaDTO(BaseModel):
91
+ max_agents: int | None = None
92
+ max_runs_per_month: int | None = None
93
+ max_storage_mb: int | None = None
94
+
95
+
96
+ class TenantUserCreateDTO(BaseModel):
97
+ tenant_id: UUID
98
+ user_id: str
99
+ email: str
100
+ role: str = Field(default="member")
101
+ permissions: list[str] = Field(default_factory=list)
102
+
103
+
104
+ class TenantUserUpdateDTO(BaseModel):
105
+ role: str | None = None
106
+ permissions: list[str] | None = None
107
+ is_active: bool | None = None
108
+
109
+
@@ -0,0 +1,24 @@
1
+ """RFC 9457 Problem Details model (shared)."""
2
+
3
+ from typing import Any
4
+
5
+ from pydantic import BaseModel, Field
6
+
7
+
8
+ class ProblemDetail(BaseModel):
9
+ """RFC 9457 Problem Details model."""
10
+
11
+ type: str = Field(default="about:blank")
12
+ title: str = Field()
13
+ status: int = Field()
14
+ detail: str | None = Field(default=None)
15
+ instance: str | None = Field(default=None)
16
+
17
+ # Extension members
18
+ timestamp: str | None = Field(default=None)
19
+ request_id: str | None = Field(default=None)
20
+ errors: dict[str, Any] | None = Field(default=None)
21
+
22
+ model_config = {"extra": "allow"}
23
+
24
+
@@ -0,0 +1,139 @@
1
+ """Application settings schemas using Pydantic Settings v2."""
2
+
3
+ from typing import Literal
4
+
5
+ from pydantic import Field, field_validator
6
+ from pydantic_settings import BaseSettings, SettingsConfigDict
7
+
8
+
9
+ class DatabaseSettings(BaseSettings):
10
+ url: str = Field(default="postgresql+asyncpg://postgres:postgres@localhost:55432/idun_agents")
11
+ echo: bool = Field(default=False)
12
+ pool_size: int = Field(default=10)
13
+ max_overflow: int = Field(default=20)
14
+ pool_pre_ping: bool = Field(default=True)
15
+
16
+ model_config = SettingsConfigDict(env_prefix="DATABASE_", env_file=".env")
17
+
18
+
19
+ class RedisSettings(BaseSettings):
20
+ url: str = Field(default="redis://localhost:6379/0")
21
+ max_connections: int = Field(default=20)
22
+
23
+ model_config = SettingsConfigDict(env_prefix="REDIS_", env_file=".env")
24
+
25
+
26
+ class AuthSettings(BaseSettings):
27
+ provider_type: Literal["okta", "auth0", "entra", "google"] = Field(default="auth0")
28
+ issuer: str = Field(default="")
29
+ client_id: str = Field(default="")
30
+ client_secret: str = Field(default="")
31
+ audience: str | None = Field(default=None)
32
+ redirect_uri: str | None = Field(default=None)
33
+ scopes: list[str] = Field(default_factory=lambda: ["openid", "profile", "email"])
34
+
35
+ allowed_algs: list[str] = Field(default_factory=lambda: ["RS256", "RS512", "ES256"])
36
+ jwks_cache_ttl: int = Field(default=300)
37
+ clock_skew_seconds: int = Field(default=60)
38
+ expected_audiences: list[str] = Field(default_factory=list)
39
+
40
+ claim_user_id_path: list[str] | None = Field(default=None)
41
+ claim_email_path: list[str] | None = Field(default=None)
42
+ claim_roles_paths: list[list[str]] | None = Field(default=None)
43
+ claim_groups_paths: list[list[str]] | None = Field(default=None)
44
+ claim_workspace_ids_paths: list[list[str]] | None = Field(default=None)
45
+
46
+ @field_validator("claim_user_id_path", "claim_email_path", mode="before")
47
+ @classmethod
48
+ def _string_to_list(cls, v):
49
+ if isinstance(v, str) and v.strip().startswith("#"):
50
+ return None
51
+ return v
52
+
53
+ @field_validator(
54
+ "claim_roles_paths",
55
+ "claim_groups_paths",
56
+ "claim_workspace_ids_paths",
57
+ mode="before",
58
+ )
59
+ @classmethod
60
+ def _string_to_list_of_lists(cls, v):
61
+ if isinstance(v, str) and v.strip().startswith("#"):
62
+ return None
63
+ return v
64
+
65
+ model_config = SettingsConfigDict(env_prefix="AUTH_", env_file=".env", extra="ignore")
66
+
67
+ @field_validator("issuer")
68
+ @classmethod
69
+ def validate_issuer(cls, v: str) -> str:
70
+ if not v:
71
+ return v
72
+ if not v.startswith("http"):
73
+ raise ValueError("issuer must be a URL")
74
+ return v
75
+
76
+
77
+ class ObservabilitySettings(BaseSettings):
78
+ otel_service_name: str = Field(default="idun-agent-manager")
79
+ otel_exporter_endpoint: str | None = Field(default=None)
80
+ otel_exporter_headers: str | None = Field(default=None)
81
+ log_level: str = Field(default="INFO")
82
+ log_format: str = Field(default="json")
83
+
84
+ model_config = SettingsConfigDict(env_prefix="OTEL_", env_file=".env")
85
+
86
+
87
+ class CelerySettings(BaseSettings):
88
+ broker_url: str = Field(default="redis://localhost:6379/1")
89
+ result_backend: str = Field(default="redis://localhost:6379/2")
90
+ task_serializer: str = Field(default="json")
91
+ result_serializer: str = Field(default="json")
92
+ accept_content: list[str] = Field(default=["json"])
93
+ timezone: str = Field(default="UTC")
94
+
95
+ model_config = SettingsConfigDict(env_prefix="CELERY_", env_file=".env")
96
+
97
+
98
+ class APISettings(BaseSettings):
99
+ title: str = Field(default="Idun Agent Manager API")
100
+ description: str = Field(default="Modern FastAPI backend for managing AI agents")
101
+ version: str = Field(default="0.1.0")
102
+ docs_url: str = Field(default="/docs")
103
+ redoc_url: str = Field(default="/redoc")
104
+ openapi_url: str = Field(default="/openapi.json")
105
+ cors_origins: list[str] = Field(default=["http://localhost:3000", "http://localhost:8080"])
106
+ cors_methods: list[str] = Field(default=["*"])
107
+ cors_headers: list[str] = Field(default=["*"])
108
+ rate_limit_enabled: bool = Field(default=True)
109
+ rate_limit_requests: int = Field(default=100)
110
+ rate_limit_window: int = Field(default=60)
111
+
112
+ model_config = SettingsConfigDict(env_prefix="API_", env_file=".env", extra="ignore")
113
+
114
+
115
+ class Settings(BaseSettings):
116
+ environment: str = Field(default="development")
117
+ debug: bool = Field(default=False)
118
+ testing: bool = Field(default=False)
119
+ host: str = Field(default="0.0.0.0")
120
+ port: int = Field(default=8000)
121
+ workers: int = Field(default=1)
122
+ reload: bool = Field(default=False)
123
+
124
+ database: DatabaseSettings = Field(default_factory=DatabaseSettings)
125
+ redis: RedisSettings = Field(default_factory=RedisSettings)
126
+ auth: AuthSettings = Field(default_factory=AuthSettings)
127
+ observability: ObservabilitySettings = Field(default_factory=ObservabilitySettings)
128
+ celery: CelerySettings = Field(default_factory=CelerySettings)
129
+ api: APISettings = Field(default_factory=APISettings)
130
+
131
+ model_config = SettingsConfigDict(
132
+ env_file=".env",
133
+ env_file_encoding="utf-8",
134
+ env_nested_delimiter="__",
135
+ case_sensitive=False,
136
+ extra="ignore",
137
+ )
138
+
139
+
@@ -0,0 +1,5 @@
1
+ """Shared cross-cutting schemas used by both Engine and Manager."""
2
+
3
+ from .observability import ObservabilityConfig
4
+
5
+ __all__ = ["ObservabilityConfig"]
@@ -0,0 +1,58 @@
1
+ """Provider-agnostic observability configuration model (shared)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from typing import Any
7
+
8
+ from pydantic import BaseModel, Field
9
+
10
+
11
+ def _resolve_env(value: Any) -> Any:
12
+ """Resolve environment placeholders in strings.
13
+
14
+ Supports patterns ${VAR} and $VAR. Non-strings are returned unchanged.
15
+ """
16
+ if isinstance(value, str):
17
+ if value.startswith("${") and value.endswith("}"):
18
+ return os.getenv(value[2:-1])
19
+ if value.startswith("$"):
20
+ return os.getenv(value[1:])
21
+ return value
22
+
23
+
24
+ class ObservabilityConfig(BaseModel):
25
+ """Provider-agnostic observability configuration based on Pydantic.
26
+
27
+ Example YAML:
28
+ observability:
29
+ provider: "langfuse" # or "phoenix"
30
+ enabled: true
31
+ options:
32
+ host: ${LANGFUSE_HOST}
33
+ public_key: ${LANGFUSE_PUBLIC_KEY}
34
+ secret_key: ${LANGFUSE_SECRET_KEY}
35
+ run_name: "my-run"
36
+ """
37
+
38
+ provider: str | None = Field(default=None)
39
+ enabled: bool = Field(default=False)
40
+ options: dict[str, Any] = Field(default_factory=dict)
41
+
42
+ def _resolve_value(self, value: Any) -> Any:
43
+ if isinstance(value, dict):
44
+ return {k: self._resolve_value(v) for k, v in value.items()}
45
+ if isinstance(value, list):
46
+ return [self._resolve_value(v) for v in value]
47
+ return _resolve_env(value)
48
+
49
+ def resolved(self) -> ObservabilityConfig:
50
+ """Return a copy with env placeholders resolved in options."""
51
+ resolved_options = self._resolve_value(self.options)
52
+ return ObservabilityConfig(
53
+ provider=self.provider,
54
+ enabled=self.enabled,
55
+ options=resolved_options,
56
+ )
57
+
58
+