Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,17 @@ jobs:
# untouched `pip install ruff` below, which isn't flagged only because it
# isn't part of this PR's diff).
run: cd sdk/python && pip install -e ".[dev,connectors]" # NOSONAR python:S4830 python:S5445
- name: Install connector/pack deps
# google-auth is only needed by the Google Sheet connector, so it lives in
# that connector's requirements rather than the SDK's extras.
run: pip install -r connectors/google_sheet/requirements.txt # NOSONAR python:S4830 python:S5445
- name: Lint
run: cd sdk/python && pip install ruff && ruff check .
- name: Test
# Same ephemeral-CI-runner rationale as the installs above.
run: pip install ruff && ruff check sdk/python connectors packs # NOSONAR python:S4830 python:S5445
- name: Test SDK
run: cd sdk/python && pytest tests/ -v
- name: Test connectors & packs
run: pytest connectors packs -q

typescript:
name: TypeScript SDK Tests
Expand Down
17 changes: 16 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,21 @@ magic-claw/
│ ├── knowledge/ # Shared knowledge hub + semantic search
│ ├── webhook/ # Event-driven webhook delivery (at-least-once)
│ └── audit/ # Audit log
├── sdk/python/ # Python SDK
├── sdk/python/ # Python SDK (+ magic_ai_sdk/connectors/ = Connector Framework)
├── sdk/go/ # Go SDK
├── connectors/ # Platform integrations, run as external workers
│ ├── zalo/ # Zalo OA — send/receive messages
│ └── google_sheet/ # Google Sheets — orders & customers
├── packs/ # Vertical business workflows
│ └── ecomops/ # Customer service for VN online shops
└── examples/ # Example workers
```

**connectors vs packs:** a connector answers "which platform" (API calls +
Common Schema mapping, no business logic); a pack answers "which business
process" (workflow logic, no hard-coded platform). Both run as external Python
workers — neither lives in the Go core.

### Module Tiers

| Tier | Modules | Purpose |
Expand Down Expand Up @@ -112,6 +122,11 @@ cd core && MAGIC_POSTGRES_URL="postgres://user:pass@localhost/magic" ./magic ser
# Python SDK
cd sdk/python && pip install -e ".[dev]"
cd sdk/python && pytest

# Connectors + packs (run from repo root — root pytest.ini supplies asyncio_mode)
pip install -e 'sdk/python[dev,connectors]' -r connectors/google_sheet/requirements.txt
pytest connectors packs
ruff check sdk/python connectors packs
```

## Key Design Decisions
Expand Down
1 change: 0 additions & 1 deletion connectors/google_sheet/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
from google.auth.transport import Request as GoogleAuthRequestBase
from google.auth.transport import Response as GoogleAuthResponseBase
from google.oauth2 import service_account

from magic_ai_sdk.connectors.base import BaseConnector
from magic_ai_sdk.connectors.errors import AuthError, PermanentError, RateLimitError, TransientError, with_retry

Expand Down
2 changes: 1 addition & 1 deletion connectors/google_sheet/tests/test_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
import pytest
import respx
from google.auth.exceptions import RefreshError
from magic_ai_sdk.connectors.errors import AuthError, PermanentError, RateLimitError, TransientError

from google_sheet.connector import API_BASE, GoogleSheetConnector
from magic_ai_sdk.connectors.errors import AuthError, PermanentError, RateLimitError, TransientError

CONFIG = {"spreadsheet_id": "sheet-123", "service_account_info": {"type": "service_account"}}

Expand Down
1 change: 0 additions & 1 deletion connectors/zalo/connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from typing import Any, Callable, Mapping

import httpx

from magic_ai_sdk.connectors.base import BaseConnector
from magic_ai_sdk.connectors.errors import AuthError, PermanentError, RateLimitError, TransientError, with_retry

Expand Down
2 changes: 1 addition & 1 deletion connectors/zalo/tests/test_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
import httpx
import pytest
import respx

from magic_ai_sdk.connectors.errors import AuthError, PermanentError, RateLimitError, TransientError

from zalo.connector import REFRESH_TOKEN_URL, SEND_MESSAGE_URL, ZaloConnector

BASE_CONFIG = {
Expand Down
26 changes: 26 additions & 0 deletions packs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# MagiC Solution Packs

Pack = workflow nghiệp vụ cho một ngành cụ thể, xây trên Connector Framework và
core MagiC.

Khác biệt với `connectors/`:

- **Connector** trả lời câu hỏi *"kết nối tới nền tảng nào"* (Zalo, Google Sheet,
Nhanh.vn) — chỉ gọi API và map dữ liệu về Common Schema.
- **Pack** trả lời câu hỏi *"làm nghiệp vụ gì"* (trả lời khách, tra đơn, kiểm tra
guardrail) — chứa logic nghiệp vụ, và **không** được hard-code nền tảng nào.

```
packs/
└── ecomops/ # Chăm sóc khách hàng cho shop bán hàng online
```

## Nguyên tắc

1. Pack nhận mọi phụ thuộc bên ngoài qua **dependency injection** (classifier,
tra đơn, model soạn tin), không tự gọi connector cụ thể — nhờ vậy cùng một
workflow chạy được trên Zalo, Facebook, hay test harness.
2. Dữ liệu ra/vào dùng **Common Data Schema** (`sdk/python/magic_ai_sdk/connectors/schema.py`).
3. Mọi câu trả lời gửi tới khách phải đi qua **guardrail** trước.
4. Gọi LLM qua **LLM gateway của MagiC** (`POST /api/v1/llm/chat`) thay vì SDK của
nhà cung cấp, để chi phí token được Cost Controller ghi nhận cùng mọi workload khác.
8 changes: 8 additions & 0 deletions packs/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
"""Makes each pack importable (e.g. `import ecomops`) during tests without a
full pip install — mirrors connectors/conftest.py.
"""

import sys
from pathlib import Path

sys.path.insert(0, str(Path(__file__).parent))
124 changes: 124 additions & 0 deletions packs/ecomops/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# EcomOps Pack

Workflow chăm sóc khách hàng cho shop bán hàng online Việt Nam:

```
Nhận tin → Phân loại ý định → Trích mã đơn/SĐT → Tra đơn hàng
→ Soạn câu trả lời → Kiểm tra Guardrail → Gửi hoặc chuyển nhân viên
```

Pack này chạy như **worker độc lập**, không nằm trong core Go. Mọi phụ thuộc bên
ngoài (classifier, tra đơn, model soạn tin) đều được **inject vào**, nên workflow
tự nó không gọi mạng và không biết gì về Zalo hay Google Sheet — cùng một workflow
chạy được trên bất kỳ kênh nào.

## 1. Cài đặt

Chạy từ thư mục gốc repo (`magic/`):

```bash
pip install -e 'sdk/python[connectors]'
pip install -r packs/ecomops/requirements.txt
pytest packs/ecomops/tests
```

## 2. Các thành phần

| Module | Vai trò |
|--------|---------|
| `text.py` | Chuẩn hoá tiếng Việt (bỏ dấu) để so khớp không phụ thuộc cách gõ |
| `intents.py` | Phân loại ý định: `RuleBasedIntentClassifier`, `LLMIntentClassifier`, `HybridIntentClassifier` |
| `extraction.py` | Trích mã đơn hàng & số điện thoại từ tin nhắn |
| `prompts.py` | Thư viện prompt tiếng Việt theo từng ý định |
| `guardrails.py` | Chặn câu trả lời rủi ro trước khi gửi cho khách |
| `workflow.py` | `EcomOpsWorkflow` — pipeline nối tất cả lại |

### Ý định được hỗ trợ

`order_status` (tình trạng đơn) · `shipping_fee` (phí ship) · `return_exchange`
(đổi trả) · `complaint` (khiếu nại) · `product_info` (thông tin sản phẩm) ·
`greeting` · `other`

Classifier **không phụ thuộc dấu tiếng Việt** — "đơn hàng của em đâu rồi" và
"don hang cua em dau roi" cho cùng kết quả, vì khách hay gõ không dấu. Nó cũng
cho phép chèn 1 từ đệm giữa các từ khoá, nên "khi nào **em** nhận được hàng" vẫn
khớp từ khoá "khi nao nhan".

`HybridIntentClassifier` chạy rule trước, **chỉ gọi LLM khi rule không chắc** —
tiết kiệm chi phí vì phần lớn tin nhắn của khách rất ngắn và lặp lại.

### Guardrail (quan trọng nhất)

Câu trả lời do LLM soạn **không mặc nhiên an toàn** để gửi từ tài khoản chính
thức của shop. Các rule mặc định chặn:

| Rule | Chặn điều gì |
|------|--------------|
| `empty_draft` | Câu trả lời rỗng |
| `leaked_contact` | SĐT/email không phải của khách hoặc của shop (lộ thông tin khách khác) |
| `delivery_promise` | Hứa chắc chắn ngày giao ("chắc chắn giao ngày mai") |
| `compensation_promise` | Tự hứa hoàn tiền/voucher/giảm giá khi chưa được duyệt |
| `internal_leak` | Lộ system prompt, API key, ghi chú nội bộ |

Guardrail cũng chạy trên text đã bỏ dấu, nên **không thể lách bằng cách gõ không dấu**.

Bất kỳ vi phạm mức `BLOCK` nào → workflow **không gửi draft đó**, mà trả về câu
trả lời an toàn và đánh dấu `action="handoff"` để nhân viên tiếp nhận.

## 3. Dùng nhanh

```python
import asyncio
from ecomops import EcomOpsWorkflow, RuleBasedIntentClassifier

async def my_order_lookup(order_code, phone):
return {"id": order_code, "status": "shipping", "total_amount": 250000}

async def my_drafter(messages):
... # gọi LLM, hoặc dùng ecomops.workflow.LLMDrafter

async def main():
wf = EcomOpsWorkflow(
classifier=RuleBasedIntentClassifier(),
drafter=my_drafter,
order_lookup=my_order_lookup,
)
result = await wf.handle("đơn #DH12345 của em tới đâu rồi ạ")
print(result.action, result.intent, result.reply)

asyncio.run(main())
```

`WorkflowResult`:

- `action`: `"send"` (bot xử lý xong) hoặc `"handoff"` (cần nhân viên tiếp nhận)
- `reply`: **luôn an toàn để gửi** — nếu draft bị guardrail chặn, đây là câu trả lời dự phòng
- `intent`, `confidence`, `extracted`, `order`, `violations`, `handoff_reason`, `trace`

Khi nào chuyển nhân viên: khiếu nại (`complaint`), độ tin cậy thấp, guardrail chặn,
hoặc soạn tin lỗi.

## 4. Demo end-to-end (Zalo + Google Sheet)

`example_worker.py` nối: webhook Zalo → tra đơn trong Google Sheet → trả lời có
guardrail → gửi lại qua Zalo.

```bash
# cần connectors/zalo/config.yaml và connectors/google_sheet/config.yaml
export MAGIC_URL=http://localhost:8080
export MAGIC_API_KEY=your-key
cd packs && python -m ecomops.example_worker
```

Sheet cần tab `Orders` với dòng header khớp Common Schema
(`id`, `customer_id`, `status`, `total_amount`, `shipping_fee`, `created_at`) —
xem `connectors/google_sheet/README.md`.

## 5. Giới hạn hiện tại

- Từ khoá phân loại là **danh sách cố định trong code**, chưa cấu hình được qua file.
- Chưa có Case Management (tạo case khi khiếu nại/đổi trả) — thuộc Tuần 3–4 của roadmap.
- `example_worker.py` quét toàn bộ dải `Orders!A1:H500` mỗi lần tra đơn; với sheet
lớn nên thay bằng Nhanh.vn connector hoặc thêm cache.
- Guardrail dựa trên cụm từ, không phải mô hình — bắt được các mẫu hứa hẹn phổ biến,
nhưng không thay thế được người duyệt với các đơn giá trị cao.
31 changes: 31 additions & 0 deletions packs/ecomops/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""EcomOps Pack — customer-service workflow for Vietnamese online shops.

Open-source reference implementation of the EcomOps workflow: classify an
inbound message, look the order up, draft a reply, and guardrail it before it
reaches the customer.
"""

from .guardrails import GuardrailContext, GuardrailEngine, GuardrailViolation, Severity
from .intents import (
HybridIntentClassifier,
Intent,
IntentResult,
LLMIntentClassifier,
RuleBasedIntentClassifier,
)
from .workflow import EcomOpsWorkflow, LLMDrafter, WorkflowResult

__all__ = [
"Intent",
"IntentResult",
"RuleBasedIntentClassifier",
"LLMIntentClassifier",
"HybridIntentClassifier",
"GuardrailEngine",
"GuardrailContext",
"GuardrailViolation",
"Severity",
"EcomOpsWorkflow",
"LLMDrafter",
"WorkflowResult",
]
Loading
Loading