langchain-fxmacrodata 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.
- langchain_fxmacrodata-0.1.0/LICENSE +21 -0
- langchain_fxmacrodata-0.1.0/PKG-INFO +66 -0
- langchain_fxmacrodata-0.1.0/README.md +52 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata/__init__.py +5 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata/client.py +161 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata/tools.py +53 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata.egg-info/PKG-INFO +66 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata.egg-info/SOURCES.txt +13 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata.egg-info/dependency_links.txt +1 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata.egg-info/requires.txt +4 -0
- langchain_fxmacrodata-0.1.0/langchain_fxmacrodata.egg-info/top_level.txt +1 -0
- langchain_fxmacrodata-0.1.0/pyproject.toml +22 -0
- langchain_fxmacrodata-0.1.0/setup.cfg +4 -0
- langchain_fxmacrodata-0.1.0/tests/test_langgraph.py +55 -0
- langchain_fxmacrodata-0.1.0/tests/test_tools.py +80 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FXMacroData
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-fxmacrodata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain and LangGraph tools for the public FXMacroData API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Documentation, https://fxmacrodata.com/docs?utm_source=pypi&utm_medium=referral&utm_campaign=langchain-fxmacrodata&utm_content=package
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: langchain-core==1.7.3
|
|
11
|
+
Provides-Extra: langgraph
|
|
12
|
+
Requires-Dist: langgraph==1.2.19; extra == "langgraph"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# FXMacroData tools for LangChain
|
|
16
|
+
|
|
17
|
+
`langchain-fxmacrodata` is a standalone package that gives a LangChain agent the public FXMacroData API.
|
|
18
|
+
|
|
19
|
+
USD catalogue, USD macro history and the USD release calendar work with no API key and no account. For other data, set `FXMACRODATA_API_KEY` in the environment. The package sends that value as the `X-API-Key` header. A key is not a tool argument and is not written into the URL.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from langchain_fxmacrodata import fxmacrodata_tools
|
|
23
|
+
|
|
24
|
+
tools = fxmacrodata_tools()
|
|
25
|
+
catalogue = next(tool for tool in tools if tool.name == "fxmacrodata_data_catalogue_currency")
|
|
26
|
+
print(catalogue.invoke({"currency": "USD"}))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## LangGraph
|
|
30
|
+
|
|
31
|
+
The same tools work in a LangGraph agent. Install the extra with `pip install "langchain-fxmacrodata[langgraph]"`, then pass the tools to a `ToolNode` and bind them to any chat model that supports tool calling:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from langgraph.graph import START, MessagesState, StateGraph
|
|
35
|
+
from langgraph.prebuilt import ToolNode, tools_condition
|
|
36
|
+
|
|
37
|
+
from langchain_fxmacrodata import fxmacrodata_tools
|
|
38
|
+
|
|
39
|
+
tools = fxmacrodata_tools()
|
|
40
|
+
model = chat_model.bind_tools(tools) # your LangChain chat model
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def agent(state: MessagesState):
|
|
44
|
+
return {"messages": [model.invoke(state["messages"])]}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
graph = StateGraph(MessagesState)
|
|
48
|
+
graph.add_node("agent", agent)
|
|
49
|
+
graph.add_node("tools", ToolNode(tools))
|
|
50
|
+
graph.add_edge(START, "agent")
|
|
51
|
+
graph.add_conditional_edges("agent", tools_condition)
|
|
52
|
+
graph.add_edge("tools", "agent")
|
|
53
|
+
app = graph.compile()
|
|
54
|
+
|
|
55
|
+
app.invoke({"messages": [("user", "What USD indicators does FXMacroData cover?")]})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To keep a model's tool list short, pass only the tools you need, for example `[tool for tool in tools if tool.name.startswith("fxmacrodata_calendar")]`.
|
|
59
|
+
|
|
60
|
+
## Behaviour
|
|
61
|
+
|
|
62
|
+
Each tool calls one public GET route and returns the response body. A non-200 response comes back as JSON with `status` and `body`. This package does not call `POST /v1/research/panel`. The event tool can send `Last-Event-ID` and returns one response.
|
|
63
|
+
|
|
64
|
+
Requests go only to `https://api.fxmacrodata.com`.
|
|
65
|
+
|
|
66
|
+
Documentation: https://fxmacrodata.com/docs?utm_source=langchain&utm_medium=integration&utm_campaign=langchain-fxmacrodata&utm_content=readme-docs
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# FXMacroData tools for LangChain
|
|
2
|
+
|
|
3
|
+
`langchain-fxmacrodata` is a standalone package that gives a LangChain agent the public FXMacroData API.
|
|
4
|
+
|
|
5
|
+
USD catalogue, USD macro history and the USD release calendar work with no API key and no account. For other data, set `FXMACRODATA_API_KEY` in the environment. The package sends that value as the `X-API-Key` header. A key is not a tool argument and is not written into the URL.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from langchain_fxmacrodata import fxmacrodata_tools
|
|
9
|
+
|
|
10
|
+
tools = fxmacrodata_tools()
|
|
11
|
+
catalogue = next(tool for tool in tools if tool.name == "fxmacrodata_data_catalogue_currency")
|
|
12
|
+
print(catalogue.invoke({"currency": "USD"}))
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## LangGraph
|
|
16
|
+
|
|
17
|
+
The same tools work in a LangGraph agent. Install the extra with `pip install "langchain-fxmacrodata[langgraph]"`, then pass the tools to a `ToolNode` and bind them to any chat model that supports tool calling:
|
|
18
|
+
|
|
19
|
+
```python
|
|
20
|
+
from langgraph.graph import START, MessagesState, StateGraph
|
|
21
|
+
from langgraph.prebuilt import ToolNode, tools_condition
|
|
22
|
+
|
|
23
|
+
from langchain_fxmacrodata import fxmacrodata_tools
|
|
24
|
+
|
|
25
|
+
tools = fxmacrodata_tools()
|
|
26
|
+
model = chat_model.bind_tools(tools) # your LangChain chat model
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def agent(state: MessagesState):
|
|
30
|
+
return {"messages": [model.invoke(state["messages"])]}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
graph = StateGraph(MessagesState)
|
|
34
|
+
graph.add_node("agent", agent)
|
|
35
|
+
graph.add_node("tools", ToolNode(tools))
|
|
36
|
+
graph.add_edge(START, "agent")
|
|
37
|
+
graph.add_conditional_edges("agent", tools_condition)
|
|
38
|
+
graph.add_edge("tools", "agent")
|
|
39
|
+
app = graph.compile()
|
|
40
|
+
|
|
41
|
+
app.invoke({"messages": [("user", "What USD indicators does FXMacroData cover?")]})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
To keep a model's tool list short, pass only the tools you need, for example `[tool for tool in tools if tool.name.startswith("fxmacrodata_calendar")]`.
|
|
45
|
+
|
|
46
|
+
## Behaviour
|
|
47
|
+
|
|
48
|
+
Each tool calls one public GET route and returns the response body. A non-200 response comes back as JSON with `status` and `body`. This package does not call `POST /v1/research/panel`. The event tool can send `Last-Event-ID` and returns one response.
|
|
49
|
+
|
|
50
|
+
Requests go only to `https://api.fxmacrodata.com`.
|
|
51
|
+
|
|
52
|
+
Documentation: https://fxmacrodata.com/docs?utm_source=langchain&utm_medium=integration&utm_campaign=langchain-fxmacrodata&utm_content=readme-docs
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Public FXMacroData HTTP requests. No third-party client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.parse
|
|
9
|
+
import urllib.request
|
|
10
|
+
|
|
11
|
+
API_ORIGIN = "https://api.fxmacrodata.com"
|
|
12
|
+
MAX_BODY = 2_000_000
|
|
13
|
+
|
|
14
|
+
# method, OpenAPI path, query names. Path fields are the {placeholders}.
|
|
15
|
+
OPERATIONS: tuple[tuple[str, str, str], ...] = (
|
|
16
|
+
("GET", "/v1/announcements/changes", "currencies indicators since limit payload strict_cursor"),
|
|
17
|
+
("GET", "/v1/announcements/{currency}/latest", ""),
|
|
18
|
+
("GET", "/v1/announcements/{currency}/{indicator}", "start_date end_date series_mode limit offset page seasonality frequency annualization period_aggregation revisions basis base official_only fields as_of availability dataset_version value_mode"),
|
|
19
|
+
("GET", "/v1/calendar/{currency}", "indicator start_date end_date timezone"),
|
|
20
|
+
("GET", "/v1/calendar/{currency}/coverage", "start_date end_date as_of"),
|
|
21
|
+
("GET", "/v1/calendar/{currency}/events/{archive_event_id}/versions", "checkpoint cursor limit"),
|
|
22
|
+
("GET", "/v1/calendar/{currency}/snapshot", "as_of start_date end_date importance event_kind knowledge_mode checkpoint cursor limit"),
|
|
23
|
+
("GET", "/v1/commodities/latest", ""),
|
|
24
|
+
("GET", "/v1/commodities/{indicator}", "start_date end_date limit offset page"),
|
|
25
|
+
("GET", "/v1/cot/{currency}", "start_date end_date limit offset page report"),
|
|
26
|
+
("GET", "/v1/curves/{currency}", "curve_family metric view method date as_of availability max_staleness_days require_common_date"),
|
|
27
|
+
("GET", "/v1/data_catalogue/{currency}", "include_capabilities include_coverage include_analytics indicator"),
|
|
28
|
+
("GET", "/v1/factors/{currency}/{factor}", "start_date end_date include_components include_sources limit offset"),
|
|
29
|
+
("GET", "/v1/financial_prices/{currency}", "source start_date end_date measure instrument_id issuer limit offset"),
|
|
30
|
+
("GET", "/v1/forex/{base}/{quote}", "start_date end_date limit offset page indicators dataset_version"),
|
|
31
|
+
("GET", "/v1/fx/intraday-reference-rates/{base}/{quote}", "start_time end_time"),
|
|
32
|
+
("GET", "/v1/fx/reference-rates/{base}/{quote}", "date start_date end_date source timestamp_type native_only include_source_legs order limit offset page dataset_version"),
|
|
33
|
+
("GET", "/v1/fx/session-rates/{base}/{quote}", "session date start_date end_date order limit offset page dataset_version"),
|
|
34
|
+
("GET", "/v1/fx/source-universe", "currency source"),
|
|
35
|
+
("GET", "/v1/fx/sources", ""),
|
|
36
|
+
("GET", "/v1/health", ""),
|
|
37
|
+
("GET", "/v1/market_sessions", "at"),
|
|
38
|
+
("GET", "/v1/native", ""),
|
|
39
|
+
("GET", "/v1/native/{source}", "year measure holder_country_code ownership_basis maturity account_type region product limit offset order"),
|
|
40
|
+
("GET", "/v1/organisations", "q category limit"),
|
|
41
|
+
("GET", "/v1/organisations/{slug}", ""),
|
|
42
|
+
("GET", "/v1/ping", ""),
|
|
43
|
+
("GET", "/v1/positioning", ""),
|
|
44
|
+
("GET", "/v1/positioning/{instrument}", "report start_date end_date limit offset"),
|
|
45
|
+
("GET", "/v1/predictions/coverage", ""),
|
|
46
|
+
("GET", "/v1/predictions/coverage/{currency}", ""),
|
|
47
|
+
("GET", "/v1/predictions/{currency}", "prediction_source prediction_class statistic respondent_population start_date end_date variable target_period publication_precision before_id limit"),
|
|
48
|
+
("GET", "/v1/predictions/{currency}/{indicator}", "prediction_class prediction_type prediction_source official_limit pre_release_only seasonality frequency annualization period_aggregation basis start_date end_date limit offset page before_date as_of event_compatible_only"),
|
|
49
|
+
("GET", "/v1/press-releases/{currency}", "limit offset"),
|
|
50
|
+
("GET", "/v1/rate_differentials/{base}/{quote}", "measure rate_type curve_family start_tenor_years end_tenor_years start_date end_date limit offset as_of availability"),
|
|
51
|
+
("POST", "/v1/research/panel", ""),
|
|
52
|
+
("GET", "/v1/risk_sentiment", "start_date end_date limit offset"),
|
|
53
|
+
("GET", "/v1/risk_sentiment/changes", "after_sequence limit"),
|
|
54
|
+
("GET", "/v1/settlement/contracts", ""),
|
|
55
|
+
("GET", "/v1/settlement/contracts/{contract_id}", "period strike comparison currency"),
|
|
56
|
+
("GET", "/v1/settlement/keys", ""),
|
|
57
|
+
("GET", "/v1/settlement/{currency}/{indicator}", "period announcement_id strike comparison decimals"),
|
|
58
|
+
("GET", "/v1/stream/events", "currencies indicators payload live_only max_age_ms"),
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class FXMacroDataError(RuntimeError):
|
|
63
|
+
pass
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def path_fields(template: str) -> tuple[str, ...]:
|
|
67
|
+
fields = []
|
|
68
|
+
start = 0
|
|
69
|
+
while True:
|
|
70
|
+
left = template.find("{", start)
|
|
71
|
+
if left < 0:
|
|
72
|
+
return tuple(fields)
|
|
73
|
+
right = template.find("}", left)
|
|
74
|
+
if right < 0:
|
|
75
|
+
raise FXMacroDataError("broken path template")
|
|
76
|
+
fields.append(template[left + 1 : right])
|
|
77
|
+
start = right + 1
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def build_url(template: str, query_names: str, values: dict[str, str], origin: str = API_ORIGIN) -> str:
|
|
81
|
+
parsed = urllib.parse.urlsplit(origin)
|
|
82
|
+
if parsed.scheme != "https" or parsed.netloc != "api.fxmacrodata.com" or parsed.path not in {"", "/"}:
|
|
83
|
+
raise FXMacroDataError("requests stay on https://api.fxmacrodata.com")
|
|
84
|
+
path = template
|
|
85
|
+
for name in path_fields(template):
|
|
86
|
+
value = values.get(name, "")
|
|
87
|
+
if not value:
|
|
88
|
+
raise FXMacroDataError(f"missing {name}")
|
|
89
|
+
path = path.replace("{" + name + "}", urllib.parse.quote(value, safe=""))
|
|
90
|
+
if "{" in path or "}" in path:
|
|
91
|
+
raise FXMacroDataError("unfilled path")
|
|
92
|
+
query = []
|
|
93
|
+
for name in query_names.split():
|
|
94
|
+
value = values.get(name, "")
|
|
95
|
+
if value:
|
|
96
|
+
query.append((name, value))
|
|
97
|
+
return urllib.parse.urlunsplit(("https", "api.fxmacrodata.com", path, urllib.parse.urlencode(query), ""))
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _opener() -> urllib.request.OpenerDirector:
|
|
101
|
+
class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
102
|
+
def redirect_request(self, req, fp, code, msg, headers, newurl): # noqa: ANN001
|
|
103
|
+
raise FXMacroDataError("redirect refused")
|
|
104
|
+
|
|
105
|
+
return urllib.request.build_opener(_NoRedirect)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def request(
|
|
109
|
+
method: str,
|
|
110
|
+
url: str,
|
|
111
|
+
api_key: str | None = None,
|
|
112
|
+
timeout: float = 30,
|
|
113
|
+
extra_headers: dict[str, str] | None = None,
|
|
114
|
+
) -> tuple[int, str]:
|
|
115
|
+
parsed = urllib.parse.urlsplit(url)
|
|
116
|
+
if parsed.scheme != "https" or parsed.netloc != "api.fxmacrodata.com":
|
|
117
|
+
raise FXMacroDataError("requests stay on https://api.fxmacrodata.com")
|
|
118
|
+
if method != "GET":
|
|
119
|
+
raise FXMacroDataError("this client sends GET requests only")
|
|
120
|
+
headers = {"Accept": "application/json", "User-Agent": "langchain-fxmacrodata/0.1"}
|
|
121
|
+
for name, value in (extra_headers or {}).items():
|
|
122
|
+
if name.lower() in {"x-api-key", "authorization", "host"}:
|
|
123
|
+
raise FXMacroDataError("that header is reserved")
|
|
124
|
+
if value:
|
|
125
|
+
headers[name] = value
|
|
126
|
+
if api_key:
|
|
127
|
+
headers["X-API-Key"] = api_key
|
|
128
|
+
call = urllib.request.Request(url, headers=headers, method="GET")
|
|
129
|
+
try:
|
|
130
|
+
with _opener().open(call, timeout=timeout) as response:
|
|
131
|
+
status = getattr(response, "status", 200)
|
|
132
|
+
body = response.read(MAX_BODY + 1)
|
|
133
|
+
except urllib.error.HTTPError as exc:
|
|
134
|
+
status = exc.code
|
|
135
|
+
body = exc.read(MAX_BODY + 1)
|
|
136
|
+
except FXMacroDataError:
|
|
137
|
+
raise
|
|
138
|
+
except urllib.error.URLError as exc:
|
|
139
|
+
raise FXMacroDataError("request failed") from exc
|
|
140
|
+
if len(body) > MAX_BODY:
|
|
141
|
+
raise FXMacroDataError("response exceeds 2 MB")
|
|
142
|
+
return status, body.decode("utf-8")
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def call_operation(template: str, query_names: str, values: dict[str, str], transport=None) -> str:
|
|
146
|
+
if template == "/v1/research/panel":
|
|
147
|
+
raise FXMacroDataError("the research panel needs a JSON body and is not a GET")
|
|
148
|
+
url = build_url(template, query_names, values)
|
|
149
|
+
key = os.environ.get("FXMACRODATA_API_KEY", "").strip() or None
|
|
150
|
+
if "api_key=" in url:
|
|
151
|
+
raise FXMacroDataError("API key must not be placed in the URL")
|
|
152
|
+
extra = {}
|
|
153
|
+
if template == "/v1/stream/events" and values.get("last_event_id"):
|
|
154
|
+
extra["Last-Event-ID"] = values["last_event_id"]
|
|
155
|
+
if transport is None:
|
|
156
|
+
status, body = request("GET", url, key, extra_headers=extra)
|
|
157
|
+
else:
|
|
158
|
+
status, body = transport("GET", url, key)
|
|
159
|
+
if status == 200:
|
|
160
|
+
return body
|
|
161
|
+
return json.dumps({"status": status, "body": body}, ensure_ascii=False)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""LangChain tools for the public FXMacroData API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
|
|
7
|
+
from langchain_core.tools import StructuredTool
|
|
8
|
+
from pydantic import create_model
|
|
9
|
+
|
|
10
|
+
from langchain_fxmacrodata.client import OPERATIONS, call_operation, path_fields
|
|
11
|
+
|
|
12
|
+
Transport = Callable[[str, str, str | None], tuple[int, str]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def tool_name(template: str) -> str:
|
|
16
|
+
parts = []
|
|
17
|
+
for part in template.strip("/").split("/"):
|
|
18
|
+
parts.append(part[1:-1] if part.startswith("{") and part.endswith("}") else part)
|
|
19
|
+
return "fxmacrodata_" + "_".join(parts[1:])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def fxmacrodata_tools(transport: Transport | None = None) -> list[StructuredTool]:
|
|
23
|
+
"""One LangChain tool for every public REST operation this client can send."""
|
|
24
|
+
tools = []
|
|
25
|
+
for method, template, query_names in OPERATIONS:
|
|
26
|
+
if method != "GET":
|
|
27
|
+
continue
|
|
28
|
+
fields = {name: (str, ...) for name in path_fields(template)}
|
|
29
|
+
for name in query_names.split():
|
|
30
|
+
fields[name] = (str, "")
|
|
31
|
+
if template == "/v1/stream/events":
|
|
32
|
+
fields["last_event_id"] = (str, "")
|
|
33
|
+
schema = create_model(tool_name(template) + "_args", **fields)
|
|
34
|
+
description = (
|
|
35
|
+
f"Read FXMacroData {method} {template}. "
|
|
36
|
+
"USD catalogue, USD macro history and the USD release calendar work with no API key. "
|
|
37
|
+
"An optional FXMACRODATA_API_KEY is sent as a header for protected data. "
|
|
38
|
+
"The response body is returned unchanged."
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def run(_template=template, _query=query_names, **kwargs: str) -> str:
|
|
42
|
+
return call_operation(_template, _query, kwargs, transport)
|
|
43
|
+
|
|
44
|
+
tools.append(
|
|
45
|
+
StructuredTool.from_function(
|
|
46
|
+
func=run,
|
|
47
|
+
name=tool_name(template),
|
|
48
|
+
description=description,
|
|
49
|
+
args_schema=schema,
|
|
50
|
+
infer_schema=False,
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
return tools
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-fxmacrodata
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain and LangGraph tools for the public FXMacroData API
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Documentation, https://fxmacrodata.com/docs?utm_source=pypi&utm_medium=referral&utm_campaign=langchain-fxmacrodata&utm_content=package
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: langchain-core==1.7.3
|
|
11
|
+
Provides-Extra: langgraph
|
|
12
|
+
Requires-Dist: langgraph==1.2.19; extra == "langgraph"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# FXMacroData tools for LangChain
|
|
16
|
+
|
|
17
|
+
`langchain-fxmacrodata` is a standalone package that gives a LangChain agent the public FXMacroData API.
|
|
18
|
+
|
|
19
|
+
USD catalogue, USD macro history and the USD release calendar work with no API key and no account. For other data, set `FXMACRODATA_API_KEY` in the environment. The package sends that value as the `X-API-Key` header. A key is not a tool argument and is not written into the URL.
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
from langchain_fxmacrodata import fxmacrodata_tools
|
|
23
|
+
|
|
24
|
+
tools = fxmacrodata_tools()
|
|
25
|
+
catalogue = next(tool for tool in tools if tool.name == "fxmacrodata_data_catalogue_currency")
|
|
26
|
+
print(catalogue.invoke({"currency": "USD"}))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## LangGraph
|
|
30
|
+
|
|
31
|
+
The same tools work in a LangGraph agent. Install the extra with `pip install "langchain-fxmacrodata[langgraph]"`, then pass the tools to a `ToolNode` and bind them to any chat model that supports tool calling:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from langgraph.graph import START, MessagesState, StateGraph
|
|
35
|
+
from langgraph.prebuilt import ToolNode, tools_condition
|
|
36
|
+
|
|
37
|
+
from langchain_fxmacrodata import fxmacrodata_tools
|
|
38
|
+
|
|
39
|
+
tools = fxmacrodata_tools()
|
|
40
|
+
model = chat_model.bind_tools(tools) # your LangChain chat model
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def agent(state: MessagesState):
|
|
44
|
+
return {"messages": [model.invoke(state["messages"])]}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
graph = StateGraph(MessagesState)
|
|
48
|
+
graph.add_node("agent", agent)
|
|
49
|
+
graph.add_node("tools", ToolNode(tools))
|
|
50
|
+
graph.add_edge(START, "agent")
|
|
51
|
+
graph.add_conditional_edges("agent", tools_condition)
|
|
52
|
+
graph.add_edge("tools", "agent")
|
|
53
|
+
app = graph.compile()
|
|
54
|
+
|
|
55
|
+
app.invoke({"messages": [("user", "What USD indicators does FXMacroData cover?")]})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
To keep a model's tool list short, pass only the tools you need, for example `[tool for tool in tools if tool.name.startswith("fxmacrodata_calendar")]`.
|
|
59
|
+
|
|
60
|
+
## Behaviour
|
|
61
|
+
|
|
62
|
+
Each tool calls one public GET route and returns the response body. A non-200 response comes back as JSON with `status` and `body`. This package does not call `POST /v1/research/panel`. The event tool can send `Last-Event-ID` and returns one response.
|
|
63
|
+
|
|
64
|
+
Requests go only to `https://api.fxmacrodata.com`.
|
|
65
|
+
|
|
66
|
+
Documentation: https://fxmacrodata.com/docs?utm_source=langchain&utm_medium=integration&utm_campaign=langchain-fxmacrodata&utm_content=readme-docs
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
langchain_fxmacrodata/__init__.py
|
|
5
|
+
langchain_fxmacrodata/client.py
|
|
6
|
+
langchain_fxmacrodata/tools.py
|
|
7
|
+
langchain_fxmacrodata.egg-info/PKG-INFO
|
|
8
|
+
langchain_fxmacrodata.egg-info/SOURCES.txt
|
|
9
|
+
langchain_fxmacrodata.egg-info/dependency_links.txt
|
|
10
|
+
langchain_fxmacrodata.egg-info/requires.txt
|
|
11
|
+
langchain_fxmacrodata.egg-info/top_level.txt
|
|
12
|
+
tests/test_langgraph.py
|
|
13
|
+
tests/test_tools.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langchain_fxmacrodata
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools==84.2.1", "wheel==0.48.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-fxmacrodata"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain and LangGraph tools for the public FXMacroData API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
dependencies = ["langchain-core==1.7.3"]
|
|
13
|
+
|
|
14
|
+
[project.optional-dependencies]
|
|
15
|
+
langgraph = ["langgraph==1.2.19"]
|
|
16
|
+
|
|
17
|
+
[project.urls]
|
|
18
|
+
Documentation = "https://fxmacrodata.com/docs?utm_source=pypi&utm_medium=referral&utm_campaign=langchain-fxmacrodata&utm_content=package"
|
|
19
|
+
|
|
20
|
+
[tool.setuptools]
|
|
21
|
+
packages = ["langchain_fxmacrodata"]
|
|
22
|
+
include-package-data = false
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import unittest
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
|
|
6
|
+
from langgraph.graph import START, MessagesState, StateGraph
|
|
7
|
+
from langgraph.prebuilt import ToolNode, tools_condition
|
|
8
|
+
except ImportError: # the langgraph extra is optional
|
|
9
|
+
StateGraph = None
|
|
10
|
+
|
|
11
|
+
from langchain_fxmacrodata import fxmacrodata_tools
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@unittest.skipIf(StateGraph is None, "install the langgraph extra")
|
|
15
|
+
class LangGraphTests(unittest.TestCase):
|
|
16
|
+
def test_tool_node_runs_a_tool_call_and_returns_the_body(self) -> None:
|
|
17
|
+
body = '{"currency":"USD","indicators":[]}'
|
|
18
|
+
seen = []
|
|
19
|
+
|
|
20
|
+
def transport(method, url, key):
|
|
21
|
+
seen.append((method, url, key))
|
|
22
|
+
return 200, body
|
|
23
|
+
|
|
24
|
+
tools = fxmacrodata_tools(transport=transport)
|
|
25
|
+
|
|
26
|
+
def agent(state):
|
|
27
|
+
# Stands in for a chat model bound with the tools: one tool call, then an answer.
|
|
28
|
+
if not any(isinstance(message, ToolMessage) for message in state["messages"]):
|
|
29
|
+
call = {
|
|
30
|
+
"name": "fxmacrodata_data_catalogue_currency",
|
|
31
|
+
"args": {"currency": "USD"},
|
|
32
|
+
"id": "call-1",
|
|
33
|
+
"type": "tool_call",
|
|
34
|
+
}
|
|
35
|
+
return {"messages": [AIMessage(content="", tool_calls=[call])]}
|
|
36
|
+
return {"messages": [AIMessage(content="done")]}
|
|
37
|
+
|
|
38
|
+
graph = StateGraph(MessagesState)
|
|
39
|
+
graph.add_node("agent", agent)
|
|
40
|
+
graph.add_node("tools", ToolNode(tools))
|
|
41
|
+
graph.add_edge(START, "agent")
|
|
42
|
+
graph.add_conditional_edges("agent", tools_condition)
|
|
43
|
+
graph.add_edge("tools", "agent")
|
|
44
|
+
result = graph.compile().invoke({"messages": [HumanMessage(content="What USD data is there?")]})
|
|
45
|
+
|
|
46
|
+
tool_messages = [message for message in result["messages"] if isinstance(message, ToolMessage)]
|
|
47
|
+
self.assertEqual(len(tool_messages), 1)
|
|
48
|
+
self.assertEqual(tool_messages[0].content, body)
|
|
49
|
+
self.assertEqual(json.loads(tool_messages[0].content)["currency"], "USD")
|
|
50
|
+
self.assertEqual(result["messages"][-1].content, "done")
|
|
51
|
+
self.assertEqual(seen, [("GET", "https://api.fxmacrodata.com/v1/data_catalogue/USD", None)])
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
unittest.main()
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import unittest
|
|
3
|
+
from unittest import mock
|
|
4
|
+
|
|
5
|
+
from langchain_fxmacrodata.client import OPERATIONS, build_url, call_operation
|
|
6
|
+
from langchain_fxmacrodata.tools import fxmacrodata_tools, tool_name
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ToolTests(unittest.TestCase):
|
|
10
|
+
def test_every_get_operation_is_a_distinct_tool(self) -> None:
|
|
11
|
+
tools = fxmacrodata_tools(transport=lambda method, url, key: (200, url))
|
|
12
|
+
names = [tool.name for tool in tools]
|
|
13
|
+
self.assertEqual(len(names), len(set(names)))
|
|
14
|
+
self.assertEqual(len(tools), sum(method == "GET" for method, _path, _query in OPERATIONS))
|
|
15
|
+
self.assertGreater(len(tools), 40)
|
|
16
|
+
self.assertIn("fxmacrodata_data_catalogue_currency", names)
|
|
17
|
+
|
|
18
|
+
def test_public_catalogue_keeps_the_response_body(self) -> None:
|
|
19
|
+
body = '{"indicator":"9007199254740993.0000"}'
|
|
20
|
+
seen = {}
|
|
21
|
+
|
|
22
|
+
def transport(method, url, key):
|
|
23
|
+
seen["method"] = method
|
|
24
|
+
seen["url"] = url
|
|
25
|
+
seen["key"] = key
|
|
26
|
+
return 200, body
|
|
27
|
+
|
|
28
|
+
tool = next(item for item in fxmacrodata_tools(transport=transport) if item.name == "fxmacrodata_data_catalogue_currency")
|
|
29
|
+
self.assertEqual(tool.invoke({"currency": "USD", "include_coverage": "true"}), body)
|
|
30
|
+
self.assertEqual(seen["method"], "GET")
|
|
31
|
+
self.assertEqual(seen["url"], "https://api.fxmacrodata.com/v1/data_catalogue/USD?include_coverage=true")
|
|
32
|
+
self.assertIsNone(seen["key"])
|
|
33
|
+
self.assertNotIn("api_key", seen["url"])
|
|
34
|
+
|
|
35
|
+
def test_optional_key_is_a_header_value_not_a_url(self) -> None:
|
|
36
|
+
def transport(method, url, key):
|
|
37
|
+
return 200, key or ""
|
|
38
|
+
|
|
39
|
+
with mock.patch.dict(os.environ, {"FXMACRODATA_API_KEY": "not-a-real-key"}):
|
|
40
|
+
tool = next(item for item in fxmacrodata_tools(transport=transport) if item.name == tool_name("/v1/forex/{base}/{quote}"))
|
|
41
|
+
self.assertEqual(tool.invoke({"base": "EUR", "quote": "USD"}), "not-a-real-key")
|
|
42
|
+
|
|
43
|
+
def test_research_panel_is_refused(self) -> None:
|
|
44
|
+
with self.assertRaises(Exception):
|
|
45
|
+
call_operation("/v1/research/panel", "", {})
|
|
46
|
+
|
|
47
|
+
def test_origin_cannot_be_replaced(self) -> None:
|
|
48
|
+
with self.assertRaises(Exception):
|
|
49
|
+
build_url("/v1/health", "", {}, origin="https://example.invalid")
|
|
50
|
+
|
|
51
|
+
def test_stream_resume_id_is_a_header(self) -> None:
|
|
52
|
+
captured = {}
|
|
53
|
+
|
|
54
|
+
class _Body:
|
|
55
|
+
status = 200
|
|
56
|
+
|
|
57
|
+
def read(self, _limit):
|
|
58
|
+
return b'{"events":[]}'
|
|
59
|
+
|
|
60
|
+
def __enter__(self):
|
|
61
|
+
return self
|
|
62
|
+
|
|
63
|
+
def __exit__(self, *_args):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
def _open(self, req, data=None, timeout=None):
|
|
67
|
+
captured["url"] = req.full_url
|
|
68
|
+
captured["headers"] = {key.lower(): value for key, value in req.header_items()}
|
|
69
|
+
return _Body()
|
|
70
|
+
|
|
71
|
+
tool = next(item for item in fxmacrodata_tools() if item.name == "fxmacrodata_stream_events")
|
|
72
|
+
with mock.patch("urllib.request.OpenerDirector.open", _open):
|
|
73
|
+
self.assertEqual(tool.invoke({"currencies": "USD", "last_event_id": "42"}), '{"events":[]}')
|
|
74
|
+
self.assertEqual(captured["url"], "https://api.fxmacrodata.com/v1/stream/events?currencies=USD")
|
|
75
|
+
self.assertEqual(captured["headers"].get("last-event-id"), "42")
|
|
76
|
+
self.assertNotIn("last_event_id", captured["url"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
unittest.main()
|