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 +193 -0
- icogito-0.1.0/README.md +172 -0
- icogito-0.1.0/pyproject.toml +64 -0
- icogito-0.1.0/pyproject.toml.orig +57 -0
- icogito-0.1.0/src/icogito/__init__.py +6 -0
- icogito-0.1.0/src/icogito/cli.py +168 -0
- icogito-0.1.0/src/icogito/core.py +81 -0
- icogito-0.1.0/src/icogito/factory.py +144 -0
- icogito-0.1.0/src/icogito/models.py +58 -0
- icogito-0.1.0/src/icogito/py.typed +0 -0
- icogito-0.1.0/src/icogito/tools/tavily_web_tools.py +72 -0
- icogito-0.1.0/src/icogito/utils/__init__.py +0 -0
- icogito-0.1.0/src/icogito/utils/get_prompt_instruction.py +18 -0
- icogito-0.1.0/src/icogito/utils/load_agent_config.py +9 -0
- icogito-0.1.0/src/icogito/utils/save_agent_config.py +20 -0
- icogito-0.1.0/src/icogito/utils/save_prompt_instruction.py +18 -0
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)
|
icogito-0.1.0/README.md
ADDED
|
@@ -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,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
|