aa-agent-tools 0.1.0__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.
@@ -0,0 +1,125 @@
1
+ """aa_agent_tools - a friendly toolbox of cool functions for kid-built agents.
2
+
3
+ Import it and call functions directly::
4
+
5
+ import aa_agent_tools as aa
6
+
7
+ aa.search_web("cute penguins", api_key="YOUR_BRAVE_KEY")
8
+ aa.fetch_page("https://example.com")
9
+ aa.get_joke()
10
+ aa.ask_ai("What is recursion?", api_key="YOUR_OPENAI_KEY")
11
+
12
+ Every function that needs a secret (an API key, a password, ...) takes it as
13
+ an argument. We never store your keys.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ __version__ = "0.1.0"
19
+
20
+ # ---- re-export the friendly public functions -----------------------------
21
+ from .apis import call_api
22
+ from .data import (
23
+ convert_money,
24
+ country_info,
25
+ define_word,
26
+ number_fact,
27
+ shorten_url,
28
+ wikipedia_summary,
29
+ )
30
+ from .email_tools import send_email, send_email_gmail
31
+ from .errors import AAError, AARequestError
32
+ from .fun import (
33
+ flip_coin,
34
+ get_advice,
35
+ get_cat_fact,
36
+ get_cat_image,
37
+ get_dog_image,
38
+ get_joke,
39
+ get_pokemon,
40
+ get_quote,
41
+ pick_one,
42
+ random_number,
43
+ roll_dice,
44
+ )
45
+ from .images import get_photo, random_image
46
+ from .space import near_earth_objects, space_image
47
+ from .text_tools import (
48
+ base64_decode,
49
+ base64_encode,
50
+ convert_units,
51
+ count_words,
52
+ hash_text,
53
+ is_palindrome,
54
+ make_qr,
55
+ reverse_text,
56
+ )
57
+ from .weather import get_forecast, get_weather
58
+ from .web import fetch_page, search_web
59
+
60
+ __all__ = [
61
+ # web
62
+ "search_web",
63
+ "fetch_page",
64
+ # email
65
+ "send_email",
66
+ "send_email_gmail",
67
+ # generic api + ai
68
+ "call_api",
69
+ # weather
70
+ "get_weather",
71
+ "get_forecast",
72
+ # space
73
+ "space_image",
74
+ "near_earth_objects",
75
+ # fun
76
+ "get_joke",
77
+ "get_advice",
78
+ "get_cat_fact",
79
+ "get_cat_image",
80
+ "get_dog_image",
81
+ "get_quote",
82
+ "roll_dice",
83
+ "flip_coin",
84
+ "random_number",
85
+ "pick_one",
86
+ "get_pokemon",
87
+ # data / knowledge
88
+ "wikipedia_summary",
89
+ "define_word",
90
+ "number_fact",
91
+ "country_info",
92
+ "convert_money",
93
+ "shorten_url",
94
+ # images
95
+ "get_photo",
96
+ "random_image",
97
+ # text tools
98
+ "hash_text",
99
+ "base64_encode",
100
+ "base64_decode",
101
+ "make_qr",
102
+ "convert_units",
103
+ "count_words",
104
+ "reverse_text",
105
+ "is_palindrome",
106
+ # errors
107
+ "AAError",
108
+ "AARequestError",
109
+ ]
110
+
111
+
112
+ def list_functions() -> list[str]:
113
+ """Return the names of every function in aa_agent_tools (handy for autocomplete)."""
114
+ return list(__all__)
115
+
116
+
117
+ def about() -> str:
118
+ """Print a friendly welcome message."""
119
+ return (
120
+ "aa_agent_tools v"
121
+ + __version__
122
+ + " - a friendly toolbox of cool functions for kid-built agents.\n"
123
+ "Try: aa.search_web(...), aa.fetch_page(...), aa.get_joke(), aa.ask_ai(...)\n"
124
+ "Need a key? Pass it as an argument - we never store your secrets."
125
+ )
@@ -0,0 +1,88 @@
1
+ """Internal HTTP helpers for aa_agent_tools.
2
+
3
+ Everything here uses only the Python standard library so it works inside a
4
+ Pyodide (WebAssembly) environment where third-party wheels like ``requests``
5
+ may not be available.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import gzip
11
+ import json
12
+ import urllib.error
13
+ import urllib.parse
14
+ import urllib.request
15
+
16
+ DEFAULT_TIMEOUT = 30
17
+ DEFAULT_USER_AGENT = "aa_agent_tools/0.1 (+https://github.com/Adullam-Technologies/aa_agent_tools)"
18
+
19
+
20
+ def build_url(base: str, params: dict | None = None) -> str:
21
+ """Add query-string ``params`` to ``base`` if any are given."""
22
+ if not params:
23
+ return base
24
+ clean = {k: v for k, v in params.items() if v is not None}
25
+ if not clean:
26
+ return base
27
+ sep = "&" if "?" in base else "?"
28
+ return base + sep + urllib.parse.urlencode(clean, doseq=True)
29
+
30
+
31
+ def request(
32
+ url: str,
33
+ method: str = "GET",
34
+ *,
35
+ params: dict | None = None,
36
+ headers: dict | None = None,
37
+ data: bytes | None = None,
38
+ json_body: dict | None = None,
39
+ timeout: int = DEFAULT_TIMEOUT,
40
+ ) -> dict:
41
+ """Make an HTTP request and return parsed JSON (or text).
42
+
43
+ Raises
44
+ ------
45
+ AARequestError
46
+ When the server returns an HTTP error status.
47
+ AAError
48
+ For other network / parsing problems.
49
+ """
50
+ from .errors import AAError, AARequestError
51
+
52
+ full_url = build_url(url, params)
53
+ merged = {"User-Agent": DEFAULT_USER_AGENT}
54
+ if headers:
55
+ merged.update({k: str(v) for k, v in headers.items()})
56
+
57
+ if json_body is not None:
58
+ data = json.dumps(json_body).encode("utf-8")
59
+ merged.setdefault("Content-Type", "application/json")
60
+ merged.setdefault("Accept", "application/json")
61
+
62
+ req = urllib.request.Request(full_url, data=data, method=method.upper(), headers=merged)
63
+
64
+ try:
65
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
66
+ raw_bytes = resp.read()
67
+ # Auto-decompress if the server sent gzip content
68
+ if resp.headers.get("Content-Encoding") == "gzip":
69
+ raw_bytes = gzip.decompress(raw_bytes)
70
+ raw = raw_bytes.decode("utf-8", errors="replace")
71
+ except urllib.error.HTTPError as exc: # status code >= 400
72
+ body = ""
73
+ try:
74
+ raw_bytes = exc.read()
75
+ if exc.headers.get("Content-Encoding") == "gzip":
76
+ raw_bytes = gzip.decompress(raw_bytes)
77
+ body = raw_bytes.decode("utf-8", errors="replace")
78
+ except Exception:
79
+ pass
80
+ raise AARequestError(exc.code, exc.reason, body) from exc
81
+ except urllib.error.URLError as exc:
82
+ raise AAError(f"Could not reach {full_url}: {exc.reason}") from exc
83
+
84
+ try:
85
+ return json.loads(raw)
86
+ except json.JSONDecodeError:
87
+ # Some endpoints return plain text; hand it back as-is.
88
+ return { "content": raw }
@@ -0,0 +1,34 @@
1
+ """Small shared helpers: key checking, text truncation, pretty printing."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from .errors import AAMissingKeyError
6
+
7
+
8
+ def require_key(value, name: str):
9
+ """Return ``value`` or raise :class:`AAMissingKeyError` if it is falsy."""
10
+ if not value:
11
+ raise AAMissingKeyError(name)
12
+ return value
13
+
14
+
15
+ def truncate(text: str, limit: int = 600) -> str:
16
+ """Trim ``text`` to ``limit`` characters and add an ellipsis if needed."""
17
+ if text is None:
18
+ return ""
19
+ text = str(text)
20
+ if len(text) <= limit:
21
+ return text
22
+ return text[: limit - 1].rstrip() + "…"
23
+
24
+
25
+ def pretty(obj) -> str:
26
+ """Return a human-friendly, readable string for any result."""
27
+ import json
28
+
29
+ if isinstance(obj, (dict, list)):
30
+ try:
31
+ return json.dumps(obj, indent=2, ensure_ascii=False)
32
+ except TypeError:
33
+ pass
34
+ return str(obj)
aa_agent_tools/apis.py ADDED
@@ -0,0 +1,60 @@
1
+ """Connect to any API with an API key (and talk to AI models).
2
+
3
+ These are the "give my agent super-powers" functions. Most web APIs just need
4
+ a GET/POST request plus a key - :func:`call_api` does that for you.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from ._http import request
10
+ from ._util import require_key
11
+
12
+
13
+ def call_api(
14
+ url: str,
15
+ api_key: str | None = None,
16
+ *,
17
+ method: str = "GET",
18
+ params: dict | None = None,
19
+ json_body: dict | None = None,
20
+ extra_headers: dict | None = None,
21
+ key_header: str = "Authorization",
22
+ key_prefix: str = "Bearer ",
23
+ ):
24
+ """Call almost any REST API in one line.
25
+
26
+ Parameters
27
+ ----------
28
+ url : str
29
+ The full API endpoint, e.g. ``"https://api.example.com/v1/things"``.
30
+ api_key : str | None
31
+ Your key. It is sent in the header ``key_header`` with ``key_prefix``.
32
+ method : str
33
+ ``"GET"``, ``"POST"``, etc.
34
+ params : dict | None
35
+ Query-string arguments (for GET requests).
36
+ json_body : dict | None
37
+ JSON body (for POST/PUT requests).
38
+ key_header, key_prefix : str, str
39
+ How to send the key. Many APIs use ``"Bearer "``; some use
40
+ ``key_header="x-api-key"`` with ``key_prefix=""``.
41
+
42
+ Example
43
+ -------
44
+ >>> data = aa.call_api(
45
+ ... "https://api.github.com/repos/octocat/Hello-World",
46
+ ... key_header="Accept",
47
+ ... key_prefix="application/vnd.github+json",
48
+ ... )
49
+ >>> print(data["stargazers_count"])
50
+ """
51
+ headers = dict(extra_headers or {})
52
+ if api_key:
53
+ headers[key_header] = f"{key_prefix}{api_key}"
54
+ return request(
55
+ url,
56
+ method=method,
57
+ params=params,
58
+ json_body=json_body,
59
+ headers=headers,
60
+ )
aa_agent_tools/data.py ADDED
@@ -0,0 +1,163 @@
1
+ """Look up facts, words, and numbers from free public knowledge APIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ._http import request
6
+ from ._util import truncate
7
+
8
+
9
+ def _is_prime(n: int) -> bool:
10
+ if n < 2:
11
+ return False
12
+ if n % 2 == 0:
13
+ return n == 2
14
+ i = 3
15
+ while i * i <= n:
16
+ if n % i == 0:
17
+ return False
18
+ i += 2
19
+ return True
20
+
21
+
22
+ def wikipedia_summary(title: str, *, sentences: int = 3):
23
+ """Get a short, friendly summary of a Wikipedia topic.
24
+
25
+ Parameters
26
+ ----------
27
+ title : str
28
+ The topic, e.g. ``"Black hole"`` or ``"Turtle"``.
29
+ sentences : int
30
+ About how many sentences to include.
31
+
32
+ Returns
33
+ -------
34
+ dict
35
+ ``title``, ``summary``, ``url`` and ``image``.
36
+
37
+ Example
38
+ -------
39
+ >>> info = aa.wikipedia_summary("Aurora")
40
+ >>> print(info["summary"])
41
+ """
42
+ try:
43
+ data = request(
44
+ f"https://en.wikipedia.org/api/rest_v1/page/summary/{title.replace(' ', '_')}"
45
+ )
46
+ except Exception:
47
+ return {"title": title, "summary": "", "url": "", "image": ""}
48
+ return {
49
+ "title": data.get("title", title),
50
+ "summary": data.get("extract", ""),
51
+ "url": (data.get("content_urls") or {}).get("desktop", {}).get("page", ""),
52
+ "image": (data.get("thumbnail") or {}).get("source", ""),
53
+ }
54
+
55
+
56
+ def define_word(word: str):
57
+ """Get the dictionary definition and example sentence for a word.
58
+
59
+ Example
60
+ -------
61
+ >>> d = aa.define_word("serendipity")
62
+ >>> print(d["definitions"][0])
63
+ """
64
+ try:
65
+ data = request(f"https://api.dictionaryapi.dev/api/v2/entries/en/{word.lower()}")
66
+ except Exception:
67
+ return {"word": word, "phonetic": "", "definitions": []}
68
+ if isinstance(data, dict): # error response
69
+ return {"word": word, "phonetic": "", "definitions": []}
70
+ elif isinstance(data, list):
71
+ defs = []
72
+ phonetic = ""
73
+ for entry in list(data):
74
+ phonetic = phonetic or entry.get("phonetic", "")
75
+ for meaning in entry.get("meanings", []):
76
+ for d in meaning.get("definitions", []):
77
+ defs.append(
78
+ f"({meaning.get('partOfSpeech', '')}) {d.get('definition', '')}"
79
+ + (f" e.g. {d['example']}" if d.get("example") else "")
80
+ )
81
+ return {"word": word, "phonetic": phonetic, "definitions": defs[:5]}
82
+ return {"word": word, "phonetic": "", "definitions": []}
83
+
84
+
85
+ def number_fact(number, *, kind: str = "math"):
86
+ """Get a fun, fact-packed description of a number (works offline!).
87
+
88
+ The fact is computed for you, so it never needs the internet. Try
89
+ ``kind="math"`` for number properties, or ``kind="year"`` / ``kind="date"``
90
+ for a playful note.
91
+
92
+ Example
93
+ -------
94
+ >>> aa.number_fact(7)
95
+ '7 has 1 digit(s). Its digits add up to 7. 7 is odd. 7 is a prime number ...'
96
+ """
97
+ try:
98
+ n = int(number)
99
+ except (TypeError, ValueError):
100
+ return f"{number} is... a number of some kind!"
101
+ facts = [f"{n} has {len(str(abs(n)))} digit(s)."]
102
+ digit_sum = sum(int(c) for c in str(abs(n)))
103
+ facts.append(f"Its digits add up to {digit_sum}.")
104
+ facts.append(f"{n} is {'even' if n % 2 == 0 else 'odd'}.")
105
+ if _is_prime(abs(n)):
106
+ facts.append(f"{n} is a prime number - only divisible by 1 and itself!")
107
+ root = int(abs(n) ** 0.5)
108
+ if root * root == abs(n) and n >= 0:
109
+ facts.append(f"{n} is a perfect square ({root} × {root}).")
110
+ if n != 0 and 1_000_000 % n == 0:
111
+ facts.append(f"{n} is a factor of 1,000,000.")
112
+ tail = {
113
+ "year": f" If {n} were a year, it would be in the {'BC' if n < 0 else 'AD'} era.",
114
+ "date": f" The {n}th day of the year is a special one!",
115
+ "trivia": "",
116
+ "math": "",
117
+ }.get(kind, "")
118
+ return " ".join(facts) + tail
119
+
120
+
121
+ def country_info(name: str):
122
+ """Get a friendly snapshot of a country using Wikipedia.
123
+
124
+ Example
125
+ -------
126
+ >>> c = aa.country_info("Japan")
127
+ >>> print(c["name"]); print(c["summary"][:80])
128
+ """
129
+ info = wikipedia_summary(name)
130
+ return {
131
+ "name": info.get("title") or name,
132
+ "summary": info.get("summary", ""),
133
+ "url": info.get("url", ""),
134
+ "image": info.get("image", ""),
135
+ }
136
+
137
+
138
+ def convert_money(amount: float, from_currency: str, to_currency: str):
139
+ """Convert money between currencies using live rates (no key needed).
140
+
141
+ Example
142
+ -------
143
+ >>> aa.convert_money(10, "USD", "EUR")
144
+ """
145
+ from ._util import require_key
146
+
147
+ data = request("https://open.er-api.com/v6/latest/" + from_currency.upper())
148
+ rates = data.get("rates", {})
149
+ rate = rates.get(to_currency.upper())
150
+ if rate is None:
151
+ return None
152
+ return round(amount * float(rate), 2)
153
+
154
+
155
+ def shorten_url(url: str):
156
+ """Make a long URL short using the free is.gd service."""
157
+ try:
158
+ data = request("https://is.gd/create.php", params={"format": "json", "url": url})
159
+ if isinstance(data, dict):
160
+ return data.get("shorturl", url)
161
+ except Exception:
162
+ pass
163
+ return url
@@ -0,0 +1,100 @@
1
+ """Send emails from your agent.
2
+
3
+ Uses only the standard library's ``smtplib`` so it works in Pyodide. You will
4
+ need an email account that allows "app passwords" (Gmail, Outlook, etc.).
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import smtplib
10
+ import ssl
11
+ from email.message import EmailMessage
12
+
13
+ from ._util import require_key
14
+
15
+
16
+ def send_email(
17
+ to: str,
18
+ subject: str,
19
+ body: str,
20
+ *,
21
+ smtp_host: str,
22
+ smtp_port: int = 587,
23
+ username: str,
24
+ password: str,
25
+ from_addr: str | None = None,
26
+ use_tls: bool = True,
27
+ ):
28
+ """Send a plain-text email through any SMTP server.
29
+
30
+ Parameters
31
+ ----------
32
+ to : str
33
+ Recipient email address.
34
+ subject, body : str
35
+ Email subject and message text.
36
+ smtp_host, smtp_port : str, int
37
+ Your email provider's SMTP server (e.g. ``"smtp.gmail.com"``, port 587).
38
+ username, password : str
39
+ Login + app password (NOT your normal account password).
40
+ from_addr : str | None
41
+ Sender address (defaults to ``username``).
42
+
43
+ Example
44
+ -------
45
+ >>> aa.send_email(
46
+ ... to="friend@example.com",
47
+ ... subject="Hello from my agent!",
48
+ ... body="My robot wrote this email.",
49
+ ... smtp_host="smtp.gmail.com",
50
+ ... username="you@gmail.com",
51
+ ... password="app-password-here",
52
+ ... )
53
+ """
54
+ require_key(username, "username")
55
+ require_key(password, "password")
56
+ sender = from_addr or username
57
+
58
+ msg = EmailMessage()
59
+ msg["Subject"] = subject
60
+ msg["From"] = sender
61
+ msg["To"] = to
62
+ msg.set_content(body)
63
+
64
+ with smtplib.SMTP(smtp_host, smtp_port, timeout=30) as server:
65
+ if use_tls:
66
+ context = ssl.create_default_context()
67
+ server.starttls(context=context)
68
+ server.login(username, password)
69
+ server.send_message(msg)
70
+ return f"Email sent to {to}"
71
+
72
+
73
+ def send_email_gmail(to: str, subject: str, body: str, *, gmail: str, app_password: str):
74
+ """Send an email using a Gmail account.
75
+
76
+ ``gmail`` is your full address (e.g. ``"you@gmail.com"``) and
77
+ ``app_password`` is a 16-character Gmail app password, not your normal
78
+ password. Make one at https://myaccount.google.com/apppasswords.
79
+
80
+ Example
81
+ -------
82
+ >>> aa.send_email_gmail(
83
+ ... to="friend@example.com",
84
+ ... subject="hi!",
85
+ ... body="sent from aa_agent_tools",
86
+ ... gmail="you@gmail.com",
87
+ ... app_password="abcd efgh ijkl mnop",
88
+ ... )
89
+ """
90
+ return send_email(
91
+ to,
92
+ subject,
93
+ body,
94
+ smtp_host="smtp.gmail.com",
95
+ smtp_port=587,
96
+ username=gmail,
97
+ password=app_password,
98
+ from_addr=gmail,
99
+ use_tls=True,
100
+ )
@@ -0,0 +1,36 @@
1
+ """Friendly error types for aa_agent_tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+
6
+ class AAError(Exception):
7
+ """Base class for every error aa_agent_tools can raise."""
8
+
9
+
10
+ class AARequestError(AAError):
11
+ """Raised when a web request comes back with an HTTP error status."""
12
+
13
+ def __init__(self, status: int, reason: str, body: str = ""):
14
+ self.status = status
15
+ self.reason = reason
16
+ self.body = body
17
+ hint = ""
18
+ if status == 401:
19
+ hint = " (did you forget your API key, or is it wrong?)"
20
+ elif status == 403:
21
+ hint = " (this key is not allowed to do that)"
22
+ elif status == 404:
23
+ hint = " (nothing was found at that address)"
24
+ elif status == 429:
25
+ hint = " (you've hit a rate limit - wait a moment and try again)"
26
+ super().__init__(f"HTTP {status} {reason}{hint}".strip())
27
+
28
+
29
+ class AAMissingKeyError(AAError):
30
+ """Raised when a required API key was not supplied."""
31
+
32
+ def __init__(self, name: str):
33
+ self.key_name = name
34
+ super().__init__(
35
+ f"Missing API key: {name}. Pass it as an argument, e.g. {name}='YOUR_KEY'."
36
+ )