pi-agent-toolkit 0.1.0

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.
Files changed (53) hide show
  1. package/dist/dotfiles/AGENTS.md +197 -0
  2. package/dist/dotfiles/APPEND_SYSTEM.md +78 -0
  3. package/dist/dotfiles/agent-modes.json +12 -0
  4. package/dist/dotfiles/agent-skills/exa-search/.env.example +4 -0
  5. package/dist/dotfiles/agent-skills/exa-search/SKILL.md +234 -0
  6. package/dist/dotfiles/agent-skills/exa-search/scripts/exa-api.cjs +197 -0
  7. package/dist/dotfiles/auth.json.template +5 -0
  8. package/dist/dotfiles/damage-control-rules.yaml +318 -0
  9. package/dist/dotfiles/extensions/btw.ts +1031 -0
  10. package/dist/dotfiles/extensions/commit-approval.ts +590 -0
  11. package/dist/dotfiles/extensions/context.ts +578 -0
  12. package/dist/dotfiles/extensions/control.ts +1748 -0
  13. package/dist/dotfiles/extensions/damage-control/index.ts +543 -0
  14. package/dist/dotfiles/extensions/damage-control/node_modules/.package-lock.json +22 -0
  15. package/dist/dotfiles/extensions/damage-control/package-lock.json +28 -0
  16. package/dist/dotfiles/extensions/damage-control/package.json +7 -0
  17. package/dist/dotfiles/extensions/dirty-repo-guard.ts +56 -0
  18. package/dist/dotfiles/extensions/exa-enforce.ts +51 -0
  19. package/dist/dotfiles/extensions/exa-search-tool.ts +384 -0
  20. package/dist/dotfiles/extensions/execute-command/index.ts +82 -0
  21. package/dist/dotfiles/extensions/files.ts +1112 -0
  22. package/dist/dotfiles/extensions/loop.ts +446 -0
  23. package/dist/dotfiles/extensions/pr-approval.ts +730 -0
  24. package/dist/dotfiles/extensions/qna-interactive.ts +532 -0
  25. package/dist/dotfiles/extensions/question-mode.ts +242 -0
  26. package/dist/dotfiles/extensions/require-session-name-on-exit.ts +141 -0
  27. package/dist/dotfiles/extensions/review.ts +2091 -0
  28. package/dist/dotfiles/extensions/session-breakdown.ts +1629 -0
  29. package/dist/dotfiles/extensions/term-notify.ts +150 -0
  30. package/dist/dotfiles/extensions/tilldone.ts +527 -0
  31. package/dist/dotfiles/extensions/todos.ts +2082 -0
  32. package/dist/dotfiles/extensions/tools.ts +146 -0
  33. package/dist/dotfiles/extensions/uv.ts +123 -0
  34. package/dist/dotfiles/global-skills/brainstorm/SKILL.md +10 -0
  35. package/dist/dotfiles/global-skills/cli-detector/SKILL.md +192 -0
  36. package/dist/dotfiles/global-skills/gh-issue-creator/SKILL.md +173 -0
  37. package/dist/dotfiles/global-skills/google-chat-cards-v2/SKILL.md +237 -0
  38. package/dist/dotfiles/global-skills/google-chat-cards-v2/references/bridge_tap_implementation.md +466 -0
  39. package/dist/dotfiles/global-skills/technical-docs/SKILL.md +204 -0
  40. package/dist/dotfiles/global-skills/technical-docs/references/diagrams.md +168 -0
  41. package/dist/dotfiles/global-skills/technical-docs/references/examples.md +449 -0
  42. package/dist/dotfiles/global-skills/technical-docs/scripts/validate_docs.py +352 -0
  43. package/dist/dotfiles/global-skills/whats-new/SKILL.md +159 -0
  44. package/dist/dotfiles/intercepted-commands/pip +7 -0
  45. package/dist/dotfiles/intercepted-commands/pip3 +7 -0
  46. package/dist/dotfiles/intercepted-commands/poetry +10 -0
  47. package/dist/dotfiles/intercepted-commands/python +104 -0
  48. package/dist/dotfiles/intercepted-commands/python3 +104 -0
  49. package/dist/dotfiles/mcp.json.template +32 -0
  50. package/dist/dotfiles/models.json +27 -0
  51. package/dist/dotfiles/settings.json +25 -0
  52. package/dist/index.js +1344 -0
  53. package/package.json +34 -0
@@ -0,0 +1,237 @@
1
+ ---
2
+ name: google-chat-cards-v2
3
+ description: Implement Google Chat Cards v2 format for rich alert notifications. This skill should be used when migrating from plain text messages to Cards v2 format, adding visual alerts to Google Chat webhooks, or building card-based notification systems.
4
+ context: fork
5
+ ---
6
+
7
+ # Google Chat Cards v2 Implementation
8
+
9
+ This skill provides patterns and guidance for implementing Google Chat Cards v2 rich message format in Python services.
10
+
11
+ ## When to Use
12
+
13
+ - Migrating existing plain text Google Chat alerts to Cards v2 format
14
+ - Adding new alert/notification functionality with rich formatting
15
+ - Implementing error alerting systems with structured card layouts
16
+
17
+ ## Cards v2 Structure
18
+
19
+ Cards v2 uses a nested structure with header, sections, and widgets:
20
+
21
+ ```json
22
+ {
23
+ "cardsV2": [{
24
+ "cardId": "unique-card-id",
25
+ "card": {
26
+ "header": {
27
+ "title": "Alert Title",
28
+ "subtitle": "service-name",
29
+ "imageUrl": "https://icon-url.svg",
30
+ "imageType": "CIRCLE"
31
+ },
32
+ "sections": [
33
+ {
34
+ "header": "Section Header (optional)",
35
+ "widgets": [...]
36
+ }
37
+ ]
38
+ }
39
+ }]
40
+ }
41
+ ```
42
+
43
+ ## Implementation Pattern
44
+
45
+ ### Step 1: Add Module-Level Retry Configuration
46
+
47
+ Extract retry configuration to a module-level constant for reuse across methods:
48
+
49
+ ```python
50
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
51
+ import httpx
52
+
53
+ # Shared retry configuration for network errors
54
+ _RETRY_CONFIG = retry(
55
+ stop=stop_after_attempt(3),
56
+ wait=wait_exponential(multiplier=1, min=2, max=10),
57
+ retry=retry_if_exception_type((httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError)),
58
+ )
59
+ ```
60
+
61
+ ### Step 2: Add send_card() Method
62
+
63
+ Add a method to send Cards v2 payloads alongside existing `send_message()`:
64
+
65
+ ```python
66
+ @_RETRY_CONFIG
67
+ def send_card(self, card_payload: dict[str, Any]) -> ChatSendMessageResponse:
68
+ """Send a Cards v2 message to Google Chat.
69
+
70
+ :param card_payload: The card payload dict with cardsV2 structure.
71
+ :returns: Response containing success status.
72
+ """
73
+ if error := self._validate_webhook("Card"):
74
+ return error
75
+ if "cardsV2" not in card_payload:
76
+ logger.error("Invalid card payload: missing 'cardsV2' key")
77
+ return ChatSendMessageResponse(success=False, error="Invalid card payload: missing 'cardsV2' key")
78
+ return self._send_request(card_payload)
79
+ ```
80
+
81
+ ### Step 3: Define Class Constants
82
+
83
+ Use ClassVar for static mappings that define card content:
84
+
85
+ ```python
86
+ from typing import ClassVar
87
+
88
+ class AlerterClass:
89
+ SERVICE_NAME: ClassVar[str] = "my-service-name"
90
+
91
+ CATEGORY_TITLE_MAP: ClassVar[dict[ErrorCategory, str]] = {
92
+ ErrorCategory.CONFIG_ERROR: "Configuration Error",
93
+ ErrorCategory.SYSTEM_ERROR: "System Error",
94
+ # ... add your categories
95
+ }
96
+
97
+ # Google Material Symbols icons from Google Fonts CDN
98
+ # Source: https://fonts.google.com/icons
99
+ SEVERITY_ICON_URLS: ClassVar[dict[SeverityLevel, str]] = {
100
+ SeverityLevel.CRITICAL: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/error/default/48px.svg",
101
+ SeverityLevel.HIGH: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/warning/default/48px.svg",
102
+ SeverityLevel.MEDIUM: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/info/default/48px.svg",
103
+ SeverityLevel.LOW: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/check_circle/default/48px.svg",
104
+ }
105
+ ```
106
+
107
+ ### Step 4: Build Card Payload with Helper Methods
108
+
109
+ Break card building into focused helper methods:
110
+
111
+ ```python
112
+ def _build_card_payload(self, payload: ErrorAlertPayload) -> dict[str, Any]:
113
+ """Build Cards v2 payload for Google Chat webhook."""
114
+ card_id = f"alert-{payload.job_id}-{payload.timestamp.strftime('%Y%m%d%H%M%S')}"
115
+
116
+ return {
117
+ "cardsV2": [{
118
+ "cardId": card_id,
119
+ "card": {
120
+ "header": self._build_card_header(payload),
121
+ "sections": [
122
+ self._build_job_info_section(payload),
123
+ self._build_error_details_section(payload),
124
+ self._build_error_message_section(payload),
125
+ ],
126
+ },
127
+ }]
128
+ }
129
+
130
+ def _build_card_header(self, payload: ErrorAlertPayload) -> dict[str, Any]:
131
+ """Build the card header section."""
132
+ return {
133
+ "title": f"Alert: {self.CATEGORY_TITLE_MAP.get(payload.error_category, 'Error')}",
134
+ "subtitle": self.SERVICE_NAME,
135
+ "imageUrl": self.SEVERITY_ICON_URLS.get(payload.severity),
136
+ "imageType": "CIRCLE",
137
+ }
138
+ ```
139
+
140
+ ## Widget Types
141
+
142
+ ### Decorated Text (with icons)
143
+
144
+ Use for key-value pairs with optional icons:
145
+
146
+ ```python
147
+ {
148
+ "decoratedText": {
149
+ "startIcon": {"knownIcon": "BOOKMARK"}, # Optional icon
150
+ "topLabel": "Job ID",
151
+ "text": "12345",
152
+ }
153
+ }
154
+ ```
155
+
156
+ Available `knownIcon` values: `BOOKMARK`, `DESCRIPTION`, `PERSON`, `CLOCK`, `EMAIL`, `STAR`, etc.
157
+
158
+ ### Decorated Text (label only)
159
+
160
+ For simpler key-value display:
161
+
162
+ ```python
163
+ {"decoratedText": {"topLabel": "Stage", "text": "validation"}}
164
+ ```
165
+
166
+ ### Text Paragraph
167
+
168
+ For longer text content:
169
+
170
+ ```python
171
+ {"textParagraph": {"text": "Error message or description here"}}
172
+ ```
173
+
174
+ ## Section Structure
175
+
176
+ Sections can have an optional header and contain widgets:
177
+
178
+ ```python
179
+ {
180
+ "header": "Error Details", # Optional
181
+ "widgets": [
182
+ {"decoratedText": {"topLabel": "Stage", "text": payload.stage}},
183
+ {"decoratedText": {"topLabel": "Category", "text": payload.error_category.value}},
184
+ ],
185
+ }
186
+ ```
187
+
188
+ ## Testing Pattern
189
+
190
+ Use pytest fixtures for reusable test setup:
191
+
192
+ ```python
193
+ @pytest.fixture
194
+ def alerter() -> Generator[ErrorAlerter, None, None]:
195
+ alerter = ErrorAlerter(webhook_url="https://webhook.test")
196
+ yield alerter
197
+ alerter.close()
198
+
199
+ @pytest.fixture
200
+ def mock_successful_response() -> MagicMock:
201
+ mock_response = MagicMock()
202
+ mock_response.status_code = 200
203
+ mock_response.content = b'{"name": "spaces/xxx/messages/yyy"}'
204
+ mock_response.json.return_value = {"name": "spaces/xxx/messages/yyy"}
205
+ mock_response.raise_for_status = MagicMock()
206
+ return mock_response
207
+
208
+ def test_send_alert_builds_card_v2_payload(alerter, mock_successful_response):
209
+ """Test that send_alert builds proper Cards v2 structure."""
210
+ with patch.object(alerter._client.client, "post", return_value=mock_successful_response):
211
+ result = alerter.send_alert(...)
212
+
213
+ # Verify cardsV2 structure
214
+ call_args = alerter._client.client.post.call_args
215
+ payload = call_args.kwargs["json"]
216
+ assert "cardsV2" in payload
217
+ assert payload["cardsV2"][0]["card"]["header"]["title"] == "Expected Title"
218
+ ```
219
+
220
+ ## Reference Implementation
221
+
222
+ See `references/bridge_tap_implementation.md` for the complete reference implementation from bridge-tap-api.
223
+
224
+ ## Checklist
225
+
226
+ When implementing Cards v2:
227
+
228
+ 1. [ ] Add module-level `_RETRY_CONFIG` constant
229
+ 2. [ ] Add `send_card()` method with validation
230
+ 3. [ ] Define `SERVICE_NAME` class constant
231
+ 4. [ ] Define `CATEGORY_TITLE_MAP` for alert titles
232
+ 5. [ ] Define `SEVERITY_ICON_URLS` with Google Material Symbols
233
+ 6. [ ] Create `_build_card_payload()` method
234
+ 7. [ ] Create helper methods for each section
235
+ 8. [ ] Update tests with pytest fixtures
236
+ 9. [ ] Verify card structure in tests
237
+ 10. [ ] Run type checks (mypy) and linting (ruff)
@@ -0,0 +1,466 @@
1
+ # Bridge-TAP API Reference Implementation
2
+
3
+ Complete reference implementation of Google Chat Cards v2 for error alerting.
4
+
5
+ ## Full Client Implementation
6
+
7
+ ```python
8
+ """Google Chat client for sending notifications to Google Chat spaces."""
9
+
10
+ import os
11
+ from datetime import UTC, datetime
12
+ from types import TracebackType
13
+ from typing import Any, ClassVar
14
+
15
+ import httpx
16
+ from loguru import logger
17
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_exponential
18
+
19
+ from bridge_tap_api.sdk.google_chat.models import (
20
+ ChatSendMessageRequest,
21
+ ChatSendMessageResponse,
22
+ ErrorAlertPayload,
23
+ ErrorCategory,
24
+ GoogleChatClientConfig,
25
+ MessageType,
26
+ SeverityLevel,
27
+ )
28
+
29
+ # Shared retry configuration for network errors
30
+ _RETRY_CONFIG = retry(
31
+ stop=stop_after_attempt(3),
32
+ wait=wait_exponential(multiplier=1, min=2, max=10),
33
+ retry=retry_if_exception_type((httpx.ConnectError, httpx.TimeoutException, httpx.NetworkError)),
34
+ )
35
+
36
+
37
+ class GoogleChatClient:
38
+ """Client for sending messages to Google Chat spaces."""
39
+
40
+ MAX_MESSAGE_LENGTH = 4096
41
+ TRUNCATION_SUFFIX = "\n...[truncated]"
42
+
43
+ def __init__(self, config: GoogleChatClientConfig) -> None:
44
+ self.config = config
45
+ self.client = httpx.Client(timeout=config.timeout)
46
+
47
+ if not self.config.webhook_url and not self.config.allow_empty_webhook:
48
+ logger.warning("No webhook URL provided. Messages will not be sent.")
49
+
50
+ def __enter__(self) -> "GoogleChatClient":
51
+ return self
52
+
53
+ def __exit__(
54
+ self,
55
+ exc_type: type[BaseException] | None,
56
+ exc_val: BaseException | None,
57
+ exc_tb: TracebackType | None,
58
+ ) -> None:
59
+ self.close()
60
+
61
+ def close(self) -> None:
62
+ self.client.close()
63
+
64
+ def _validate_webhook(self, operation: str) -> ChatSendMessageResponse | None:
65
+ """Validate webhook URL is configured."""
66
+ if not self.config.webhook_url:
67
+ logger.error("No webhook URL configured. {} not sent.", operation)
68
+ return ChatSendMessageResponse(success=False, error="No webhook URL configured")
69
+ return None
70
+
71
+ @_RETRY_CONFIG
72
+ def send_card(self, card_payload: dict[str, Any]) -> ChatSendMessageResponse:
73
+ """Send a Cards v2 message to Google Chat."""
74
+ if error := self._validate_webhook("Card"):
75
+ return error
76
+ if "cardsV2" not in card_payload:
77
+ logger.error("Invalid card payload: missing 'cardsV2' key")
78
+ return ChatSendMessageResponse(success=False, error="Invalid card payload: missing 'cardsV2' key")
79
+ return self._send_request(card_payload)
80
+
81
+ def _send_request(self, payload: dict[str, Any]) -> ChatSendMessageResponse:
82
+ """Send a request to the Google Chat webhook."""
83
+ try:
84
+ response = self.client.post(
85
+ self.config.webhook_url,
86
+ json=payload,
87
+ headers={"Content-Type": "application/json"},
88
+ )
89
+ response.raise_for_status()
90
+
91
+ logger.info("Message sent to Google Chat successfully")
92
+ response_data = response.json() if response.content else {}
93
+ return ChatSendMessageResponse(success=True, response_data=response_data)
94
+
95
+ except httpx.HTTPStatusError as e:
96
+ error_msg = f"HTTP error sending to Google Chat: {e.response.status_code}"
97
+ logger.error("Failed to send message to Google Chat: {}", error_msg)
98
+ return ChatSendMessageResponse(success=False, error=error_msg)
99
+
100
+ except httpx.RequestError as e:
101
+ error_msg = f"Request error sending to Google Chat: {e}"
102
+ logger.error("Failed to send message to Google Chat: {}", error_msg)
103
+ raise
104
+
105
+
106
+ class ErrorAlerter:
107
+ """Specialized alerter for sending error notifications to Google Chat."""
108
+
109
+ SERVICE_NAME: ClassVar[str] = "bridge-tap-api"
110
+
111
+ CATEGORY_TITLE_MAP: ClassVar[dict[ErrorCategory, str]] = {
112
+ ErrorCategory.CONFIG_ERROR: "Configuration Error",
113
+ ErrorCategory.SYSTEM_ERROR: "System Error",
114
+ ErrorCategory.ETL_REQUIRED: "ETL Required",
115
+ ErrorCategory.USER_FILE: "User File Error",
116
+ }
117
+
118
+ # Google Material Symbols icons served from Google Fonts CDN.
119
+ # Source: https://fonts.google.com/icons
120
+ SEVERITY_ICON_URLS: ClassVar[dict[SeverityLevel, str]] = {
121
+ SeverityLevel.CRITICAL: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/error/default/48px.svg",
122
+ SeverityLevel.HIGH: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/warning/default/48px.svg",
123
+ SeverityLevel.MEDIUM: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/info/default/48px.svg",
124
+ SeverityLevel.LOW: "https://fonts.gstatic.com/s/i/short-term/release/googlesymbols/check_circle/default/48px.svg",
125
+ }
126
+
127
+ def __init__(self, webhook_url: str | None = None) -> None:
128
+ if webhook_url is not None:
129
+ self.webhook_url = webhook_url or os.getenv("GOOGLE_CHAT_ERROR_WEBHOOK_URL", "")
130
+ else:
131
+ self.webhook_url = os.getenv("GOOGLE_CHAT_ERROR_WEBHOOK_URL", "")
132
+ config = GoogleChatClientConfig(
133
+ webhook_url=self.webhook_url,
134
+ allow_empty_webhook=not self.webhook_url,
135
+ )
136
+ self._client = GoogleChatClient(config=config)
137
+
138
+ def __enter__(self) -> "ErrorAlerter":
139
+ return self
140
+
141
+ def __exit__(
142
+ self,
143
+ exc_type: type[BaseException] | None,
144
+ exc_val: BaseException | None,
145
+ exc_tb: TracebackType | None,
146
+ ) -> None:
147
+ self.close()
148
+
149
+ def close(self) -> None:
150
+ self._client.close()
151
+
152
+ def send_alert(
153
+ self,
154
+ error_category: str,
155
+ job: dict[str, Any],
156
+ error_info: dict[str, Any],
157
+ severity: str = "medium",
158
+ ) -> bool:
159
+ """Send alert to Google Chat webhook for error notification."""
160
+ if not self.webhook_url:
161
+ logger.warning("No webhook URL configured. Alert not sent for job {}", job.get("id", "unknown"))
162
+ return False
163
+
164
+ alert_payload = self._build_alert_payload(error_category, job, error_info, severity)
165
+ card_payload = self._build_card_payload(alert_payload)
166
+
167
+ logger.info(
168
+ "Sending {} alert to Google Chat for job {} (severity: {})",
169
+ error_category,
170
+ job.get("id", "unknown"),
171
+ severity,
172
+ )
173
+
174
+ response = self._client.send_card(card_payload)
175
+
176
+ if response.success:
177
+ logger.info("Alert sent successfully to Google Chat for job {}", job.get("id", "unknown"))
178
+ return True
179
+ logger.error("Failed to send alert to Google Chat: {}", response.error)
180
+ return False
181
+
182
+ def _build_alert_payload(
183
+ self,
184
+ error_category: str,
185
+ job: dict[str, Any],
186
+ error_info: dict[str, Any],
187
+ severity: str,
188
+ ) -> ErrorAlertPayload:
189
+ """Build structured alert payload for Google Chat."""
190
+ job_id = int(job.get("id", 0))
191
+ ticket_id = str(job.get("ticket_id", "unknown"))
192
+ client_name = str(job.get("client_name", "unknown"))
193
+
194
+ stage = error_info.get("stage", "unknown")
195
+ error_message = error_info.get("error_message", "Unknown error")
196
+ error_type = error_info.get("error_type", "UnknownError")
197
+
198
+ try:
199
+ category_enum = ErrorCategory(error_category)
200
+ except ValueError:
201
+ category_enum = ErrorCategory.SYSTEM_ERROR
202
+
203
+ try:
204
+ severity_enum = SeverityLevel(severity)
205
+ except ValueError:
206
+ severity_enum = SeverityLevel.MEDIUM
207
+
208
+ return ErrorAlertPayload(
209
+ timestamp=datetime.now(UTC),
210
+ error_category=category_enum,
211
+ severity=severity_enum,
212
+ job_id=job_id,
213
+ ticket_id=ticket_id,
214
+ client_name=client_name,
215
+ stage=stage,
216
+ error_message=f"{error_type}: {error_message}",
217
+ retry_count=job.get("retry_count", 0),
218
+ max_retries=job.get("max_retries", 0),
219
+ )
220
+
221
+ def _build_card_payload(self, payload: ErrorAlertPayload) -> dict[str, Any]:
222
+ """Build Cards v2 payload for Google Chat webhook (BCC pattern)."""
223
+ card_id = f"alert-{payload.job_id}-{payload.timestamp.strftime('%Y%m%d%H%M%S')}"
224
+
225
+ return {
226
+ "cardsV2": [
227
+ {
228
+ "cardId": card_id,
229
+ "card": {
230
+ "header": self._build_card_header(payload),
231
+ "sections": [
232
+ self._build_job_info_section(payload),
233
+ self._build_error_details_section(payload),
234
+ self._build_error_message_section(payload),
235
+ ],
236
+ },
237
+ }
238
+ ]
239
+ }
240
+
241
+ def _build_card_header(self, payload: ErrorAlertPayload) -> dict[str, Any]:
242
+ """Build the card header section."""
243
+ return {
244
+ "title": f"Bridge-TAP Alert: {self.CATEGORY_TITLE_MAP.get(payload.error_category, 'Error')}",
245
+ "subtitle": self.SERVICE_NAME,
246
+ "imageUrl": self.SEVERITY_ICON_URLS.get(payload.severity),
247
+ "imageType": "CIRCLE",
248
+ }
249
+
250
+ def _build_job_info_section(self, payload: ErrorAlertPayload) -> dict[str, Any]:
251
+ """Build the job information section with decorated text widgets."""
252
+ return {
253
+ "widgets": [
254
+ {
255
+ "decoratedText": {
256
+ "startIcon": {"knownIcon": "BOOKMARK"},
257
+ "topLabel": "Job ID",
258
+ "text": str(payload.job_id),
259
+ }
260
+ },
261
+ {
262
+ "decoratedText": {
263
+ "startIcon": {"knownIcon": "DESCRIPTION"},
264
+ "topLabel": "Ticket ID",
265
+ "text": payload.ticket_id,
266
+ }
267
+ },
268
+ {
269
+ "decoratedText": {
270
+ "startIcon": {"knownIcon": "PERSON"},
271
+ "topLabel": "Client",
272
+ "text": payload.client_name,
273
+ }
274
+ },
275
+ {
276
+ "decoratedText": {
277
+ "startIcon": {"knownIcon": "CLOCK"},
278
+ "topLabel": "Timestamp",
279
+ "text": payload.timestamp.strftime("%Y-%m-%d %H:%M:%S UTC"),
280
+ }
281
+ },
282
+ ]
283
+ }
284
+
285
+ def _build_error_details_section(self, payload: ErrorAlertPayload) -> dict[str, Any]:
286
+ """Build the error details section."""
287
+ return {
288
+ "header": "Error Details",
289
+ "widgets": [
290
+ {"decoratedText": {"topLabel": "Stage", "text": payload.stage}},
291
+ {"decoratedText": {"topLabel": "Category", "text": payload.error_category.value}},
292
+ {"decoratedText": {"topLabel": "Severity", "text": payload.severity.value}},
293
+ {"decoratedText": {"topLabel": "Retries", "text": f"{payload.retry_count}/{payload.max_retries}"}},
294
+ ],
295
+ }
296
+
297
+ def _build_error_message_section(self, payload: ErrorAlertPayload) -> dict[str, Any]:
298
+ """Build the error message section."""
299
+ return {
300
+ "header": "Error Message",
301
+ "widgets": [{"textParagraph": {"text": payload.error_message}}],
302
+ }
303
+ ```
304
+
305
+ ## Pydantic Models
306
+
307
+ ```python
308
+ from datetime import datetime
309
+ from enum import Enum
310
+ from typing import Any
311
+
312
+ from pydantic import BaseModel, ConfigDict
313
+
314
+
315
+ class ErrorCategory(str, Enum):
316
+ """Categories for error classification."""
317
+ CONFIG_ERROR = "config_error"
318
+ SYSTEM_ERROR = "system_error"
319
+ ETL_REQUIRED = "etl_required"
320
+ USER_FILE = "user_file"
321
+
322
+
323
+ class SeverityLevel(str, Enum):
324
+ """Severity levels for alerts."""
325
+ CRITICAL = "critical"
326
+ HIGH = "high"
327
+ MEDIUM = "medium"
328
+ LOW = "low"
329
+
330
+
331
+ class ErrorAlertPayload(BaseModel):
332
+ """Payload for error alerts."""
333
+ model_config = ConfigDict(frozen=True)
334
+
335
+ timestamp: datetime
336
+ error_category: ErrorCategory
337
+ severity: SeverityLevel
338
+ job_id: int
339
+ ticket_id: str
340
+ client_name: str
341
+ stage: str
342
+ error_message: str
343
+ retry_count: int = 0
344
+ max_retries: int = 0
345
+
346
+
347
+ class ChatSendMessageResponse(BaseModel):
348
+ """Response from sending a message."""
349
+ model_config = ConfigDict(frozen=True)
350
+
351
+ success: bool
352
+ error: str | None = None
353
+ response_data: dict[str, Any] | None = None
354
+
355
+
356
+ class GoogleChatClientConfig(BaseModel):
357
+ """Configuration for Google Chat client."""
358
+ model_config = ConfigDict(frozen=True)
359
+
360
+ webhook_url: str = ""
361
+ timeout: float = 30.0
362
+ allow_empty_webhook: bool = False
363
+ ```
364
+
365
+ ## Test Fixtures
366
+
367
+ ```python
368
+ from collections.abc import Generator
369
+ from datetime import UTC, datetime
370
+ from unittest.mock import MagicMock, patch
371
+
372
+ import pytest
373
+
374
+ from bridge_tap_api.sdk.google_chat.client import ErrorAlerter, GoogleChatClient
375
+ from bridge_tap_api.sdk.google_chat.models import (
376
+ ErrorCategory,
377
+ GoogleChatClientConfig,
378
+ SeverityLevel,
379
+ )
380
+
381
+
382
+ @pytest.fixture
383
+ def google_chat_client() -> Generator[GoogleChatClient, None, None]:
384
+ """Create a GoogleChatClient for testing."""
385
+ config = GoogleChatClientConfig(webhook_url="https://webhook.test")
386
+ client = GoogleChatClient(config=config)
387
+ yield client
388
+ client.close()
389
+
390
+
391
+ @pytest.fixture
392
+ def alerter() -> Generator[ErrorAlerter, None, None]:
393
+ """Create an ErrorAlerter for testing."""
394
+ alerter = ErrorAlerter(webhook_url="https://webhook.test")
395
+ yield alerter
396
+ alerter.close()
397
+
398
+
399
+ @pytest.fixture
400
+ def mock_successful_response() -> MagicMock:
401
+ """Create a mock successful HTTP response."""
402
+ mock_response = MagicMock()
403
+ mock_response.status_code = 200
404
+ mock_response.content = b'{"name": "spaces/xxx/messages/yyy"}'
405
+ mock_response.json.return_value = {"name": "spaces/xxx/messages/yyy"}
406
+ mock_response.raise_for_status = MagicMock()
407
+ return mock_response
408
+
409
+
410
+ @pytest.fixture
411
+ def sample_job() -> dict:
412
+ """Create a sample job dictionary for testing."""
413
+ return {
414
+ "id": 123,
415
+ "ticket_id": "TICKET-456",
416
+ "client_name": "test-client",
417
+ "retry_count": 2,
418
+ "max_retries": 6,
419
+ }
420
+
421
+
422
+ @pytest.fixture
423
+ def sample_error_info() -> dict:
424
+ """Create sample error info for testing."""
425
+ return {
426
+ "stage": "validation",
427
+ "error_type": "ValidationError",
428
+ "error_message": "Required column 'email' is missing",
429
+ }
430
+ ```
431
+
432
+ ## Example Test
433
+
434
+ ```python
435
+ def test_send_alert_builds_card_v2_payload(
436
+ alerter: ErrorAlerter,
437
+ mock_successful_response: MagicMock,
438
+ sample_job: dict,
439
+ sample_error_info: dict,
440
+ ) -> None:
441
+ """Test that send_alert builds proper Cards v2 structure."""
442
+ with patch.object(alerter._client.client, "post", return_value=mock_successful_response):
443
+ result = alerter.send_alert(
444
+ error_category="config_error",
445
+ job=sample_job,
446
+ error_info=sample_error_info,
447
+ severity="high",
448
+ )
449
+
450
+ assert result is True
451
+
452
+ # Verify cardsV2 structure
453
+ call_args = alerter._client.client.post.call_args
454
+ payload = call_args.kwargs["json"]
455
+
456
+ assert "cardsV2" in payload
457
+ card = payload["cardsV2"][0]["card"]
458
+
459
+ # Verify header
460
+ assert card["header"]["title"] == "Bridge-TAP Alert: Configuration Error"
461
+ assert card["header"]["subtitle"] == "bridge-tap-api"
462
+ assert "warning" in card["header"]["imageUrl"] # HIGH severity uses warning icon
463
+
464
+ # Verify sections
465
+ assert len(card["sections"]) == 3
466
+ ```