personal memory agent
1# SPDX-License-Identifier: AGPL-3.0-only
2# Copyright (c) 2026 sol pbc
3
4"""Support tool functions for agent workflows.
5
6Each function provides a discrete capability that both the talent agent
7(via ``sol call support``) and the convey routes can use. All outbound
8operations are **consent-gated** — they return a draft for review rather
9than submitting directly.
10"""
11
12from __future__ import annotations
13
14import logging
15from typing import Any
16
17logger = logging.getLogger(__name__)
18
19
20def support_diagnose() -> dict[str, Any]:
21 """Run local diagnostics — no network.
22
23 Returns a dict of system state suitable for the ``user_context`` field.
24 """
25 from apps.support.diagnostics import collect_all
26
27 return collect_all()
28
29
30def support_search(query: str, portal_url: str | None = None) -> list[dict[str, Any]]:
31 """Search the knowledge base for articles matching *query*."""
32 from apps.support.portal import get_client
33
34 client = get_client(portal_url=portal_url)
35 return client.search_articles(query=query)
36
37
38def support_article(slug: str, portal_url: str | None = None) -> dict[str, Any]:
39 """Read a single KB article by slug."""
40 from apps.support.portal import get_client
41
42 client = get_client(portal_url=portal_url)
43 return client.get_article(slug)
44
45
46def support_create(
47 *,
48 subject: str,
49 description: str,
50 product: str = "solstone",
51 severity: str = "medium",
52 category: str | None = None,
53 user_email: str | None = None,
54 user_context: dict | None = None,
55 auto_context: bool = True,
56 portal_url: str | None = None,
57 anonymous: bool = False,
58) -> dict[str, Any]:
59 """Create a support ticket.
60
61 If *auto_context* is True (default), diagnostic data is collected
62 and merged into *user_context*.
63 """
64 from apps.support.portal import get_client
65
66 if auto_context:
67 from apps.support.diagnostics import collect_all
68
69 diag = collect_all()
70 if user_context:
71 diag.update(user_context)
72 user_context = diag
73
74 client = get_client(portal_url=portal_url, anonymous=anonymous)
75 return client.create_ticket(
76 product=product,
77 subject=subject,
78 description=description,
79 severity=severity,
80 category=category,
81 user_email=user_email,
82 user_context=user_context,
83 )
84
85
86def support_feedback(
87 *,
88 body: str,
89 product: str = "solstone",
90 portal_url: str | None = None,
91 anonymous: bool = False,
92) -> dict[str, Any]:
93 """Submit feedback (lower-friction path).
94
95 Feedback is a ticket with ``category="feedback"`` and low severity.
96 """
97 return support_create(
98 subject="User feedback",
99 description=body,
100 product=product,
101 severity="low",
102 category="feedback",
103 portal_url=portal_url,
104 anonymous=anonymous,
105 )
106
107
108def support_list(
109 *,
110 status: str | None = None,
111 portal_url: str | None = None,
112) -> list[dict[str, Any]]:
113 """List the user's tickets."""
114 from apps.support.portal import get_client
115
116 client = get_client(portal_url=portal_url)
117 return client.list_tickets(status=status)
118
119
120def support_check(
121 ticket_id: int,
122 portal_url: str | None = None,
123) -> dict[str, Any]:
124 """Check status of a specific ticket (with message thread)."""
125 from apps.support.portal import get_client
126
127 client = get_client(portal_url=portal_url)
128 return client.get_ticket(ticket_id)
129
130
131def support_reply(
132 ticket_id: int,
133 content: str,
134 portal_url: str | None = None,
135) -> dict[str, Any]:
136 """Reply to a ticket."""
137 from apps.support.portal import get_client
138
139 client = get_client(portal_url=portal_url)
140 return client.reply_to_ticket(ticket_id, content)
141
142
143def support_attach(
144 ticket_id: int,
145 file_path: str,
146 *,
147 filename: str | None = None,
148 portal_url: str | None = None,
149) -> dict[str, Any]:
150 """Attach a file to an existing ticket.
151
152 Returns the attachment metadata from the portal.
153 """
154 from pathlib import Path
155
156 from apps.support.portal import get_client
157
158 client = get_client(portal_url=portal_url)
159 return client.attach_file(ticket_id, Path(file_path), filename=filename)
160
161
162def support_announcements(
163 portal_url: str | None = None,
164) -> list[dict[str, Any]]:
165 """List active announcements."""
166 from apps.support.portal import get_client
167
168 client = get_client(portal_url=portal_url)
169 return client.list_announcements()