icogito 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.
icogito-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,193 @@
1
+ Metadata-Version: 2.3
2
+ Name: icogito
3
+ Version: 0.1.0
4
+ Summary: A modern Python CLI framework to build Agent/Multi-Agents systems
5
+ Author: Daniel Rondon
6
+ Author-email: Daniel Rondon <hello@danielrondon.com>
7
+ Requires-Dist: graphifyy>=0.9.67
8
+ Requires-Dist: loguru>=0.7.3
9
+ Requires-Dist: pydantic>=2.13.5
10
+ Requires-Dist: pydantic-ai>=2.48.0
11
+ Requires-Dist: pydantic-ai-harness>=0.34.0
12
+ Requires-Dist: pyyaml>=6.0.3
13
+ Requires-Dist: questionary>=2.1.1
14
+ Requires-Dist: tavily-python>=0.8.4
15
+ Requires-Dist: typer>=0.27.2
16
+ Requires-Dist: pytest>=8.0.0 ; extra == 'test'
17
+ Requires-Dist: pytest-mock>=3.12.0 ; extra == 'test'
18
+ Requires-Python: >=3.12
19
+ Provides-Extra: test
20
+ Description-Content-Type: text/markdown
21
+
22
+ # icogito
23
+
24
+ > A modern Python CLI framework to build Agent/Multi-Agent systems on top of `pydantic-ai`, `pydantic-ai-harness`, OpenRouter, and Tavily tools.
25
+
26
+ `icogito` provides an end-to-end toolkit for scaffolding, configuring, and running production-grade AI agent and multi-agent architectures. It integrates durable execution via Prefect, modular tool registry integration (including Tavily search, fetch, and crawl tools), and OpenRouter model routing.
27
+
28
+ ---
29
+
30
+ ## Key Features
31
+
32
+ - **CLI-Driven Scaffolding**: Easily create standardized directory structures (`agents/`, `configs/`, `prompts/`, `schemas/`, `tools/`, `utils/`) using `icogito init`.
33
+ - **Interactive Configuration**: Build single-agent and multi-agent workflow configurations interactively with `icogito configure`.
34
+ - **System Diagnostics**: Verify tools, environment setup, and framework status with `icogito doctor`.
35
+ - **Agent Factory**: Instantiates `pydantic-ai` Agents dynamically from YAML configurations (`AgentConfig`).
36
+ - **Durable Execution & Subagents**: Supports task durability via `PrefectDurability` and hierarchical multi-agent delegation via `SubAgents`.
37
+ - **Built-in Web Tools**: Tavily web integration for search, fetch, and crawl capabilities with built-in retry handling.
38
+
39
+ ---
40
+
41
+ ## Get Started
42
+
43
+ ### Prerequisites
44
+
45
+ - **Python**: Version `>= 3.12`
46
+ - **uv**: Recommended fast Python package and environment manager ([docs.astral.sh/uv](https://docs.astral.sh/uv/))
47
+ - **API Keys**:
48
+ - `OPENROUTER_API_KEY`: Required for model inference via OpenRouter.
49
+ - `TAVILY_API_KEY`: Required when using Tavily web tools (`web_search`, `web_fetch`, `web_crawl`).
50
+
51
+ ### Installation
52
+
53
+ Clone the repository and install dependencies using `uv`:
54
+
55
+ ```bash
56
+ # Clone the repository
57
+ git clone https://github.com/danielrondon/icogito.git
58
+ cd icogito
59
+
60
+ # Sync project dependencies into .venv
61
+ uv sync
62
+
63
+ # (Optional) Sync test extra dependencies if developing or running tests
64
+ uv sync --extra test
65
+ ```
66
+
67
+ ### Local Environment Setup
68
+
69
+ Configure your environment variables before running agent workflows or tools:
70
+
71
+ ```bash
72
+ export OPENROUTER_API_KEY="your-openrouter-api-key"
73
+ export TAVILY_API_KEY="your-tavily-api-key"
74
+ ```
75
+
76
+ ---
77
+
78
+ ## Usage
79
+
80
+ ### CLI Commands
81
+
82
+ The `icogito` CLI provides commands for project initialization, diagnostics, and agent configuration.
83
+
84
+ #### 1. Initialize Project Scaffolding (`icogito init`)
85
+
86
+ Scaffold a standard agent directory structure in your project or a target directory:
87
+
88
+ ```bash
89
+ # Initialize scaffolding in the current directory
90
+ uv run icogito init
91
+
92
+ # Or specify a target directory
93
+ uv run icogito init ./my_agent_project
94
+ ```
95
+
96
+ This creates the following directory layout:
97
+ ```
98
+ .
99
+ ├── agents/
100
+ ├── configs/
101
+ ├── prompts/
102
+ ├── schemas/
103
+ ├── tools/
104
+ └── utils/
105
+ ```
106
+
107
+ #### 2. System Diagnostics (`icogito doctor`)
108
+
109
+ Check system environment status, global app directory path, available Tavily web tools, and `AgentFactory` readiness:
110
+
111
+ ```bash
112
+ # Run standard diagnostic check
113
+ uv run icogito doctor
114
+
115
+ # Verbose diagnostic output
116
+ uv run icogito doctor --verbose
117
+ ```
118
+
119
+ #### 3. Interactive Agent Configuration (`icogito configure`)
120
+
121
+ Interactively configure single or multi-agent systems. This wizard prompts for agent names, OpenRouter models, providers, instructions, enabled tools, and subagents, then generates the corresponding YAML configs in `configs/` and Markdown instructions in `prompts/`:
122
+
123
+ ```bash
124
+ uv run icogito configure
125
+ ```
126
+
127
+ ---
128
+
129
+ ### Programmatic Usage
130
+
131
+ `icogito` provides high-level utilities and `AgentFactory` for loading configs and instantiating runtime agents.
132
+
133
+ #### Loading Configuration and Instantiating Agents
134
+
135
+ ```python
136
+ import os
137
+ from icogito.factory import AgentFactory
138
+ from icogito.utils.load_agent_config import load_agent_config
139
+
140
+ # Load agent configuration from YAML
141
+ config = load_agent_config("configs/research_agent_config.yaml")
142
+
143
+ # Initialize AgentFactory with your OpenRouter API Key
144
+ factory = AgentFactory(openrouter_api_key=os.getenv("OPENROUTER_API_KEY"))
145
+
146
+ # Create a pydantic-ai Agent instance
147
+ agent = factory.create_agent(config)
148
+
149
+ # Run the agent (async)
150
+ # result = await agent.run("Research recent developments in AI agent frameworks.")
151
+ ```
152
+
153
+ #### Multi-Agent Setup with Subagents
154
+
155
+ When building a multi-agent system, subagent configurations are registered with `AgentFactory`:
156
+
157
+ ```python
158
+ from icogito.factory import AgentFactory
159
+ from icogito.utils.load_agent_config import load_agent_config
160
+
161
+ # Load root agent config and subagent configs
162
+ sub_config = load_agent_config("configs/sub_researcher_config.yaml")
163
+ root_config = load_agent_config("configs/root_manager_config.yaml")
164
+
165
+ # Register subagent configs map
166
+ subagent_configs = {
167
+ sub_config.agent_name: sub_config
168
+ }
169
+
170
+ factory = AgentFactory(
171
+ openrouter_api_key=os.getenv("OPENROUTER_API_KEY"),
172
+ subagent_configs=subagent_configs
173
+ )
174
+
175
+ # Build the root agent (automatically wires subagents recursively)
176
+ root_agent = factory.create_agent(root_config)
177
+ ```
178
+
179
+ ---
180
+
181
+ ## Development & Testing
182
+
183
+ To run the full test suite using `pytest`:
184
+
185
+ ```bash
186
+ uv run --extra test pytest
187
+ ```
188
+
189
+ ---
190
+
191
+ ## Authors
192
+
193
+ - **Daniel Rondon** — [hello@danielrondon.com](mailto:hello@danielrondon.com)
@@ -0,0 +1,172 @@
1
+ # icogito
2
+
3
+ > A modern Python CLI framework to build Agent/Multi-Agent systems on top of `pydantic-ai`, `pydantic-ai-harness`, OpenRouter, and Tavily tools.
4
+
5
+ `icogito` provides an end-to-end toolkit for scaffolding, configuring, and running production-grade AI agent and multi-agent architectures. It integrates durable execution via Prefect, modular tool registry integration (including Tavily search, fetch, and crawl tools), and OpenRouter model routing.
6
+
7
+ ---
8
+
9
+ ## Key Features
10
+
11
+ - **CLI-Driven Scaffolding**: Easily create standardized directory structures (`agents/`, `configs/`, `prompts/`, `schemas/`, `tools/`, `utils/`) using `icogito init`.
12
+ - **Interactive Configuration**: Build single-agent and multi-agent workflow configurations interactively with `icogito configure`.
13
+ - **System Diagnostics**: Verify tools, environment setup, and framework status with `icogito doctor`.
14
+ - **Agent Factory**: Instantiates `pydantic-ai` Agents dynamically from YAML configurations (`AgentConfig`).
15
+ - **Durable Execution & Subagents**: Supports task durability via `PrefectDurability` and hierarchical multi-agent delegation via `SubAgents`.
16
+ - **Built-in Web Tools**: Tavily web integration for search, fetch, and crawl capabilities with built-in retry handling.
17
+
18
+ ---
19
+
20
+ ## Get Started
21
+
22
+ ### Prerequisites
23
+
24
+ - **Python**: Version `>= 3.12`
25
+ - **uv**: Recommended fast Python package and environment manager ([docs.astral.sh/uv](https://docs.astral.sh/uv/))
26
+ - **API Keys**:
27
+ - `OPENROUTER_API_KEY`: Required for model inference via OpenRouter.
28
+ - `TAVILY_API_KEY`: Required when using Tavily web tools (`web_search`, `web_fetch`, `web_crawl`).
29
+
30
+ ### Installation
31
+
32
+ Clone the repository and install dependencies using `uv`:
33
+
34
+ ```bash
35
+ # Clone the repository
36
+ git clone https://github.com/danielrondon/icogito.git
37
+ cd icogito
38
+
39
+ # Sync project dependencies into .venv
40
+ uv sync
41
+
42
+ # (Optional) Sync test extra dependencies if developing or running tests
43
+ uv sync --extra test
44
+ ```
45
+
46
+ ### Local Environment Setup
47
+
48
+ Configure your environment variables before running agent workflows or tools:
49
+
50
+ ```bash
51
+ export OPENROUTER_API_KEY="your-openrouter-api-key"
52
+ export TAVILY_API_KEY="your-tavily-api-key"
53
+ ```
54
+
55
+ ---
56
+
57
+ ## Usage
58
+
59
+ ### CLI Commands
60
+
61
+ The `icogito` CLI provides commands for project initialization, diagnostics, and agent configuration.
62
+
63
+ #### 1. Initialize Project Scaffolding (`icogito init`)
64
+
65
+ Scaffold a standard agent directory structure in your project or a target directory:
66
+
67
+ ```bash
68
+ # Initialize scaffolding in the current directory
69
+ uv run icogito init
70
+
71
+ # Or specify a target directory
72
+ uv run icogito init ./my_agent_project
73
+ ```
74
+
75
+ This creates the following directory layout:
76
+ ```
77
+ .
78
+ ├── agents/
79
+ ├── configs/
80
+ ├── prompts/
81
+ ├── schemas/
82
+ ├── tools/
83
+ └── utils/
84
+ ```
85
+
86
+ #### 2. System Diagnostics (`icogito doctor`)
87
+
88
+ Check system environment status, global app directory path, available Tavily web tools, and `AgentFactory` readiness:
89
+
90
+ ```bash
91
+ # Run standard diagnostic check
92
+ uv run icogito doctor
93
+
94
+ # Verbose diagnostic output
95
+ uv run icogito doctor --verbose
96
+ ```
97
+
98
+ #### 3. Interactive Agent Configuration (`icogito configure`)
99
+
100
+ Interactively configure single or multi-agent systems. This wizard prompts for agent names, OpenRouter models, providers, instructions, enabled tools, and subagents, then generates the corresponding YAML configs in `configs/` and Markdown instructions in `prompts/`:
101
+
102
+ ```bash
103
+ uv run icogito configure
104
+ ```
105
+
106
+ ---
107
+
108
+ ### Programmatic Usage
109
+
110
+ `icogito` provides high-level utilities and `AgentFactory` for loading configs and instantiating runtime agents.
111
+
112
+ #### Loading Configuration and Instantiating Agents
113
+
114
+ ```python
115
+ import os
116
+ from icogito.factory import AgentFactory
117
+ from icogito.utils.load_agent_config import load_agent_config
118
+
119
+ # Load agent configuration from YAML
120
+ config = load_agent_config("configs/research_agent_config.yaml")
121
+
122
+ # Initialize AgentFactory with your OpenRouter API Key
123
+ factory = AgentFactory(openrouter_api_key=os.getenv("OPENROUTER_API_KEY"))
124
+
125
+ # Create a pydantic-ai Agent instance
126
+ agent = factory.create_agent(config)
127
+
128
+ # Run the agent (async)
129
+ # result = await agent.run("Research recent developments in AI agent frameworks.")
130
+ ```
131
+
132
+ #### Multi-Agent Setup with Subagents
133
+
134
+ When building a multi-agent system, subagent configurations are registered with `AgentFactory`:
135
+
136
+ ```python
137
+ from icogito.factory import AgentFactory
138
+ from icogito.utils.load_agent_config import load_agent_config
139
+
140
+ # Load root agent config and subagent configs
141
+ sub_config = load_agent_config("configs/sub_researcher_config.yaml")
142
+ root_config = load_agent_config("configs/root_manager_config.yaml")
143
+
144
+ # Register subagent configs map
145
+ subagent_configs = {
146
+ sub_config.agent_name: sub_config
147
+ }
148
+
149
+ factory = AgentFactory(
150
+ openrouter_api_key=os.getenv("OPENROUTER_API_KEY"),
151
+ subagent_configs=subagent_configs
152
+ )
153
+
154
+ # Build the root agent (automatically wires subagents recursively)
155
+ root_agent = factory.create_agent(root_config)
156
+ ```
157
+
158
+ ---
159
+
160
+ ## Development & Testing
161
+
162
+ To run the full test suite using `pytest`:
163
+
164
+ ```bash
165
+ uv run --extra test pytest
166
+ ```
167
+
168
+ ---
169
+
170
+ ## Authors
171
+
172
+ - **Daniel Rondon** — [hello@danielrondon.com](mailto:hello@danielrondon.com)
@@ -0,0 +1,64 @@
1
+ [project]
2
+ name = "icogito"
3
+ version = "0.1.0"
4
+ description = "A modern Python CLI framework to build Agent/Multi-Agents systems"
5
+ readme = "README.md"
6
+ requires-python = ">=3.12"
7
+ dependencies = [
8
+ "graphifyy>=0.9.67",
9
+ "loguru>=0.7.3",
10
+ "pydantic>=2.13.5",
11
+ "pydantic-ai>=2.48.0",
12
+ "pydantic-ai-harness>=0.34.0",
13
+ "pyyaml>=6.0.3",
14
+ "questionary>=2.1.1",
15
+ "tavily-python>=0.8.4",
16
+ "typer>=0.27.2",
17
+ ]
18
+
19
+ [[project.authors]]
20
+ name = "Daniel Rondon"
21
+ email = "hello@danielrondon.com"
22
+
23
+ [project.optional-dependencies]
24
+ test = [
25
+ "pytest>=8.0.0",
26
+ "pytest-mock>=3.12.0",
27
+ ]
28
+
29
+ [project.scripts]
30
+ icogito = "icogito.cli:main"
31
+
32
+ [tool.pytest.ini_options]
33
+ pythonpath = ["src"]
34
+ testpaths = ["tests"]
35
+
36
+ [tool.mypy]
37
+ python_version = "3.12"
38
+ files = [
39
+ "src",
40
+ "tests",
41
+ ]
42
+ warn_unused_ignores = true
43
+ warn_return_any = true
44
+ check_untyped_defs = true
45
+
46
+ [[tool.mypy.overrides]]
47
+ module = [
48
+ "tavily",
49
+ "tavily.*",
50
+ ]
51
+ ignore_missing_imports = true
52
+
53
+ [build-system]
54
+ requires = ["uv_build>=0.12.14,<0.14.0"]
55
+ build-backend = "uv_build"
56
+
57
+ [dependency-groups]
58
+ dev = [
59
+ "mypy>=2.4.0",
60
+ "pytest>=8.0.0",
61
+ "pytest-mock>=3.12.0",
62
+ "types-pyyaml>=6.0.12.20260906",
63
+ "types-requests>=2.33.0.20261006",
64
+ ]
@@ -0,0 +1,57 @@
1
+ [project]
2
+ name = "icogito"
3
+ version = "0.1.0"
4
+ description = "A modern Python CLI framework to build Agent/Multi-Agents systems"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Daniel Rondon", email = "hello@danielrondon.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "graphifyy>=0.9.67",
12
+ "loguru>=0.7.3",
13
+ "pydantic>=2.13.5",
14
+ "pydantic-ai>=2.48.0",
15
+ "pydantic-ai-harness>=0.34.0",
16
+ "pyyaml>=6.0.3",
17
+ "questionary>=2.1.1",
18
+ "tavily-python>=0.8.4",
19
+ "typer>=0.27.2",
20
+ ]
21
+
22
+ [project.optional-dependencies]
23
+ test = [
24
+ "pytest>=8.0.0",
25
+ "pytest-mock>=3.12.0",
26
+ ]
27
+
28
+ [project.scripts]
29
+ icogito = "icogito.cli:main"
30
+
31
+ [tool.pytest.ini_options]
32
+ pythonpath = ["src"]
33
+ testpaths = ["tests"]
34
+
35
+ [build-system]
36
+ requires = ["uv_build>=0.12.14,<0.14.0"]
37
+ build-backend = "uv_build"
38
+
39
+ [tool.mypy]
40
+ python_version = "3.12"
41
+ files = ["src", "tests"]
42
+ warn_unused_ignores = true
43
+ warn_return_any = true
44
+ check_untyped_defs = true
45
+
46
+ [[tool.mypy.overrides]]
47
+ module = ["tavily", "tavily.*"]
48
+ ignore_missing_imports = true
49
+
50
+ [dependency-groups]
51
+ dev = [
52
+ "mypy>=2.4.0",
53
+ "pytest>=8.0.0",
54
+ "pytest-mock>=3.12.0",
55
+ "types-pyyaml>=6.0.12.20260906",
56
+ "types-requests>=2.33.0.20261006",
57
+ ]
@@ -0,0 +1,6 @@
1
+ # src/icogito/__init__.py
2
+ from .factory import AgentFactory
3
+ from .models import AgentConfig
4
+ from .core import create_scaffolding
5
+
6
+ __all__ = ["AgentFactory", "AgentConfig", "create_scaffolding"]
@@ -0,0 +1,168 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+ import typer
4
+ import questionary
5
+
6
+ from icogito import core
7
+ from icogito.models import AgentConfig
8
+
9
+ APP_NAME = "icogito"
10
+
11
+
12
+ def get_global_app_dir() -> Path:
13
+ """Returns the global OS-appropriate application directory for icogito."""
14
+ app_dir = Path(typer.get_app_dir(APP_NAME))
15
+ app_dir.mkdir(parents=True, exist_ok=True)
16
+ return app_dir
17
+
18
+
19
+ app = typer.Typer(
20
+ name="icogito-cli",
21
+ help="A modern Python CLI framework to build Agent/Multi-Agents systems.",
22
+ add_completion=True,
23
+ )
24
+
25
+
26
+ @app.command()
27
+ def init(
28
+ target_dir: Path = typer.Argument(
29
+ default=Path.cwd(),
30
+ help="Target directory for the new project",
31
+ ),
32
+ force: bool = typer.Option(False, "--force", "-f", help="Force re-initialization"),
33
+ ):
34
+ """
35
+ Initialize directory scaffolding for a new agent project.
36
+ """
37
+ typer.secho(f"Initializing agent scaffolding at {target_dir}...", fg=typer.colors.GREEN)
38
+ core.create_scaffolding(target_dir)
39
+ typer.secho(
40
+ f"Successfully generated directory structure at {target_dir}.",
41
+ fg=typer.colors.GREEN,
42
+ )
43
+
44
+
45
+ @app.command()
46
+ def doctor(
47
+ verbose: Optional[bool] = typer.Option(False, "--verbose", "-v", help="Show extra details")
48
+ ):
49
+ """
50
+ Check the system status.
51
+ """
52
+ global_app_dir = get_global_app_dir()
53
+ status = core.get_doctor_status(verbose=bool(verbose))
54
+ typer.echo(f"System status: {status['status']}")
55
+ typer.echo(f"Global App Directory: {global_app_dir}")
56
+ typer.echo(f"Available Tavily tools: {', '.join(status['tools_available'])}")
57
+ typer.echo(f"AgentFactory ready: {status['agent_factory_ready']}")
58
+
59
+ if verbose and "details" in status:
60
+ typer.echo(f"Detailed logs: {status['details']}")
61
+
62
+
63
+ def prompt_agent_config(agent: str, is_sub_agent: bool = False) -> AgentConfig:
64
+ """Interactively prompts user for agent configuration via questionary."""
65
+ agent_name = questionary.text(
66
+ f"Enter agent name for {agent}:",
67
+ default="research_agent"
68
+ ).ask()
69
+ model_name = questionary.select(
70
+ "Select OpenRouter model:",
71
+ choices=[
72
+ "openai/gpt-4o-mini",
73
+ "openai/gpt-6-luna",
74
+ "openai/gpt-6-luna-pro"
75
+ ]
76
+ ).ask()
77
+ model_providers = questionary.checkbox(
78
+ "Select OpenRouter Providers:",
79
+ choices=[
80
+ "openai",
81
+ "openai/flex",
82
+ "azure"
83
+ ]
84
+ ).ask()
85
+ allow_fallbacks = questionary.confirm(
86
+ "Allow model provider fallbacks?",
87
+ default=False
88
+ ).ask()
89
+ agent_instruction = questionary.text(
90
+ "Enter agent's instructions:",
91
+ default="You are an AI agent built with icogito"
92
+ ).ask()
93
+ selected_tools = questionary.checkbox(
94
+ "Select tools to enable for the agent:",
95
+ choices=[
96
+ "web_search",
97
+ "web_fetch",
98
+ "web_crawl"
99
+ ],
100
+ default="web_search"
101
+ ).ask()
102
+
103
+ if is_sub_agent:
104
+ number_of_sub_agents = 0
105
+ else:
106
+ number_of_sub_agents = int(
107
+ questionary.text(
108
+ "Number of sub agents: ",
109
+ validate=lambda text: text.isdigit() or "Please enter a valid number"
110
+ ).ask()
111
+ )
112
+
113
+ sub_agents_names = []
114
+ if number_of_sub_agents > 0:
115
+ for i in range(int(number_of_sub_agents)):
116
+ subagent_config = prompt_agent_config(f"{agent_name}'s sub-agent-{i}", is_sub_agent=True)
117
+ sub_agents_names.append(subagent_config.agent_name)
118
+ core.save_config(subagent_config)
119
+
120
+ config = core.create_agent_config(
121
+ agent_name=agent_name,
122
+ model_name=model_name,
123
+ model_providers=model_providers,
124
+ allow_fallbacks=allow_fallbacks,
125
+ agent_instruction=agent_instruction,
126
+ selected_tools=selected_tools,
127
+ sub_agents_names=sub_agents_names,
128
+ )
129
+ return config
130
+
131
+
132
+ @app.command()
133
+ def configure():
134
+ """
135
+ Interactively configure agent workflows with questionary prompts.
136
+ """
137
+ typer.echo("--- Dynamic Agent Configuration ---")
138
+ enable_multi_agent = questionary.select(
139
+ "Enable multi-agent configuration?",
140
+ choices=[
141
+ "Yes",
142
+ "No"
143
+ ],
144
+ ).ask()
145
+
146
+ if enable_multi_agent == "No":
147
+ config = prompt_agent_config("root-agent")
148
+ core.save_config(config)
149
+ else:
150
+ number_of_agents = int(
151
+ questionary.text(
152
+ "Number of root agents: ",
153
+ validate=lambda text: text.isdigit() or "Please enter a valid number"
154
+ ).ask()
155
+ )
156
+ for i in range(int(number_of_agents)):
157
+ config = prompt_agent_config(f"root-agent-{i}")
158
+ core.save_config(config)
159
+
160
+ typer.echo("AgentConfig initialized successfully!")
161
+
162
+
163
+ def main():
164
+ app()
165
+
166
+
167
+ if __name__ == "__main__":
168
+ main()
@@ -0,0 +1,81 @@
1
+ from pathlib import Path
2
+ from typing import Optional, List, Dict, Any
3
+
4
+ from icogito.factory import AgentFactory
5
+ from icogito.tools.tavily_web_tools import web_search, web_fetch, web_crawl
6
+ from icogito.models import AgentConfig
7
+ from icogito.utils.save_agent_config import save_agent_config
8
+ from icogito.utils.save_prompt_instruction import save_prompt_instruction
9
+
10
+
11
+ def create_scaffolding(target_dir: Path) -> List[Path]:
12
+ """Creates directory structure for agent scaffolding."""
13
+ subdirs = ["agents", "configs", "schemas", "prompts", "tools", "utils"]
14
+
15
+ target_dir.mkdir(parents=True, exist_ok=True)
16
+ created_paths = [target_dir]
17
+
18
+ for subdir in subdirs:
19
+ dir_path = target_dir / subdir
20
+ dir_path.mkdir(parents=True, exist_ok=True)
21
+ created_paths.append(dir_path)
22
+
23
+ return created_paths
24
+
25
+
26
+ def get_doctor_status(verbose: bool = False) -> Dict[str, Any]:
27
+ """Retrieves system status and availability of tools and components."""
28
+ tools_available = [web_search.__name__, web_fetch.__name__, web_crawl.__name__]
29
+ status: Dict[str, Any] = {
30
+ "status": "OK",
31
+ "tools_available": tools_available,
32
+ "agent_factory_ready": AgentFactory.__name__,
33
+ }
34
+ if verbose:
35
+ status["details"] = "All services reporting nominal operations."
36
+ return status
37
+
38
+
39
+ def create_agent_config(
40
+ agent_name: str,
41
+ model_name: str,
42
+ model_providers: List[str],
43
+ allow_fallbacks: bool,
44
+ agent_instruction: str,
45
+ selected_tools: List[str],
46
+ sub_agents_names: Optional[List[str]] = None,
47
+ prompts_dir: Path | str = "prompts",
48
+ configs_dir: Path | str = "configs",
49
+ ) -> AgentConfig:
50
+ """Constructs an AgentConfig object with target prompt and config paths."""
51
+ target_prompts_dir = Path(prompts_dir)
52
+ target_config_dir = Path(configs_dir)
53
+ target_prompts_dir.mkdir(parents=True, exist_ok=True)
54
+ target_config_dir.mkdir(parents=True, exist_ok=True)
55
+
56
+ instructions_path = f"{target_prompts_dir}/{agent_name}_instruction.md"
57
+ config_path = f"{target_config_dir}/{agent_name}_agent.yaml"
58
+
59
+ return AgentConfig(
60
+ model_name=model_name,
61
+ agent_name=agent_name,
62
+ agent_instruction=agent_instruction,
63
+ instructions_path=instructions_path,
64
+ config_file_path=config_path,
65
+ settings_openrouter_provider_list=model_providers,
66
+ allow_fallbacks=allow_fallbacks,
67
+ subagents_list=sub_agents_names or [],
68
+ tools_list=selected_tools,
69
+ )
70
+
71
+
72
+ def save_config(config: AgentConfig) -> None:
73
+ """Saves the agent configuration file and prompt instruction."""
74
+ save_agent_config(
75
+ config_file=config.config_file_path,
76
+ config_data=config
77
+ )
78
+ save_prompt_instruction(
79
+ path=config.instructions_path,
80
+ instruction_data=config.agent_instruction
81
+ )
@@ -0,0 +1,144 @@
1
+ from pydantic_ai import Agent
2
+ from pydantic_ai_harness import SubAgent, SubAgents, ToolOutputLimits
3
+ from pydantic_ai.toolsets.abstract import AbstractToolset
4
+ # from pydantic_ai.durable_exec.prefect import PrefectDurability, TaskConfig
5
+ from pydantic_ai.models.openrouter import OpenRouterModel, OpenRouterModelSettings
6
+ from pydantic_ai.providers.openrouter import OpenRouterProvider
7
+ from pydantic_ai.output import ToolOutput
8
+ from pydantic_ai.toolsets.function import FunctionToolset
9
+ from icogito.utils.get_prompt_instruction import get_prompt_instruction
10
+ from icogito.tools.tavily_web_tools import web_fetch, web_search
11
+ from icogito.models import AgentConfig
12
+ from typing import Callable, Any
13
+ from loguru import logger
14
+
15
+ class IdentifiedToolOutputLimits(ToolOutputLimits):
16
+ """ToolOutputLimits, but stamps `id` onto its internal `read_tool_result` toolset.
17
+
18
+ Upstream bug: `ToolOutputLimits.get_toolset()` always returns a bare
19
+ `FunctionToolset([read_tool_result])` with no id, ignoring `self.id`.
20
+ A leaf toolset without an id breaks Prefect/Temporal durable execution.
21
+ """
22
+
23
+ def get_toolset(self) -> AbstractToolset[Any] | Callable[..., Any] | None:
24
+ toolset = super().get_toolset()
25
+ if isinstance(toolset, AbstractToolset) and toolset.id is None:
26
+ toolset._id = self.id # type: ignore[attr-defined]
27
+ return toolset
28
+
29
+
30
+ class IdentifiedSubAgents(SubAgents):
31
+ """SubAgents, but stamps `id` onto its internal `delegate_task` toolset.
32
+
33
+ Same upstream bug as `ToolOutputLimits`: `SubAgents.get_toolset()` never
34
+ forwards `self.id` to the `SubAgentToolset` it builds.
35
+ """
36
+
37
+ def get_toolset(self) -> AbstractToolset[Any] | Callable[..., Any] | None:
38
+ toolset = super().get_toolset()
39
+ if isinstance(toolset, AbstractToolset) and toolset.id is None:
40
+ toolset._id = self.id # type: ignore[attr-defined]
41
+ return toolset
42
+
43
+
44
+ class AgentFactory:
45
+ def __init__(self, openrouter_api_key: str | None, subagent_configs: dict[str, AgentConfig] | None = None):
46
+ self.openrouter_api_key: str | None = openrouter_api_key
47
+ # Provide a lookup map for subagent configs so we can recursively build them
48
+ self.subagent_configs: dict[str, AgentConfig] = subagent_configs or {}
49
+
50
+ # Tool registry mapping config strings to actual Python functions
51
+ self.tool_registry: dict[str, Callable] = {
52
+ "web_fetch": web_fetch,
53
+ "web_search": web_search,
54
+ }
55
+
56
+ def create_subagent(self, config: AgentConfig) -> SubAgent:
57
+ return SubAgent(self.create_agent(config=config))
58
+
59
+ def create_agent(self, config: AgentConfig) -> Agent:
60
+ settings = OpenRouterModelSettings(
61
+ openrouter_cache_instructions=True,
62
+ openrouter_provider={
63
+ "order": config.settings_openrouter_provider_list, # TODO: Add to accept None as value
64
+ "allow_fallbacks": False
65
+ }
66
+ )
67
+ model = OpenRouterModel(
68
+ config.model_name,
69
+ provider=OpenRouterProvider(api_key=self.openrouter_api_key),
70
+ )
71
+
72
+ # 3. Build base kwargs for the Agent
73
+ agent_kwargs = {
74
+ "model": model,
75
+ "name": config.agent_name,
76
+ "model_settings": settings,
77
+ "instructions": get_prompt_instruction(config.instructions_path),
78
+ "description": get_prompt_instruction(config.sub_descriptions_path),
79
+ "retries": {'output': config.retries_output},
80
+ "max_concurrency": config.max_concurrency,
81
+ }
82
+
83
+ # 4. Conditionally add Output Type and Dependencies Type
84
+ if config.output_schema:
85
+ agent_kwargs["output_type"] = ToolOutput(config.output_schema)
86
+ if config.deps_type:
87
+ agent_kwargs["deps_type"] = config.deps_type
88
+
89
+ # 5. Conditionally add Capabilities (Subagents)
90
+ capabilities: list[Any] = [
91
+ IdentifiedToolOutputLimits(id=f"{config.agent_name}_tool_output_limits"),
92
+ # PrefectDurability(
93
+ # model_task_config=TaskConfig(
94
+ # retries=3,
95
+ # retry_delay_seconds=[1.0, 2.0, 5.0], # Exponential backoff
96
+ # timeout_seconds=600.0,
97
+ # log_prints=True
98
+ # )
99
+ # ),
100
+ ]
101
+ if config.subagents_list:
102
+ subagents_pool = self._get_subagents_pool(config.subagents_list)
103
+ if subagents_pool:
104
+ capabilities.append(
105
+ IdentifiedSubAgents(
106
+ agents=subagents_pool,
107
+ agent_folders=None,
108
+ tool_retries=3,
109
+ id=f"{config.agent_name}_subagents",
110
+ )
111
+ )
112
+ agent_kwargs["capabilities"] = capabilities
113
+
114
+ # 6. Conditionally add Tools
115
+ if config.tools_list:
116
+ agent_kwargs["toolsets"] = [
117
+ FunctionToolset(self._get_tools_pool(config.tools_list), id=f"{config.agent_name}_tools")
118
+ ]
119
+
120
+ # 7. Unpack kwargs into the Agent
121
+ return Agent(**agent_kwargs) # type: ignore[call-overload, no-any-return]
122
+
123
+ def _get_tools_pool(self, tools_list: list[str]) -> list[Callable]:
124
+ """Maps list of string names to actual tool functions."""
125
+ tool_seq = []
126
+ for tool_name in tools_list:
127
+ tool_fn = self.tool_registry.get(tool_name)
128
+ if tool_fn:
129
+ tool_seq.append(tool_fn)
130
+ else:
131
+ logger.warning(f"Tool '{tool_name}' not found in registry.")
132
+ return tool_seq
133
+
134
+ def _get_subagents_pool(self, subagents_list: list[str]) -> list[SubAgent]:
135
+ """Maps list of string names to actual SubAgent instances recursively."""
136
+ subagent_seq = []
137
+ for subagent_name in subagents_list:
138
+ sub_config = self.subagent_configs.get(subagent_name)
139
+ if sub_config:
140
+ # Recursively create the subagent using the mapped config
141
+ subagent_seq.append(self.create_subagent(sub_config))
142
+ else:
143
+ logger.warning(f"Subagent config for '{subagent_name}' not found in registry.")
144
+ return subagent_seq
@@ -0,0 +1,58 @@
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional
3
+ from datetime import datetime, timezone
4
+ from typing import Type, Any
5
+ from dataclasses import dataclass, field
6
+
7
+ @dataclass
8
+ class ResearchState:
9
+ visited_urls: set[str] = field(default_factory=set)
10
+ searched_queries: set[str] = field(default_factory=set)
11
+
12
+ class AgentConfig(BaseModel):
13
+ model_name: str
14
+ agent_name: str
15
+ instructions_path: str
16
+ config_file_path: str
17
+ agent_instruction: str
18
+ allow_fallbacks: bool
19
+ settings_openrouter_provider_list: list[str] # TODO: Add to accept None as value
20
+ sub_instructions_path: str | None = None
21
+ sub_descriptions_path: str | None = None
22
+ sub_agent_description: str | None = None
23
+ subagents_list: list[str] | None = None
24
+ tools_list: list[str] | None = None
25
+ output_schema: Type[BaseModel] | None = None
26
+ deps_type: Type[Any] | None = None
27
+ retries_output: int = 3
28
+ max_concurrency: int = 1
29
+
30
+ class UsageLimitErrorResponse(BaseModel):
31
+ error_code: str = Field(
32
+ default="UsageLimitExceeded",
33
+ description="The standard error code identifier for routing."
34
+ )
35
+ message: str = Field(
36
+ ...,
37
+ description="The raw error message explaining the usage limit violation."
38
+ )
39
+ limit_type: Optional[str] = Field(
40
+ default=None,
41
+ description="The type of quota exceeded (e.g., 'requests_per_minute', 'tokens_per_day', 'budget')."
42
+ )
43
+ current_usage: Optional[int | float] = Field(
44
+ default=None,
45
+ description="The recorded usage at the time the exception was raised."
46
+ )
47
+ max_limit: Optional[int | float] = Field(
48
+ default=None,
49
+ description="The maximum allowed threshold for the specified limit_type."
50
+ )
51
+ retry_after_seconds: Optional[int] = Field(
52
+ default=None,
53
+ description="The suggested backoff time in seconds extracted from the API response headers."
54
+ )
55
+ timestamp: datetime = Field(
56
+ default_factory=lambda: datetime.now(timezone.utc),
57
+ description="The exact UTC timestamp when the error occurred."
58
+ )
File without changes
@@ -0,0 +1,72 @@
1
+ from tavily import AsyncTavilyClient
2
+ from pydantic_ai import ModelRetry, RunContext
3
+ from icogito.models import ResearchState
4
+ from dotenv import load_dotenv
5
+ from loguru import logger
6
+ from typing import Any, Literal, cast
7
+ import os
8
+
9
+ load_dotenv()
10
+
11
+
12
+ def get_tavily_client() -> AsyncTavilyClient:
13
+ api_key = os.getenv("TAVILY_API_KEY")
14
+ if not api_key:
15
+ logger.error("TAVILY_API_KEY is missing!")
16
+ raise ValueError("TAVILY_API_KEY is missing! Check your .env file.")
17
+ return AsyncTavilyClient(api_key=api_key)
18
+
19
+
20
+ BLOCKED_MESSAGE = (
21
+ "This URL is blocked by a paywall or anti-bot system. DO NOT retry "
22
+ "fetching this URL. Proceed with the draft using only the available "
23
+ "search context."
24
+ )
25
+
26
+ async def web_search(
27
+ search: str,
28
+ topic: Literal['general', 'news', 'finance'] = "news",
29
+ days: int = 7
30
+ ) -> dict:
31
+ """Performs a web search specifically for recent news using Tavily"""
32
+ logger.info(f"Web search looking for {search}")
33
+ tavily_client = get_tavily_client()
34
+ search_response = await tavily_client.search(
35
+ query=search,
36
+ topic=topic,
37
+ days=days,
38
+ max_results=20
39
+ )
40
+ if search_response is None:
41
+ logger.warning("Tavily found nothing or got blocked")
42
+ raise ModelRetry(BLOCKED_MESSAGE)
43
+ return cast(dict[str, Any], search_response)
44
+
45
+ async def web_fetch(ctx: RunContext[ResearchState], urls: str | list[str]) -> dict[str, Any]:
46
+ """Performs a web fetch using Tavily"""
47
+ for url in urls:
48
+ if url in ctx.deps.visited_urls:
49
+ return {"error": "URL already processed. Stop fetching this URL and synthesize."}
50
+ ctx.deps.visited_urls.add(url)
51
+ logger.info(f"Extracting from {urls}")
52
+ tavily_client = get_tavily_client()
53
+ response = await tavily_client.extract(
54
+ urls=urls,
55
+ extract_depth="advanced",
56
+ format="markdown"
57
+ )
58
+ if not response or "cloudflare" in str(response).lower():
59
+ logger.warning("Tavily found nothing or got blocked")
60
+ raise ModelRetry(BLOCKED_MESSAGE)
61
+ return cast(dict[str, Any], response)
62
+
63
+
64
+ async def web_crawl(url: str, instructions: str) -> dict[str, Any]:
65
+ """Performs a smart web crawling using Tavily"""
66
+ logger.info(f"Crawling: {url}")
67
+ tavily_client = get_tavily_client()
68
+ response = await tavily_client.crawl(url, instructions=instructions)
69
+ if not response:
70
+ logger.warning("Tavily found nothing or got blocked")
71
+ raise ModelRetry(BLOCKED_MESSAGE)
72
+ return cast(dict[str, Any], response)
File without changes
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ # Get the project root directory (utils/ -> project_root)
5
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
6
+
7
+ def get_prompt_instruction(path: Optional[str | Path]) -> Optional[str]:
8
+ # If the YAML didn't provide a path (it's None or empty string), return None safely
9
+ if not path:
10
+ return None
11
+
12
+ file_path = Path(path)
13
+
14
+ # If a relative path was passed, resolve it against the project root
15
+ if not file_path.is_absolute():
16
+ file_path = PROJECT_ROOT / file_path
17
+
18
+ return file_path.read_text(encoding="utf-8")
@@ -0,0 +1,9 @@
1
+ import yaml
2
+ from icogito.models import AgentConfig
3
+
4
+ def load_agent_config(yaml_path: str) -> AgentConfig:
5
+ """Loads a single YAML file and converts it into an AgentConfig model."""
6
+ with open(yaml_path, 'r') as file:
7
+ raw_config = yaml.safe_load(file)
8
+
9
+ return AgentConfig(**raw_config)
@@ -0,0 +1,20 @@
1
+ import yaml
2
+ from typing import Optional
3
+ from pathlib import Path
4
+ from icogito.models import AgentConfig
5
+
6
+ def save_agent_config(config_file: Optional[str | Path], config_data: AgentConfig) -> Optional[str | Path] | None:
7
+ """Saves a single YAML file and converts it into an AgentConfig model."""
8
+ # If the file didn't provide a path (it's None or empty string), return None safely
9
+ if not config_file:
10
+ return None
11
+ file_path = Path(config_file)
12
+ # If a relative path was passed, resolve it against the current working directory
13
+ if not file_path.is_absolute():
14
+ file_path = Path.cwd() / file_path
15
+ try:
16
+ with open(file_path, "w") as file:
17
+ yaml.dump(config_data.model_dump(), file, sort_keys=False)
18
+ return file_path
19
+ except FileNotFoundError as e:
20
+ raise e
@@ -0,0 +1,18 @@
1
+ from pathlib import Path
2
+ from typing import Optional
3
+
4
+ def save_prompt_instruction(path: Optional[str | Path], instruction_data: str) -> Optional[str | Path] | None:
5
+ """Saves agent's instruction into a markdown file."""
6
+ # If the file didn't provide a path (it's None or empty string), return None safely
7
+ if not path:
8
+ return None
9
+ file_path = Path(path)
10
+ # If a relative path was passed, resolve it against the current working directory
11
+ if not file_path.is_absolute():
12
+ file_path = Path.cwd() / file_path
13
+ try:
14
+ with open(file_path, 'w') as file:
15
+ file.write(instruction_data)
16
+ return file_path
17
+ except FileNotFoundError as e:
18
+ raise e