tooluniverse 1.0.5__py3-none-any.whl → 1.0.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tooluniverse might be problematic. Click here for more details.
- tooluniverse/__init__.py +39 -0
- tooluniverse/agentic_tool.py +82 -12
- tooluniverse/arxiv_tool.py +113 -0
- tooluniverse/biorxiv_tool.py +97 -0
- tooluniverse/core_tool.py +153 -0
- tooluniverse/crossref_tool.py +73 -0
- tooluniverse/data/arxiv_tools.json +87 -0
- tooluniverse/data/biorxiv_tools.json +70 -0
- tooluniverse/data/core_tools.json +105 -0
- tooluniverse/data/crossref_tools.json +70 -0
- tooluniverse/data/dblp_tools.json +73 -0
- tooluniverse/data/doaj_tools.json +94 -0
- tooluniverse/data/fatcat_tools.json +72 -0
- tooluniverse/data/hal_tools.json +70 -0
- tooluniverse/data/medrxiv_tools.json +70 -0
- tooluniverse/data/openaire_tools.json +85 -0
- tooluniverse/data/osf_preprints_tools.json +77 -0
- tooluniverse/data/pmc_tools.json +109 -0
- tooluniverse/data/pubmed_tools.json +65 -0
- tooluniverse/data/unpaywall_tools.json +86 -0
- tooluniverse/data/wikidata_sparql_tools.json +42 -0
- tooluniverse/data/zenodo_tools.json +82 -0
- tooluniverse/dblp_tool.py +62 -0
- tooluniverse/default_config.py +17 -0
- tooluniverse/doaj_tool.py +124 -0
- tooluniverse/execute_function.py +70 -9
- tooluniverse/fatcat_tool.py +66 -0
- tooluniverse/hal_tool.py +77 -0
- tooluniverse/llm_clients.py +286 -0
- tooluniverse/medrxiv_tool.py +97 -0
- tooluniverse/openaire_tool.py +145 -0
- tooluniverse/osf_preprints_tool.py +67 -0
- tooluniverse/pmc_tool.py +181 -0
- tooluniverse/pubmed_tool.py +110 -0
- tooluniverse/smcp.py +109 -79
- tooluniverse/test/test_claude_sdk.py +11 -4
- tooluniverse/unpaywall_tool.py +63 -0
- tooluniverse/wikidata_sparql_tool.py +61 -0
- tooluniverse/zenodo_tool.py +74 -0
- {tooluniverse-1.0.5.dist-info → tooluniverse-1.0.6.dist-info}/METADATA +2 -1
- {tooluniverse-1.0.5.dist-info → tooluniverse-1.0.6.dist-info}/RECORD +45 -13
- {tooluniverse-1.0.5.dist-info → tooluniverse-1.0.6.dist-info}/entry_points.txt +1 -0
- {tooluniverse-1.0.5.dist-info → tooluniverse-1.0.6.dist-info}/WHEEL +0 -0
- {tooluniverse-1.0.5.dist-info → tooluniverse-1.0.6.dist-info}/licenses/LICENSE +0 -0
- {tooluniverse-1.0.5.dist-info → tooluniverse-1.0.6.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .base_tool import BaseTool
|
|
3
|
+
from .tool_registry import register_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register_tool("DBLPTool")
|
|
7
|
+
class DBLPTool(BaseTool):
|
|
8
|
+
"""
|
|
9
|
+
Search DBLP Computer Science Bibliography for publications.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
tool_config,
|
|
15
|
+
base_url="https://dblp.org/search/publ/api",
|
|
16
|
+
):
|
|
17
|
+
super().__init__(tool_config)
|
|
18
|
+
self.base_url = base_url
|
|
19
|
+
|
|
20
|
+
def run(self, arguments):
|
|
21
|
+
query = arguments.get("query")
|
|
22
|
+
limit = int(arguments.get("limit", 10))
|
|
23
|
+
if not query:
|
|
24
|
+
return {"error": "`query` parameter is required."}
|
|
25
|
+
return self._search(query, limit)
|
|
26
|
+
|
|
27
|
+
def _search(self, query, limit):
|
|
28
|
+
params = {
|
|
29
|
+
"q": query,
|
|
30
|
+
"h": max(1, min(limit, 100)),
|
|
31
|
+
"format": "json",
|
|
32
|
+
}
|
|
33
|
+
try:
|
|
34
|
+
response = requests.get(self.base_url, params=params, timeout=20)
|
|
35
|
+
except requests.RequestException as e:
|
|
36
|
+
return {
|
|
37
|
+
"error": "Network error calling DBLP API",
|
|
38
|
+
"reason": str(e),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if response.status_code != 200:
|
|
42
|
+
return {
|
|
43
|
+
"error": f"DBLP API error {response.status_code}",
|
|
44
|
+
"reason": response.reason,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
hits = response.json().get("result", {}).get("hits", {}).get("hit", [])
|
|
48
|
+
results = []
|
|
49
|
+
for hit in hits:
|
|
50
|
+
info = hit.get("info", {})
|
|
51
|
+
results.append(
|
|
52
|
+
{
|
|
53
|
+
"title": info.get("title"),
|
|
54
|
+
"authors": info.get("authors", {}).get("author"),
|
|
55
|
+
"year": info.get("year"),
|
|
56
|
+
"venue": info.get("venue"),
|
|
57
|
+
"url": info.get("url"),
|
|
58
|
+
"ee": info.get("ee"),
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return results
|
tooluniverse/default_config.py
CHANGED
|
@@ -33,6 +33,23 @@ default_tool_files = {
|
|
|
33
33
|
"Enrichr": os.path.join(current_dir, "data", "enrichr_tools.json"),
|
|
34
34
|
"HumanBase": os.path.join(current_dir, "data", "humanbase_tools.json"),
|
|
35
35
|
"OpenAlex": os.path.join(current_dir, "data", "openalex_tools.json"),
|
|
36
|
+
# Literature search tools
|
|
37
|
+
"arxiv": os.path.join(current_dir, "data", "arxiv_tools.json"),
|
|
38
|
+
"crossref": os.path.join(current_dir, "data", "crossref_tools.json"),
|
|
39
|
+
"dblp": os.path.join(current_dir, "data", "dblp_tools.json"),
|
|
40
|
+
"pubmed": os.path.join(current_dir, "data", "pubmed_tools.json"),
|
|
41
|
+
"doaj": os.path.join(current_dir, "data", "doaj_tools.json"),
|
|
42
|
+
"unpaywall": os.path.join(current_dir, "data", "unpaywall_tools.json"),
|
|
43
|
+
"biorxiv": os.path.join(current_dir, "data", "biorxiv_tools.json"),
|
|
44
|
+
"medrxiv": os.path.join(current_dir, "data", "medrxiv_tools.json"),
|
|
45
|
+
"hal": os.path.join(current_dir, "data", "hal_tools.json"),
|
|
46
|
+
"core": os.path.join(current_dir, "data", "core_tools.json"),
|
|
47
|
+
"pmc": os.path.join(current_dir, "data", "pmc_tools.json"),
|
|
48
|
+
"zenodo": os.path.join(current_dir, "data", "zenodo_tools.json"),
|
|
49
|
+
"openaire": os.path.join(current_dir, "data", "openaire_tools.json"),
|
|
50
|
+
"osf_preprints": os.path.join(current_dir, "data", "osf_preprints_tools.json"),
|
|
51
|
+
"fatcat": os.path.join(current_dir, "data", "fatcat_tools.json"),
|
|
52
|
+
"wikidata_sparql": os.path.join(current_dir, "data", "wikidata_sparql_tools.json"),
|
|
36
53
|
"agents": os.path.join(current_dir, "data", "agentic_tools.json"),
|
|
37
54
|
"dataset": os.path.join(current_dir, "data", "dataset_tools.json"),
|
|
38
55
|
# 'mcp_clients': os.path.join(current_dir, 'data', 'mcp_client_tools_example.json'),
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .base_tool import BaseTool
|
|
3
|
+
from .tool_registry import register_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register_tool("DOAJTool")
|
|
7
|
+
class DOAJTool(BaseTool):
|
|
8
|
+
"""
|
|
9
|
+
Search DOAJ (Directory of Open Access Journals) articles and journals.
|
|
10
|
+
|
|
11
|
+
Parameters (arguments):
|
|
12
|
+
query (str): Query string (Lucene syntax supported by DOAJ)
|
|
13
|
+
max_results (int): Max number of results (default 10, max 100)
|
|
14
|
+
type (str): "articles" or "journals" (default: "articles")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, tool_config):
|
|
18
|
+
super().__init__(tool_config)
|
|
19
|
+
self.base_url = "https://doaj.org/api/search"
|
|
20
|
+
|
|
21
|
+
def run(self, arguments=None):
|
|
22
|
+
arguments = arguments or {}
|
|
23
|
+
query = arguments.get("query")
|
|
24
|
+
search_type = arguments.get("type", "articles")
|
|
25
|
+
max_results = int(arguments.get("max_results", 10))
|
|
26
|
+
|
|
27
|
+
if not query:
|
|
28
|
+
return {"error": "`query` parameter is required."}
|
|
29
|
+
|
|
30
|
+
if search_type not in ["articles", "journals"]:
|
|
31
|
+
return {"error": "`type` must be 'articles' or 'journals'."}
|
|
32
|
+
|
|
33
|
+
endpoint = f"{self.base_url}/{search_type}/{query}"
|
|
34
|
+
params = {
|
|
35
|
+
"pageSize": max(1, min(max_results, 100)),
|
|
36
|
+
}
|
|
37
|
+
try:
|
|
38
|
+
resp = requests.get(endpoint, params=params, timeout=20)
|
|
39
|
+
resp.raise_for_status()
|
|
40
|
+
data = resp.json()
|
|
41
|
+
except requests.RequestException as e:
|
|
42
|
+
return {
|
|
43
|
+
"error": "Network/API error calling DOAJ",
|
|
44
|
+
"reason": str(e),
|
|
45
|
+
}
|
|
46
|
+
except ValueError:
|
|
47
|
+
return {"error": "Failed to decode DOAJ response as JSON"}
|
|
48
|
+
|
|
49
|
+
results = data.get("results", [])
|
|
50
|
+
items = []
|
|
51
|
+
if search_type == "articles":
|
|
52
|
+
for r in results:
|
|
53
|
+
b = r.get("bibjson", {})
|
|
54
|
+
title = b.get("title")
|
|
55
|
+
year = None
|
|
56
|
+
try:
|
|
57
|
+
year = int((b.get("year") or 0))
|
|
58
|
+
except Exception:
|
|
59
|
+
year = b.get("year")
|
|
60
|
+
authors = [
|
|
61
|
+
a.get("name")
|
|
62
|
+
for a in b.get("author", [])
|
|
63
|
+
if a.get("name")
|
|
64
|
+
]
|
|
65
|
+
doi = None
|
|
66
|
+
for i in b.get("identifier", []):
|
|
67
|
+
if i.get("type") == "doi":
|
|
68
|
+
doi = i.get("id")
|
|
69
|
+
break
|
|
70
|
+
url = None
|
|
71
|
+
for link_item in b.get("link", []):
|
|
72
|
+
if (
|
|
73
|
+
link_item.get("type") == "fulltext"
|
|
74
|
+
or link_item.get("url")
|
|
75
|
+
):
|
|
76
|
+
url = link_item.get("url")
|
|
77
|
+
break
|
|
78
|
+
journal = (b.get("journal") or {}).get("title")
|
|
79
|
+
items.append(
|
|
80
|
+
{
|
|
81
|
+
"title": title,
|
|
82
|
+
"authors": authors,
|
|
83
|
+
"year": year,
|
|
84
|
+
"doi": doi,
|
|
85
|
+
"venue": journal,
|
|
86
|
+
"url": url,
|
|
87
|
+
"source": "DOAJ",
|
|
88
|
+
}
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
for r in results:
|
|
92
|
+
b = r.get("bibjson", {})
|
|
93
|
+
title = b.get("title")
|
|
94
|
+
publisher = b.get("publisher")
|
|
95
|
+
eissn = None
|
|
96
|
+
pissn = None
|
|
97
|
+
for i in b.get("identifier", []):
|
|
98
|
+
if i.get("type") == "eissn":
|
|
99
|
+
eissn = i.get("id")
|
|
100
|
+
if i.get("type") == "pissn":
|
|
101
|
+
pissn = i.get("id")
|
|
102
|
+
homepage_url = None
|
|
103
|
+
for link_item in b.get("link", []):
|
|
104
|
+
if link_item.get("url"):
|
|
105
|
+
homepage_url = link_item.get("url")
|
|
106
|
+
break
|
|
107
|
+
subjects = [
|
|
108
|
+
s.get("term")
|
|
109
|
+
for s in b.get("subject", [])
|
|
110
|
+
if s.get("term")
|
|
111
|
+
]
|
|
112
|
+
items.append(
|
|
113
|
+
{
|
|
114
|
+
"title": title,
|
|
115
|
+
"publisher": publisher,
|
|
116
|
+
"eissn": eissn,
|
|
117
|
+
"pissn": pissn,
|
|
118
|
+
"subjects": subjects,
|
|
119
|
+
"url": homepage_url,
|
|
120
|
+
"source": "DOAJ",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
return items
|
tooluniverse/execute_function.py
CHANGED
|
@@ -25,6 +25,7 @@ Constants:
|
|
|
25
25
|
"""
|
|
26
26
|
|
|
27
27
|
import copy
|
|
28
|
+
import inspect
|
|
28
29
|
import json
|
|
29
30
|
import random
|
|
30
31
|
import string
|
|
@@ -1638,7 +1639,14 @@ class ToolUniverse:
|
|
|
1638
1639
|
"""
|
|
1639
1640
|
return "".join(random.choices(string.ascii_letters + string.digits, k=9))
|
|
1640
1641
|
|
|
1641
|
-
def run(
|
|
1642
|
+
def run(
|
|
1643
|
+
self,
|
|
1644
|
+
fcall_str,
|
|
1645
|
+
return_message=False,
|
|
1646
|
+
verbose=True,
|
|
1647
|
+
format="llama",
|
|
1648
|
+
stream_callback=None,
|
|
1649
|
+
):
|
|
1642
1650
|
"""
|
|
1643
1651
|
Execute function calls from input string or data.
|
|
1644
1652
|
|
|
@@ -1671,7 +1679,9 @@ class ToolUniverse:
|
|
|
1671
1679
|
# return the function call+result message with call id.
|
|
1672
1680
|
call_results = []
|
|
1673
1681
|
for i in range(len(function_call_json)):
|
|
1674
|
-
call_result = self.run_one_function(
|
|
1682
|
+
call_result = self.run_one_function(
|
|
1683
|
+
function_call_json[i], stream_callback=stream_callback
|
|
1684
|
+
)
|
|
1675
1685
|
call_id = self.call_id_gen()
|
|
1676
1686
|
function_call_json[i]["call_id"] = call_id
|
|
1677
1687
|
call_results.append(
|
|
@@ -1691,12 +1701,14 @@ class ToolUniverse:
|
|
|
1691
1701
|
] + call_results
|
|
1692
1702
|
return revised_messages
|
|
1693
1703
|
else:
|
|
1694
|
-
return self.run_one_function(
|
|
1704
|
+
return self.run_one_function(
|
|
1705
|
+
function_call_json, stream_callback=stream_callback
|
|
1706
|
+
)
|
|
1695
1707
|
else:
|
|
1696
1708
|
error("Not a function call")
|
|
1697
1709
|
return None
|
|
1698
1710
|
|
|
1699
|
-
def run_one_function(self, function_call_json):
|
|
1711
|
+
def run_one_function(self, function_call_json, stream_callback=None):
|
|
1700
1712
|
"""
|
|
1701
1713
|
Execute a single function call.
|
|
1702
1714
|
|
|
@@ -1719,8 +1731,13 @@ class ToolUniverse:
|
|
|
1719
1731
|
arguments = function_call_json["arguments"]
|
|
1720
1732
|
|
|
1721
1733
|
# Execute the tool
|
|
1734
|
+
tool_instance = None
|
|
1735
|
+
tool_arguments = arguments
|
|
1722
1736
|
if function_name in self.callable_functions:
|
|
1723
|
-
|
|
1737
|
+
tool_instance = self.callable_functions[function_name]
|
|
1738
|
+
result, tool_arguments = self._execute_tool_with_stream(
|
|
1739
|
+
tool_instance, arguments, stream_callback
|
|
1740
|
+
)
|
|
1724
1741
|
else:
|
|
1725
1742
|
if function_name in self.all_tool_dict:
|
|
1726
1743
|
self.logger.debug(
|
|
@@ -1729,7 +1746,10 @@ class ToolUniverse:
|
|
|
1729
1746
|
tool = self.init_tool(
|
|
1730
1747
|
self.all_tool_dict[function_name], add_to_cache=True
|
|
1731
1748
|
)
|
|
1732
|
-
|
|
1749
|
+
tool_instance = tool
|
|
1750
|
+
result, tool_arguments = self._execute_tool_with_stream(
|
|
1751
|
+
tool_instance, arguments, stream_callback
|
|
1752
|
+
)
|
|
1733
1753
|
else:
|
|
1734
1754
|
return f"Tool '{function_name}' not found"
|
|
1735
1755
|
|
|
@@ -1738,17 +1758,58 @@ class ToolUniverse:
|
|
|
1738
1758
|
context = {
|
|
1739
1759
|
"tool_name": function_name,
|
|
1740
1760
|
"tool_type": (
|
|
1741
|
-
|
|
1761
|
+
tool_instance.__class__.__name__
|
|
1762
|
+
if tool_instance is not None
|
|
1763
|
+
else "unknown"
|
|
1742
1764
|
),
|
|
1743
1765
|
"execution_time": time.time(),
|
|
1744
|
-
"arguments":
|
|
1766
|
+
"arguments": tool_arguments,
|
|
1745
1767
|
}
|
|
1746
1768
|
result = self.hook_manager.apply_hooks(
|
|
1747
|
-
result, function_name,
|
|
1769
|
+
result, function_name, tool_arguments, context
|
|
1748
1770
|
)
|
|
1749
1771
|
|
|
1750
1772
|
return result
|
|
1751
1773
|
|
|
1774
|
+
def _execute_tool_with_stream(self, tool_instance, arguments, stream_callback):
|
|
1775
|
+
"""Invoke a tool, forwarding stream callbacks when supported."""
|
|
1776
|
+
|
|
1777
|
+
tool_arguments = arguments
|
|
1778
|
+
stream_flag_key = (
|
|
1779
|
+
getattr(tool_instance, "STREAM_FLAG_KEY", None)
|
|
1780
|
+
if stream_callback
|
|
1781
|
+
else None
|
|
1782
|
+
)
|
|
1783
|
+
|
|
1784
|
+
if isinstance(arguments, dict):
|
|
1785
|
+
tool_arguments = dict(arguments)
|
|
1786
|
+
|
|
1787
|
+
if (
|
|
1788
|
+
stream_callback
|
|
1789
|
+
and stream_flag_key
|
|
1790
|
+
and stream_flag_key not in tool_arguments
|
|
1791
|
+
):
|
|
1792
|
+
tool_arguments[stream_flag_key] = True
|
|
1793
|
+
|
|
1794
|
+
if stream_callback is None:
|
|
1795
|
+
return tool_instance.run(tool_arguments), tool_arguments
|
|
1796
|
+
|
|
1797
|
+
try:
|
|
1798
|
+
signature = inspect.signature(tool_instance.run)
|
|
1799
|
+
if "stream_callback" in signature.parameters:
|
|
1800
|
+
return (
|
|
1801
|
+
tool_instance.run(
|
|
1802
|
+
tool_arguments, stream_callback=stream_callback
|
|
1803
|
+
),
|
|
1804
|
+
tool_arguments,
|
|
1805
|
+
)
|
|
1806
|
+
except (ValueError, TypeError):
|
|
1807
|
+
# If inspection fails, fall back to best-effort execution
|
|
1808
|
+
pass
|
|
1809
|
+
|
|
1810
|
+
# Tool doesn't support streaming yet; execute normally
|
|
1811
|
+
return tool_instance.run(tool_arguments), tool_arguments
|
|
1812
|
+
|
|
1752
1813
|
def toggle_hooks(self, enabled: bool):
|
|
1753
1814
|
"""
|
|
1754
1815
|
Enable or disable output hooks globally.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .base_tool import BaseTool
|
|
3
|
+
from .tool_registry import register_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register_tool("FatcatScholarTool")
|
|
7
|
+
class FatcatScholarTool(BaseTool):
|
|
8
|
+
"""
|
|
9
|
+
Search Internet Archive Scholar via Fatcat releases search.
|
|
10
|
+
|
|
11
|
+
Parameters (arguments):
|
|
12
|
+
query (str): Query string
|
|
13
|
+
max_results (int): Max results (default 10, max 100)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(self, tool_config):
|
|
17
|
+
super().__init__(tool_config)
|
|
18
|
+
self.base_url = "https://api.fatcat.wiki/v0/release/search"
|
|
19
|
+
|
|
20
|
+
def run(self, arguments=None):
|
|
21
|
+
arguments = arguments or {}
|
|
22
|
+
query = arguments.get("query")
|
|
23
|
+
max_results = int(arguments.get("max_results", 10))
|
|
24
|
+
|
|
25
|
+
if not query:
|
|
26
|
+
return {"error": "`query` parameter is required."}
|
|
27
|
+
|
|
28
|
+
params = {
|
|
29
|
+
"q": query,
|
|
30
|
+
"size": max(1, min(max_results, 100)),
|
|
31
|
+
}
|
|
32
|
+
try:
|
|
33
|
+
resp = requests.get(self.base_url, params=params, timeout=20)
|
|
34
|
+
resp.raise_for_status()
|
|
35
|
+
data = resp.json()
|
|
36
|
+
except requests.RequestException as e:
|
|
37
|
+
return {
|
|
38
|
+
"error": "Network/API error calling Fatcat",
|
|
39
|
+
"reason": str(e),
|
|
40
|
+
}
|
|
41
|
+
except ValueError:
|
|
42
|
+
return {"error": "Failed to decode Fatcat response as JSON"}
|
|
43
|
+
|
|
44
|
+
results = []
|
|
45
|
+
for r in data.get("hits", {}).get("hits", []):
|
|
46
|
+
src = r.get("_source", {})
|
|
47
|
+
title = src.get("title")
|
|
48
|
+
year = src.get("release_year")
|
|
49
|
+
doi = src.get("doi")
|
|
50
|
+
authors = src.get("contrib_names", [])
|
|
51
|
+
url = None
|
|
52
|
+
if src.get("wikidata_qid"):
|
|
53
|
+
url = f"https://fatcat.wiki/release/{src.get('ident') or ''}"
|
|
54
|
+
results.append(
|
|
55
|
+
{
|
|
56
|
+
"title": title,
|
|
57
|
+
"authors": authors,
|
|
58
|
+
"year": year,
|
|
59
|
+
"doi": doi,
|
|
60
|
+
"url": url,
|
|
61
|
+
"source": "Fatcat/IA Scholar",
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return results
|
|
66
|
+
|
tooluniverse/hal_tool.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
from .base_tool import BaseTool
|
|
3
|
+
from .tool_registry import register_tool
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@register_tool("HALTool")
|
|
7
|
+
class HALTool(BaseTool):
|
|
8
|
+
"""
|
|
9
|
+
Search the French HAL open archive via its public API.
|
|
10
|
+
|
|
11
|
+
Arguments:
|
|
12
|
+
query (str): Search term (Lucene syntax)
|
|
13
|
+
max_results (int): Max results to return (default 10, max 100)
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
tool_config,
|
|
19
|
+
base_url="https://api.archives-ouvertes.fr/search/",
|
|
20
|
+
):
|
|
21
|
+
super().__init__(tool_config)
|
|
22
|
+
self.base_url = base_url.rstrip("/") + "/"
|
|
23
|
+
|
|
24
|
+
def run(self, arguments=None):
|
|
25
|
+
arguments = arguments or {}
|
|
26
|
+
query = arguments.get("query")
|
|
27
|
+
max_results = int(arguments.get("max_results", 10))
|
|
28
|
+
if not query:
|
|
29
|
+
return {"error": "`query` parameter is required."}
|
|
30
|
+
return self._search(query, max_results)
|
|
31
|
+
|
|
32
|
+
def _search(self, query, max_results):
|
|
33
|
+
params = {
|
|
34
|
+
"q": query,
|
|
35
|
+
"wt": "json",
|
|
36
|
+
"rows": max(1, min(max_results, 100)),
|
|
37
|
+
"fl": (
|
|
38
|
+
"title_s,authFullName_s,producedDateY_i,uri_s,doiId_s,"
|
|
39
|
+
"docType_s,abstract_s,source_s"
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
try:
|
|
43
|
+
resp = requests.get(f"{self.base_url}", params=params, timeout=20)
|
|
44
|
+
resp.raise_for_status()
|
|
45
|
+
data = resp.json()
|
|
46
|
+
except requests.RequestException as e:
|
|
47
|
+
return {"error": "Network/API error calling HAL", "reason": str(e)}
|
|
48
|
+
except ValueError:
|
|
49
|
+
return {"error": "Failed to decode HAL response as JSON"}
|
|
50
|
+
|
|
51
|
+
docs = data.get("response", {}).get("docs", [])
|
|
52
|
+
results = []
|
|
53
|
+
for d in docs:
|
|
54
|
+
title = (d.get("title_s") or [None])
|
|
55
|
+
if isinstance(title, list):
|
|
56
|
+
title = title[0]
|
|
57
|
+
authors = d.get("authFullName_s") or []
|
|
58
|
+
year = d.get("producedDateY_i")
|
|
59
|
+
doi = d.get("doiId_s")
|
|
60
|
+
url = d.get("uri_s")
|
|
61
|
+
abstract = d.get("abstract_s")
|
|
62
|
+
if isinstance(abstract, list):
|
|
63
|
+
abstract = abstract[0]
|
|
64
|
+
source = d.get("source_s")
|
|
65
|
+
results.append(
|
|
66
|
+
{
|
|
67
|
+
"title": title,
|
|
68
|
+
"authors": authors,
|
|
69
|
+
"year": year,
|
|
70
|
+
"doi": doi,
|
|
71
|
+
"url": url,
|
|
72
|
+
"abstract": abstract,
|
|
73
|
+
"source": source,
|
|
74
|
+
}
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return results
|