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.
- aa_agent_tools/__init__.py +125 -0
- aa_agent_tools/_http.py +88 -0
- aa_agent_tools/_util.py +34 -0
- aa_agent_tools/apis.py +60 -0
- aa_agent_tools/data.py +163 -0
- aa_agent_tools/email_tools.py +100 -0
- aa_agent_tools/errors.py +36 -0
- aa_agent_tools/fun.py +111 -0
- aa_agent_tools/images.py +27 -0
- aa_agent_tools/py.typed +0 -0
- aa_agent_tools/space.py +55 -0
- aa_agent_tools/text_tools.py +83 -0
- aa_agent_tools/weather.py +68 -0
- aa_agent_tools/web.py +92 -0
- aa_agent_tools-0.1.0.dist-info/METADATA +434 -0
- aa_agent_tools-0.1.0.dist-info/RECORD +17 -0
- aa_agent_tools-0.1.0.dist-info/WHEEL +4 -0
|
@@ -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
|
+
)
|
aa_agent_tools/_http.py
ADDED
|
@@ -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 }
|
aa_agent_tools/_util.py
ADDED
|
@@ -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
|
+
)
|
aa_agent_tools/errors.py
ADDED
|
@@ -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
|
+
)
|