crewai-econdash 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.
- crewai_econdash-0.1.0/.gitignore +11 -0
- crewai_econdash-0.1.0/PKG-INFO +70 -0
- crewai_econdash-0.1.0/README.md +54 -0
- crewai_econdash-0.1.0/examples/crewai_macro_research.py +67 -0
- crewai_econdash-0.1.0/pyproject.toml +32 -0
- crewai_econdash-0.1.0/src/crewai_econdash/__init__.py +10 -0
- crewai_econdash-0.1.0/src/crewai_econdash/py.typed +0 -0
- crewai_econdash-0.1.0/src/crewai_econdash/tools.py +214 -0
- crewai_econdash-0.1.0/tests/test_tools.py +164 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: crewai-econdash
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: CrewAI tools for EconDash macroeconomic data. Same free trial, API key and pay-per-call rules as the econdash client.
|
|
5
|
+
Project-URL: Homepage, https://econdash.org
|
|
6
|
+
Project-URL: Repository, https://github.com/econdash/econdash-python
|
|
7
|
+
Project-URL: Documentation, https://econdash.org/docs/python
|
|
8
|
+
Author-email: EconDash <noderguru@gmail.com>
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: agents,crewai,econdash,economics,tools,x402
|
|
11
|
+
Requires-Python: <3.14,>=3.10
|
|
12
|
+
Requires-Dist: crewai<2,>=1.15
|
|
13
|
+
Requires-Dist: econdash>=0.2.2
|
|
14
|
+
Requires-Dist: pydantic>=2.5
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# crewai-econdash
|
|
18
|
+
|
|
19
|
+
[CrewAI](https://www.crewai.com) tools for [EconDash](https://econdash.org). They call the `econdash` client, so they follow the same access rules. They do not open a free back door to the archive.
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install crewai-econdash
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Releases are tags `crewai-vX.Y.Z` on `econdash-python` (the `X.Y.Z` matches this
|
|
26
|
+
package's version). A `v*` tag publishes the `econdash` client, not this package.
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from econdash import EconDash
|
|
30
|
+
from crewai_econdash import EconDashCompareTool, EconDashSearchTool, EconDashSeriesTool
|
|
31
|
+
|
|
32
|
+
client = EconDash() # free trial, or ECONDASH_API_KEY from env
|
|
33
|
+
# client = EconDash(api_key="ed_...")
|
|
34
|
+
# from eth_account import Account
|
|
35
|
+
# client = EconDash(x402_signer=Account.from_key("0x..."), max_usd_per_call=0.05)
|
|
36
|
+
|
|
37
|
+
tools = [
|
|
38
|
+
EconDashSearchTool(client=client),
|
|
39
|
+
EconDashSeriesTool(client=client),
|
|
40
|
+
EconDashCompareTool(client=client),
|
|
41
|
+
]
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Each tool builds its own `EconDash()` when you pass no client.
|
|
45
|
+
|
|
46
|
+
| Tool | Name the model sees | What the model gets |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| `EconDashSearchTool` | `econdash_search_indicators` | Slug, name, unit, source, frequency. No numbers. |
|
|
49
|
+
| `EconDashSeriesTool` | `econdash_get_series` | One country. Last 20 points, unit, freshness, a citation and the econdash.org chart link. `full=true` for the whole reply. |
|
|
50
|
+
| `EconDashCompareTool` | `econdash_compare_countries` | Latest point and chart link for 1–8 ISO3 codes, each fetched on its own. |
|
|
51
|
+
|
|
52
|
+
## What is free
|
|
53
|
+
|
|
54
|
+
Without a key and without a wallet the tools only receive the free trial: 10 countries (USA, CHN, DEU, JPN, GBR, FRA, IND, BRA, RUS, KOR), 50 indicators, 300 requests a day and 30 a minute per IP.
|
|
55
|
+
|
|
56
|
+
A country or indicator outside that trial makes the API answer 402. The tool turns that into a sentence for the agent and does **not** include the figures. Nothing is charged. A 429 becomes "Rate limit reached" with the wait time. Bad ISO3 codes and dates are answered with a hint, not an exception.
|
|
57
|
+
|
|
58
|
+
## How to pay
|
|
59
|
+
|
|
60
|
+
- **API key** from an EconDash subscription (`api_key=` or `ECONDASH_API_KEY`): full archive, no per-call charge.
|
|
61
|
+
- **Wallet** on the client (`x402_signer`, `tempo_account` or `solana_keypair`) together with `max_usd_per_call`: pays per call, only when the price is within the cap. See the [econdash README](https://github.com/econdash/econdash-python#pay-per-call-for-ai-agents).
|
|
62
|
+
|
|
63
|
+
## Example crew
|
|
64
|
+
|
|
65
|
+
[`examples/crewai_macro_research.py`](examples/crewai_macro_research.py): a researcher agent collects GDP growth, inflation and unemployment for a country and compares it with USA and JPN; an analyst agent writes a short brief with every figure linked to its econdash.org chart.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
export MODEL=gpt-4.1-mini OPENAI_API_KEY=... # any model CrewAI supports
|
|
69
|
+
python examples/crewai_macro_research.py DEU
|
|
70
|
+
```
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# crewai-econdash
|
|
2
|
+
|
|
3
|
+
[CrewAI](https://www.crewai.com) tools for [EconDash](https://econdash.org). They call the `econdash` client, so they follow the same access rules. They do not open a free back door to the archive.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install crewai-econdash
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Releases are tags `crewai-vX.Y.Z` on `econdash-python` (the `X.Y.Z` matches this
|
|
10
|
+
package's version). A `v*` tag publishes the `econdash` client, not this package.
|
|
11
|
+
|
|
12
|
+
```python
|
|
13
|
+
from econdash import EconDash
|
|
14
|
+
from crewai_econdash import EconDashCompareTool, EconDashSearchTool, EconDashSeriesTool
|
|
15
|
+
|
|
16
|
+
client = EconDash() # free trial, or ECONDASH_API_KEY from env
|
|
17
|
+
# client = EconDash(api_key="ed_...")
|
|
18
|
+
# from eth_account import Account
|
|
19
|
+
# client = EconDash(x402_signer=Account.from_key("0x..."), max_usd_per_call=0.05)
|
|
20
|
+
|
|
21
|
+
tools = [
|
|
22
|
+
EconDashSearchTool(client=client),
|
|
23
|
+
EconDashSeriesTool(client=client),
|
|
24
|
+
EconDashCompareTool(client=client),
|
|
25
|
+
]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Each tool builds its own `EconDash()` when you pass no client.
|
|
29
|
+
|
|
30
|
+
| Tool | Name the model sees | What the model gets |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `EconDashSearchTool` | `econdash_search_indicators` | Slug, name, unit, source, frequency. No numbers. |
|
|
33
|
+
| `EconDashSeriesTool` | `econdash_get_series` | One country. Last 20 points, unit, freshness, a citation and the econdash.org chart link. `full=true` for the whole reply. |
|
|
34
|
+
| `EconDashCompareTool` | `econdash_compare_countries` | Latest point and chart link for 1–8 ISO3 codes, each fetched on its own. |
|
|
35
|
+
|
|
36
|
+
## What is free
|
|
37
|
+
|
|
38
|
+
Without a key and without a wallet the tools only receive the free trial: 10 countries (USA, CHN, DEU, JPN, GBR, FRA, IND, BRA, RUS, KOR), 50 indicators, 300 requests a day and 30 a minute per IP.
|
|
39
|
+
|
|
40
|
+
A country or indicator outside that trial makes the API answer 402. The tool turns that into a sentence for the agent and does **not** include the figures. Nothing is charged. A 429 becomes "Rate limit reached" with the wait time. Bad ISO3 codes and dates are answered with a hint, not an exception.
|
|
41
|
+
|
|
42
|
+
## How to pay
|
|
43
|
+
|
|
44
|
+
- **API key** from an EconDash subscription (`api_key=` or `ECONDASH_API_KEY`): full archive, no per-call charge.
|
|
45
|
+
- **Wallet** on the client (`x402_signer`, `tempo_account` or `solana_keypair`) together with `max_usd_per_call`: pays per call, only when the price is within the cap. See the [econdash README](https://github.com/econdash/econdash-python#pay-per-call-for-ai-agents).
|
|
46
|
+
|
|
47
|
+
## Example crew
|
|
48
|
+
|
|
49
|
+
[`examples/crewai_macro_research.py`](examples/crewai_macro_research.py): a researcher agent collects GDP growth, inflation and unemployment for a country and compares it with USA and JPN; an analyst agent writes a short brief with every figure linked to its econdash.org chart.
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
export MODEL=gpt-4.1-mini OPENAI_API_KEY=... # any model CrewAI supports
|
|
53
|
+
python examples/crewai_macro_research.py DEU
|
|
54
|
+
```
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Macro Research Analyst: a two-agent CrewAI crew on EconDash data.
|
|
2
|
+
|
|
3
|
+
pip install crewai-econdash
|
|
4
|
+
export MODEL=gpt-4.1-mini OPENAI_API_KEY=... # any model CrewAI supports
|
|
5
|
+
python examples/crewai_macro_research.py DEU
|
|
6
|
+
|
|
7
|
+
The researcher pulls figures with the EconDash tools; the analyst writes a short brief
|
|
8
|
+
that cites each figure with its econdash.org chart link. Without ECONDASH_API_KEY the
|
|
9
|
+
crew runs on the free trial (10 countries, 50 indicators).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import sys
|
|
13
|
+
|
|
14
|
+
from crewai import Agent, Crew, Process, Task
|
|
15
|
+
from crewai_econdash import EconDashCompareTool, EconDashSearchTool, EconDashSeriesTool
|
|
16
|
+
|
|
17
|
+
from econdash import EconDash
|
|
18
|
+
|
|
19
|
+
country = sys.argv[1] if len(sys.argv) > 1 else "DEU"
|
|
20
|
+
client = EconDash()
|
|
21
|
+
|
|
22
|
+
researcher = Agent(
|
|
23
|
+
role="Macro data researcher",
|
|
24
|
+
goal=f"Collect current, sourced macro figures for {country} from EconDash",
|
|
25
|
+
backstory="You only report numbers a tool returned, with the chart link it gave.",
|
|
26
|
+
tools=[
|
|
27
|
+
EconDashSearchTool(client=client),
|
|
28
|
+
EconDashSeriesTool(client=client),
|
|
29
|
+
EconDashCompareTool(client=client),
|
|
30
|
+
],
|
|
31
|
+
)
|
|
32
|
+
analyst = Agent(
|
|
33
|
+
role="Macro research analyst",
|
|
34
|
+
goal="Write a short, accurate country brief from the researcher's notes",
|
|
35
|
+
backstory="You never add a figure that is not in the notes.",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
collect = Task(
|
|
39
|
+
description=(
|
|
40
|
+
f"For {country} (ISO3), find and load GDP growth, CPI inflation and unemployment with "
|
|
41
|
+
"the EconDash tools: search first, pick the best slug among several matches, then "
|
|
42
|
+
"load the series. Compare GDP growth "
|
|
43
|
+
"with USA and JPN. If a tool says payment is required, write that down instead of a "
|
|
44
|
+
"number."
|
|
45
|
+
),
|
|
46
|
+
expected_output="Notes: indicator, latest value, date, unit and the econdash.org chart link.",
|
|
47
|
+
agent=researcher,
|
|
48
|
+
)
|
|
49
|
+
brief = Task(
|
|
50
|
+
description=(
|
|
51
|
+
f"Write a brief on the {country} economy from the notes. Every figure has its date "
|
|
52
|
+
"and is followed by its econdash.org link in brackets. End with 'Source: EconDash "
|
|
53
|
+
"(econdash.org)'."
|
|
54
|
+
),
|
|
55
|
+
expected_output="Markdown, 150-250 words, with figures and econdash.org links.",
|
|
56
|
+
agent=analyst,
|
|
57
|
+
context=[collect],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
crew = Crew(
|
|
62
|
+
agents=[researcher, analyst],
|
|
63
|
+
tasks=[collect, brief],
|
|
64
|
+
process=Process.sequential,
|
|
65
|
+
verbose=True,
|
|
66
|
+
)
|
|
67
|
+
print(crew.kickoff())
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "crewai-econdash"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "CrewAI tools for EconDash macroeconomic data. Same free trial, API key and pay-per-call rules as the econdash client."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10,<3.14"
|
|
12
|
+
authors = [{ name = "EconDash", email = "noderguru@gmail.com" }]
|
|
13
|
+
keywords = ["crewai", "econdash", "economics", "tools", "agents", "x402"]
|
|
14
|
+
dependencies = [
|
|
15
|
+
"econdash>=0.2.2",
|
|
16
|
+
"crewai>=1.15,<2",
|
|
17
|
+
"pydantic>=2.5",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
[project.urls]
|
|
21
|
+
Homepage = "https://econdash.org"
|
|
22
|
+
Repository = "https://github.com/econdash/econdash-python"
|
|
23
|
+
Documentation = "https://econdash.org/docs/python"
|
|
24
|
+
|
|
25
|
+
[tool.hatch.build.targets.wheel]
|
|
26
|
+
packages = ["src/crewai_econdash"]
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.sdist]
|
|
29
|
+
include = ["src", "tests", "README.md", "examples"]
|
|
30
|
+
|
|
31
|
+
[tool.pytest.ini_options]
|
|
32
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""CrewAI tools for the EconDash API.
|
|
2
|
+
|
|
3
|
+
Data access is the econdash client's access. Nothing here bypasses the trial,
|
|
4
|
+
an API key, or pay-per-call.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from crewai_econdash.tools import EconDashCompareTool, EconDashSearchTool, EconDashSeriesTool
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
__all__ = ["EconDashCompareTool", "EconDashSearchTool", "EconDashSeriesTool", "__version__"]
|
|
File without changes
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
"""Three CrewAI tools over the econdash client.
|
|
2
|
+
|
|
3
|
+
>>> from econdash import EconDash
|
|
4
|
+
>>> from crewai_econdash import EconDashCompareTool, EconDashSearchTool, EconDashSeriesTool
|
|
5
|
+
>>> client = EconDash() # free trial, or ECONDASH_API_KEY from env
|
|
6
|
+
>>> tools = [EconDashSearchTool(client=client), EconDashSeriesTool(client=client),
|
|
7
|
+
... EconDashCompareTool(client=client)]
|
|
8
|
+
|
|
9
|
+
The tools use the client's access as is: no key and no wallet means the free trial.
|
|
10
|
+
Errors, including 402 and 429, come back as text for the model, never as figures.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from crewai.tools import BaseTool, EnvVar
|
|
18
|
+
from pydantic import BaseModel, Field
|
|
19
|
+
|
|
20
|
+
from econdash import (
|
|
21
|
+
EconDash,
|
|
22
|
+
EconDashAuthError,
|
|
23
|
+
EconDashError,
|
|
24
|
+
EconDashNotFound,
|
|
25
|
+
EconDashPaymentRequired,
|
|
26
|
+
EconDashRateLimitError,
|
|
27
|
+
Indicator,
|
|
28
|
+
Series,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
__all__ = ["EconDashCompareTool", "EconDashSearchTool", "EconDashSeriesTool"]
|
|
32
|
+
|
|
33
|
+
_TRIAL = (
|
|
34
|
+
"Free trial only: 10 countries (USA, CHN, DEU, JPN, GBR, FRA, IND, BRA, RUS, KOR), "
|
|
35
|
+
"50 indicators, 300 requests/day. Anything else comes back as a payment message, "
|
|
36
|
+
"not as numbers. Find the slug with econdash_search_indicators first."
|
|
37
|
+
)
|
|
38
|
+
_PAYMENT = (
|
|
39
|
+
"Payment required. No figures were returned and nothing was charged. "
|
|
40
|
+
"The free trial is 10 countries (USA, CHN, DEU, JPN, GBR, FRA, IND, BRA, RUS, KOR), "
|
|
41
|
+
"50 indicators, 300 requests per day and 30 per minute. "
|
|
42
|
+
"Set ECONDASH_API_KEY for a subscription, or give the tools an EconDash client "
|
|
43
|
+
"with a wallet and max_usd_per_call to pay per call with x402 or MPP. "
|
|
44
|
+
"Do not guess the missing value."
|
|
45
|
+
)
|
|
46
|
+
_ISO = "Use a 3-letter ISO3 code such as USA, DEU or JPN, not a country name."
|
|
47
|
+
_ENV = [
|
|
48
|
+
EnvVar(
|
|
49
|
+
name="ECONDASH_API_KEY",
|
|
50
|
+
description="EconDash subscription key. Without it the tools use the free trial.",
|
|
51
|
+
required=False,
|
|
52
|
+
)
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _explain(exc: Exception) -> str:
|
|
57
|
+
if isinstance(exc, EconDashPaymentRequired):
|
|
58
|
+
return _PAYMENT
|
|
59
|
+
if isinstance(exc, EconDashRateLimitError):
|
|
60
|
+
wait = f" Retry after {exc.retry_after} seconds." if exc.retry_after else ""
|
|
61
|
+
return f"Rate limit reached.{wait} No data in this call."
|
|
62
|
+
if isinstance(exc, EconDashNotFound):
|
|
63
|
+
return f"Not found ({exc}). Search with econdash_search_indicators. {_ISO}"
|
|
64
|
+
if isinstance(exc, EconDashAuthError):
|
|
65
|
+
return f"API key was rejected ({exc}). Check ECONDASH_API_KEY."
|
|
66
|
+
return f"EconDash error: {exc}"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _country_error(code: str) -> str | None:
|
|
70
|
+
return None if len(code) == 3 and code.isalpha() else f"Invalid country code {code!r}. {_ISO}"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _search_text(rows: list[Indicator]) -> str:
|
|
74
|
+
if not rows:
|
|
75
|
+
return "No indicators matched. Try a shorter query such as gdp or inflation."
|
|
76
|
+
lines = ["slug | name | unit | source | frequency"]
|
|
77
|
+
for ind in rows:
|
|
78
|
+
cells = [ind.slug or ind.code, ind.name, ind.unit, ind.source, ind.frequency]
|
|
79
|
+
lines.append(" | ".join(c or "" for c in cells))
|
|
80
|
+
return "\n".join(lines)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _series_text(series: Series, slug: str, full: bool) -> str:
|
|
84
|
+
points = series.data
|
|
85
|
+
if not points:
|
|
86
|
+
return "No observations for this country and period."
|
|
87
|
+
shown = points if full else points[-20:]
|
|
88
|
+
last = shown[-1]
|
|
89
|
+
name = series.indicator_name_en or series.indicator_name_ru
|
|
90
|
+
country = last.country_name or last.country
|
|
91
|
+
unit = series.unit or ""
|
|
92
|
+
chart = f"https://econdash.org/chart/{slug}/{last.country}" if slug else ""
|
|
93
|
+
lines = [
|
|
94
|
+
f"{name} — {country}",
|
|
95
|
+
f"unit: {unit or 'n/a'}",
|
|
96
|
+
f"frequency: {series.frequency or 'n/a'}",
|
|
97
|
+
f"data_changed_at: {series.data_changed_at or 'n/a'}",
|
|
98
|
+
f"points: {len(points)} total, showing {len(shown)}",
|
|
99
|
+
f"chart: {chart or 'n/a'}",
|
|
100
|
+
f"citation: {country} {name} was {last.value} {unit} on {last.date.isoformat()} "
|
|
101
|
+
f"(EconDash). {chart}".rstrip(),
|
|
102
|
+
]
|
|
103
|
+
lines += [f"{p.date.isoformat()} {p.value}" for p in shown]
|
|
104
|
+
if len(points) > len(shown):
|
|
105
|
+
lines.append("Set full=true only when the user asked for the whole history.")
|
|
106
|
+
return "\n".join(lines)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class _EconDashTool(BaseTool):
|
|
110
|
+
client: Any = Field(default_factory=EconDash, exclude=True)
|
|
111
|
+
env_vars: list[EnvVar] = Field(default_factory=lambda: list(_ENV))
|
|
112
|
+
|
|
113
|
+
def _slug(self, indicator: str, series: Series) -> str:
|
|
114
|
+
if "__" not in indicator:
|
|
115
|
+
return indicator
|
|
116
|
+
try:
|
|
117
|
+
return self.client.get_indicator(series.indicator_code).slug or ""
|
|
118
|
+
except EconDashError:
|
|
119
|
+
return ""
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class SearchInput(BaseModel):
|
|
123
|
+
query: str = Field(description="Words to match against indicator names, slugs and sources.")
|
|
124
|
+
limit: int = Field(default=10, ge=1, le=20, description="How many matches to return.")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class EconDashSearchTool(_EconDashTool):
|
|
128
|
+
name: str = "econdash_search_indicators"
|
|
129
|
+
description: str = (
|
|
130
|
+
"Search the EconDash indicator catalog. Returns slug, name, unit, source and frequency. "
|
|
131
|
+
"Call this before a series. The catalog is metadata; it has no numbers. " + _TRIAL
|
|
132
|
+
)
|
|
133
|
+
args_schema: type[BaseModel] = SearchInput
|
|
134
|
+
|
|
135
|
+
def _run(self, query: str, limit: int = 10, **_: Any) -> str:
|
|
136
|
+
try:
|
|
137
|
+
return _search_text(self.client.search_indicators(query, limit=limit))
|
|
138
|
+
except EconDashError as exc:
|
|
139
|
+
return _explain(exc)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class SeriesInput(BaseModel):
|
|
143
|
+
indicator: str = Field(description="Slug from econdash_search_indicators, e.g. gdp-nominal.")
|
|
144
|
+
country: str = Field(description="ISO3 code, e.g. USA. Not a country name.")
|
|
145
|
+
start: str | None = Field(default=None, description="Start year or date, e.g. 2000.")
|
|
146
|
+
end: str | None = Field(default=None, description="End year or date.")
|
|
147
|
+
full: bool = Field(
|
|
148
|
+
default=False,
|
|
149
|
+
description="Return the whole series the API sent. Default is the last 20 points.",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class EconDashSeriesTool(_EconDashTool):
|
|
154
|
+
name: str = "econdash_get_series"
|
|
155
|
+
description: str = (
|
|
156
|
+
"Load one indicator for one country: unit, when the data last changed, a citation "
|
|
157
|
+
"with the econdash.org chart link, and the last 20 observations. " + _TRIAL
|
|
158
|
+
)
|
|
159
|
+
args_schema: type[BaseModel] = SeriesInput
|
|
160
|
+
|
|
161
|
+
def _run(
|
|
162
|
+
self,
|
|
163
|
+
indicator: str,
|
|
164
|
+
country: str,
|
|
165
|
+
start: str | None = None,
|
|
166
|
+
end: str | None = None,
|
|
167
|
+
full: bool = False,
|
|
168
|
+
**_: Any,
|
|
169
|
+
) -> str:
|
|
170
|
+
if message := _country_error(country):
|
|
171
|
+
return message
|
|
172
|
+
try:
|
|
173
|
+
series = self.client.get_series(indicator, country.upper(), start=start, end=end)
|
|
174
|
+
except EconDashError as exc:
|
|
175
|
+
return _explain(exc)
|
|
176
|
+
except ValueError:
|
|
177
|
+
return "Invalid start or end. Use a year such as 2000 or a date such as 2000-06-30."
|
|
178
|
+
return _series_text(series, self._slug(indicator, series), full)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class CompareInput(BaseModel):
|
|
182
|
+
indicator: str = Field(description="Indicator slug, e.g. gdp-nominal.")
|
|
183
|
+
countries: list[str] = Field(
|
|
184
|
+
min_length=1, max_length=8, description="1 to 8 ISO3 codes, e.g. ['USA', 'DEU', 'JPN']."
|
|
185
|
+
)
|
|
186
|
+
year: int | None = Field(default=None, description="Calendar year. Omit for the latest point.")
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class EconDashCompareTool(_EconDashTool):
|
|
190
|
+
name: str = "econdash_compare_countries"
|
|
191
|
+
description: str = (
|
|
192
|
+
"Latest value of one indicator for a few countries, each requested on its own, "
|
|
193
|
+
"with the econdash.org chart link per country. A country outside the trial is a "
|
|
194
|
+
"payment message on that line; the other countries are not unlocked by it. " + _TRIAL
|
|
195
|
+
)
|
|
196
|
+
args_schema: type[BaseModel] = CompareInput
|
|
197
|
+
|
|
198
|
+
def _line(self, indicator: str, country: str, year: int | None) -> str:
|
|
199
|
+
if message := _country_error(country):
|
|
200
|
+
return f"{country}: {message}"
|
|
201
|
+
country = country.upper()
|
|
202
|
+
try:
|
|
203
|
+
series = self.client.get_series(indicator, country, start=year, end=year)
|
|
204
|
+
except EconDashError as exc:
|
|
205
|
+
return f"{country}: {_explain(exc)}"
|
|
206
|
+
if not series.data:
|
|
207
|
+
return f"{country}: no observations."
|
|
208
|
+
last = series.data[-1]
|
|
209
|
+
slug = self._slug(indicator, series)
|
|
210
|
+
chart = f" https://econdash.org/chart/{slug}/{country}" if slug else ""
|
|
211
|
+
return f"{country}: {last.value} {series.unit or ''} on {last.date.isoformat()}{chart}"
|
|
212
|
+
|
|
213
|
+
def _run(self, indicator: str, countries: list[str], year: int | None = None, **_: Any) -> str:
|
|
214
|
+
return "\n".join(self._line(indicator, c, year) for c in countries)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""CrewAI tools on a fake client: trial data comes back as text, errors as a hint, never figures."""
|
|
2
|
+
|
|
3
|
+
from datetime import date
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
from crewai_econdash import (
|
|
8
|
+
EconDashCompareTool,
|
|
9
|
+
EconDashSearchTool,
|
|
10
|
+
EconDashSeriesTool,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
from econdash import (
|
|
14
|
+
EconDashNotFound,
|
|
15
|
+
EconDashPaymentRequired,
|
|
16
|
+
EconDashRateLimitError,
|
|
17
|
+
Indicator,
|
|
18
|
+
Point,
|
|
19
|
+
Series,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _indicator() -> Indicator:
|
|
24
|
+
return Indicator(
|
|
25
|
+
code="world_bank__gdp_nominal",
|
|
26
|
+
slug="gdp-nominal",
|
|
27
|
+
name_ru="ВВП",
|
|
28
|
+
name_en="GDP",
|
|
29
|
+
unit="USD",
|
|
30
|
+
source="World Bank",
|
|
31
|
+
frequency="annual",
|
|
32
|
+
category=None,
|
|
33
|
+
fetch_method=None,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _series(country: str) -> Series:
|
|
38
|
+
points = [Point(date=date(2000 + i, 1, 1), value=float(i), country=country) for i in range(25)]
|
|
39
|
+
return Series(
|
|
40
|
+
indicator_code="world_bank__gdp_nominal",
|
|
41
|
+
indicator_name_ru="ВВП",
|
|
42
|
+
indicator_name_en="GDP",
|
|
43
|
+
unit="USD",
|
|
44
|
+
data=points,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class Fake:
|
|
49
|
+
def __init__(self) -> None:
|
|
50
|
+
self.calls: list[tuple[Any, ...]] = []
|
|
51
|
+
|
|
52
|
+
def search_indicators(self, query: str, limit: int = 20) -> list[Indicator]:
|
|
53
|
+
self.calls.append(("search", query, limit))
|
|
54
|
+
if query == "busy":
|
|
55
|
+
raise EconDashRateLimitError("429", retry_after=30, status_code=429)
|
|
56
|
+
return [_indicator()] if query == "gdp" else []
|
|
57
|
+
|
|
58
|
+
def get_series(
|
|
59
|
+
self, indicator: str, country: str, start: Any = None, end: Any = None
|
|
60
|
+
) -> Series:
|
|
61
|
+
self.calls.append(("series", indicator, country, start, end))
|
|
62
|
+
if start == "abc":
|
|
63
|
+
raise ValueError("bad date")
|
|
64
|
+
if indicator == "missing":
|
|
65
|
+
raise EconDashNotFound("404: unknown", status_code=404)
|
|
66
|
+
if country == "ZWE":
|
|
67
|
+
raise EconDashPaymentRequired("402: outside trial", status_code=402)
|
|
68
|
+
return _series(country)
|
|
69
|
+
|
|
70
|
+
def get_indicator(self, indicator: str) -> Indicator:
|
|
71
|
+
self.calls.append(("meta", indicator))
|
|
72
|
+
return _indicator()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@pytest.fixture
|
|
76
|
+
def fake() -> Fake:
|
|
77
|
+
return Fake()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_search_returns_slug_table(fake: Fake) -> None:
|
|
81
|
+
out = EconDashSearchTool(client=fake).run(query="gdp", limit=3)
|
|
82
|
+
assert "gdp-nominal | GDP | USD | World Bank | annual" in out
|
|
83
|
+
assert fake.calls == [("search", "gdp", 3)]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_search_no_match_and_rate_limit_are_text(fake: Fake) -> None:
|
|
87
|
+
tool = EconDashSearchTool(client=fake)
|
|
88
|
+
assert "No indicators matched" in tool.run(query="zzz")
|
|
89
|
+
assert "Retry after 30 seconds" in tool.run(query="busy")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_search_validates_limit(fake: Fake) -> None:
|
|
93
|
+
with pytest.raises(ValueError):
|
|
94
|
+
EconDashSearchTool(client=fake).run(query="gdp", limit=50)
|
|
95
|
+
assert fake.calls == []
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_series_gives_last_points_citation_and_chart_link(fake: Fake) -> None:
|
|
99
|
+
out = EconDashSeriesTool(client=fake).run(indicator="gdp-nominal", country="usa", start="2000")
|
|
100
|
+
assert "https://econdash.org/chart/gdp-nominal/USA" in out
|
|
101
|
+
assert "citation: USA GDP was 24.0 USD on 2024-01-01" in out
|
|
102
|
+
assert "points: 25 total, showing 20" in out
|
|
103
|
+
assert "2004-01-01" not in out and "2005-01-01 5.0" in out
|
|
104
|
+
assert fake.calls == [("series", "gdp-nominal", "USA", "2000", None)]
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_series_full_and_code_resolves_slug(fake: Fake) -> None:
|
|
108
|
+
out = EconDashSeriesTool(client=fake).run(
|
|
109
|
+
indicator="world_bank__gdp_nominal", country="DEU", full=True
|
|
110
|
+
)
|
|
111
|
+
assert "2000-01-01 0.0" in out
|
|
112
|
+
assert "https://econdash.org/chart/gdp-nominal/DEU" in out
|
|
113
|
+
assert ("meta", "world_bank__gdp_nominal") in fake.calls
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_series_payment_is_hint_without_figures(fake: Fake) -> None:
|
|
117
|
+
out = EconDashSeriesTool(client=fake).run(indicator="gdp-nominal", country="ZWE")
|
|
118
|
+
assert out.startswith("Payment required.")
|
|
119
|
+
assert "nothing was charged" in out
|
|
120
|
+
assert not any(ch.isdigit() for ch in out.split("free trial")[0])
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_series_bad_country_date_and_missing_are_text(fake: Fake) -> None:
|
|
124
|
+
tool = EconDashSeriesTool(client=fake)
|
|
125
|
+
assert "ISO3" in tool.run(indicator="gdp-nominal", country="Germany")
|
|
126
|
+
assert "Invalid start or end" in tool.run(indicator="gdp-nominal", country="USA", start="abc")
|
|
127
|
+
assert tool.run(indicator="missing", country="USA").startswith("Not found")
|
|
128
|
+
assert all(call[2] != "Germany" for call in fake.calls)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_series_requires_country(fake: Fake) -> None:
|
|
132
|
+
with pytest.raises(ValueError):
|
|
133
|
+
EconDashSeriesTool(client=fake).run(indicator="gdp-nominal")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_compare_each_country_on_its_own(fake: Fake) -> None:
|
|
137
|
+
out = (
|
|
138
|
+
EconDashCompareTool(client=fake)
|
|
139
|
+
.run(indicator="gdp-nominal", countries=["USA", "ZWE", "Japan"], year=2020)
|
|
140
|
+
.splitlines()
|
|
141
|
+
)
|
|
142
|
+
assert out[0] == "USA: 24.0 USD on 2024-01-01 https://econdash.org/chart/gdp-nominal/USA"
|
|
143
|
+
assert out[1].startswith("ZWE: Payment required.")
|
|
144
|
+
assert out[2].startswith("Japan: Invalid country code")
|
|
145
|
+
assert [c for c in fake.calls if c[0] == "series"] == [
|
|
146
|
+
("series", "gdp-nominal", "USA", 2020, 2020),
|
|
147
|
+
("series", "gdp-nominal", "ZWE", 2020, 2020),
|
|
148
|
+
]
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_compare_validates_country_count(fake: Fake) -> None:
|
|
152
|
+
tool = EconDashCompareTool(client=fake)
|
|
153
|
+
with pytest.raises(ValueError):
|
|
154
|
+
tool.run(indicator="gdp-nominal", countries=[])
|
|
155
|
+
with pytest.raises(ValueError):
|
|
156
|
+
tool.run(indicator="gdp-nominal", countries=["USA"] * 9)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def test_default_client_and_env_var_declared(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
160
|
+
monkeypatch.setenv("ECONDASH_API_KEY", "k")
|
|
161
|
+
tool = EconDashSearchTool()
|
|
162
|
+
assert tool.client.api_key == "k"
|
|
163
|
+
assert [v.name for v in tool.env_vars] == ["ECONDASH_API_KEY"]
|
|
164
|
+
assert tool.env_vars[0].required is False
|