-
Notifications
You must be signed in to change notification settings - Fork 0
feat(ecomops): knowledge base — shop policies for draft replies #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,189 @@ | ||
| """Knowledge lookup for EcomOps drafts. | ||
|
|
||
| The system prompt tells the model to answer *only* from the "Dữ liệu" block, so | ||
| without a knowledge source the shipping-fee and return-policy intents have | ||
| nothing to answer from — the bot can only say it will check and get back. This | ||
| module supplies that block. | ||
|
|
||
| Two backends: | ||
|
|
||
| - `MagicKnowledgeLookup` — reads MagiC's Knowledge Hub (`GET /api/v1/knowledge`), | ||
| so policies live in one place shared with every other MagiC workload. | ||
| - `StaticKnowledgeLookup` — a YAML file, for shops running the pack without a | ||
| Knowledge Hub (in-memory core, no Postgres). | ||
|
|
||
| Both are plain async callables matching the workflow's `knowledge_lookup` | ||
| signature, so they're interchangeable. | ||
| """ | ||
|
|
||
| import logging | ||
| from pathlib import Path | ||
| from typing import Any | ||
|
|
||
| import httpx | ||
|
|
||
| from .intents import Intent | ||
|
|
||
| logger = logging.getLogger("magic_packs.ecomops.knowledge") | ||
|
|
||
| # Tag every EcomOps entry carries, so the pack's lookups don't drag in unrelated | ||
| # org knowledge that happens to share a keyword. | ||
| ECOMOPS_TAG = "ecomops" | ||
|
|
||
| # Per-intent tag + the keyword query sent to the Hub. The Hub's `q` search is | ||
| # keyword-based over title/content, so the query is deliberately in the same | ||
| # Vietnamese wording the sample entries use. | ||
| INTENT_TOPICS: dict[Intent, tuple[str, str]] = { | ||
| Intent.SHIPPING_FEE: ("shipping", "phí ship vận chuyển"), | ||
| Intent.RETURN_EXCHANGE: ("return", "đổi trả hoàn tiền bảo hành"), | ||
| Intent.ORDER_STATUS: ("order", "trạng thái đơn hàng giao hàng"), | ||
| Intent.PRODUCT_INFO: ("product", "sản phẩm size màu chất liệu"), | ||
| Intent.COMPLAINT: ("complaint", "khiếu nại xử lý sự cố"), | ||
| } | ||
|
|
||
|
|
||
| def topic_for(intent: Intent) -> str | None: | ||
| """The knowledge tag an intent needs, or None if it needs no policy data.""" | ||
| topic = INTENT_TOPICS.get(intent) | ||
| return topic[0] if topic else None | ||
|
|
||
|
|
||
| class MagicKnowledgeLookup: | ||
| """Pulls shop policy snippets from MagiC's Knowledge Hub.""" | ||
|
|
||
| def __init__(self, magic_url: str, api_key: str = "", top_k: int = 3, timeout: float = 10.0): | ||
| self.magic_url = magic_url.rstrip("/") | ||
| self.api_key = api_key | ||
| self.top_k = top_k | ||
| self.timeout = timeout | ||
|
|
||
| async def __call__(self, intent: Intent) -> list[str]: | ||
| topic = INTENT_TOPICS.get(intent) | ||
| if topic is None: | ||
| return [] | ||
| tag, query = topic | ||
|
|
||
| headers = {"Authorization": f"Bearer {self.api_key}"} if self.api_key else {} | ||
| try: | ||
| async with httpx.AsyncClient(timeout=self.timeout) as client: | ||
| resp = await client.get( | ||
| f"{self.magic_url}/api/v1/knowledge", headers=headers, params={"q": query} | ||
| ) | ||
| resp.raise_for_status() | ||
| entries = resp.json() | ||
| except (httpx.HTTPError, ValueError) as e: | ||
| # Missing policy data degrades the answer; it shouldn't drop the | ||
| # message. The workflow still drafts, and the guardrails still stop | ||
| # anything the model invents to fill the gap. | ||
| logger.warning("knowledge lookup failed for %s: %s", intent.value, e) | ||
| return [] | ||
|
|
||
| if not isinstance(entries, list): | ||
| return [] | ||
|
|
||
| # The Hub has no server-side tag filter, so narrow here: an entry must | ||
| # carry both the ecomops tag and this intent's topic tag. | ||
| snippets = [] | ||
| for entry in entries: | ||
| if not isinstance(entry, dict): | ||
| continue | ||
| tags = entry.get("tags") or [] | ||
| if ECOMOPS_TAG in tags and tag in tags: | ||
| content = (entry.get("content") or "").strip() | ||
| if content: | ||
| snippets.append(content) | ||
| return snippets[: self.top_k] | ||
|
|
||
|
|
||
| class StaticKnowledgeLookup: | ||
| """Serves policy snippets from a local mapping — no Knowledge Hub needed.""" | ||
|
|
||
| def __init__(self, entries: dict[str, list[str]] | None = None): | ||
| self.entries = entries or {} | ||
|
|
||
| async def __call__(self, intent: Intent) -> list[str]: | ||
| tag = topic_for(intent) | ||
| return list(self.entries.get(tag, [])) if tag else [] | ||
|
|
||
| @classmethod | ||
| def from_yaml(cls, path: str | Path) -> "StaticKnowledgeLookup": | ||
| """Load from a YAML mapping of topic -> list of snippets. | ||
|
|
||
| Imports yaml lazily so the rest of the pack stays dependency-free for | ||
| callers that build their knowledge mapping in code. | ||
| """ | ||
| import yaml | ||
|
|
||
| data = yaml.safe_load(_resolve_yaml_path(path).read_text(encoding="utf-8")) or {} | ||
| return cls({str(k): [str(s) for s in (v or [])] for k, v in data.items()}) | ||
|
|
||
|
|
||
| def sample_knowledge_path() -> Path: | ||
| return Path(__file__).parent / "sample_knowledge.yaml" | ||
|
|
||
|
|
||
| _ALLOWED_SUFFIXES = {".yaml", ".yml"} | ||
|
|
||
|
|
||
| def _resolve_yaml_path(path: str | Path) -> Path: | ||
| """Validate a caller-supplied knowledge path before touching the filesystem. | ||
|
|
||
| `seed_knowledge.py` takes this straight from argv and everything it reads is | ||
| POSTed into the Knowledge Hub, where it becomes queryable. An unvalidated | ||
| path therefore turns "seed the knowledge base" into "copy any file this | ||
| process can read into a searchable store" — so constrain it to an existing | ||
| regular YAML file. | ||
|
|
||
| `resolve()` runs first on purpose: it collapses `..` and follows symlinks, so | ||
| a `policy.yaml` symlinked at `/etc/shadow` is checked by its real name and | ||
| fails the suffix test rather than sailing through it. | ||
| """ | ||
| resolved = Path(path).expanduser().resolve() | ||
| if resolved.suffix.lower() not in _ALLOWED_SUFFIXES: | ||
| raise ValueError( | ||
| f"knowledge file must be .yaml or .yml, got {resolved.suffix or '(no suffix)'}: {resolved}" | ||
| ) | ||
| if not resolved.is_file(): | ||
| raise FileNotFoundError(f"knowledge file not found: {resolved}") | ||
| return resolved | ||
|
|
||
|
|
||
| def knowledge_entries_for_seeding(path: str | Path | None = None) -> list[dict[str, Any]]: | ||
| """Turn the sample YAML into `POST /api/v1/knowledge` payloads. | ||
|
|
||
| Each entry is tagged `ecomops` plus its topic so `MagicKnowledgeLookup` can | ||
| find it again. | ||
| """ | ||
| import yaml | ||
|
|
||
| src = _resolve_yaml_path(path) if path else sample_knowledge_path() | ||
| data = yaml.safe_load(src.read_text(encoding="utf-8")) or {} | ||
|
|
||
| payloads = [] | ||
| for topic, snippets in data.items(): | ||
| for i, snippet in enumerate(snippets or [], start=1): | ||
| payloads.append( | ||
| { | ||
| "title": f"EcomOps — {topic} #{i}", | ||
| "content": str(snippet).strip(), | ||
| "tags": [ECOMOPS_TAG, str(topic)], | ||
| "scope": "org", | ||
| } | ||
| ) | ||
| return payloads | ||
|
|
||
|
|
||
| async def seed_knowledge_hub( | ||
| magic_url: str, api_key: str = "", path: str | Path | None = None, timeout: float = 15.0 | ||
|
Check warning on line 177 in packs/ecomops/knowledge.py
|
||
| ) -> int: | ||
| """Push the sample knowledge base into MagiC's Knowledge Hub. Returns count.""" | ||
| payloads = knowledge_entries_for_seeding(path) | ||
| headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} | ||
|
|
||
| async with httpx.AsyncClient(timeout=timeout) as client: | ||
| for payload in payloads: | ||
| resp = await client.post( | ||
| f"{magic_url.rstrip('/')}/api/v1/knowledge", headers=headers, json=payload | ||
| ) | ||
| resp.raise_for_status() | ||
| return len(payloads) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,2 +1,5 @@ | ||
| # Install the framework first: pip install -e ../../sdk/python[connectors] | ||
| httpx>=0.27 | ||
| # Reading sample_knowledge.yaml / a shop's own policy YAML, and the connector | ||
| # configs in example_worker.py. | ||
| pyyaml>=6.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| # Knowledge Base mẫu cho EcomOps. | ||
| # | ||
| # Đây là DỮ LIỆU MẪU để chạy thử — thay bằng chính sách thật của shop trước khi | ||
| # dùng cho khách. Model chỉ được phép trả lời dựa trên nội dung ở đây (xem | ||
| # SYSTEM_PROMPT trong prompts.py), nên nội dung sai ở đây sẽ thành câu trả lời sai. | ||
| # | ||
| # Mỗi khoá là một "topic" khớp với intent (xem INTENT_TOPICS trong knowledge.py): | ||
| # shipping | return | order | product | complaint | ||
| # | ||
| # Nạp vào MagiC Knowledge Hub: | ||
| # python -m ecomops.seed_knowledge | ||
|
|
||
| shipping: | ||
| - >- | ||
| Phí ship nội thành Hà Nội và TP.HCM là 25.000đ, giao trong 1-2 ngày làm việc. | ||
| - >- | ||
| Phí ship các tỉnh khác là 35.000đ, thời gian giao dự kiến 3-5 ngày làm việc, | ||
| vùng sâu vùng xa có thể lâu hơn. | ||
| - >- | ||
| Miễn phí vận chuyển cho đơn hàng từ 500.000đ trở lên, áp dụng toàn quốc. | ||
|
|
||
| return: | ||
| - >- | ||
| Shop hỗ trợ đổi trả trong vòng 7 ngày kể từ ngày khách nhận hàng, với điều kiện | ||
| sản phẩm còn nguyên tem mác, chưa qua sử dụng và còn đủ hộp/phụ kiện. | ||
| - >- | ||
| Trường hợp shop giao sai mẫu, sai size hoặc sản phẩm bị lỗi từ nhà sản xuất, | ||
| shop chịu toàn bộ phí đổi trả và phí vận chuyển hai chiều. | ||
| - >- | ||
| Trường hợp khách đổi vì lý do cá nhân (không thích, đổi ý), khách chịu phí | ||
| vận chuyển chiều đổi hàng. | ||
| - >- | ||
| Sản phẩm khuyến mãi trên 50% và đồ lót không áp dụng chính sách đổi trả, | ||
| trừ khi bị lỗi từ nhà sản xuất. | ||
|
|
||
| order: | ||
| - >- | ||
| Các trạng thái đơn hàng: pending (chờ xác nhận), confirmed (đã xác nhận), | ||
| shipping (đang giao), delivered (đã giao), cancelled (đã huỷ), | ||
| returned (đã hoàn trả). | ||
| - >- | ||
| Đơn hàng được xác nhận trong vòng 24 giờ kể từ khi đặt. Sau khi chuyển sang | ||
| trạng thái shipping, khách có thể tra cứu vị trí đơn qua mã vận đơn. | ||
| - >- | ||
| Khách có thể huỷ đơn miễn phí khi đơn còn ở trạng thái pending hoặc confirmed. | ||
| Đơn đã chuyển sang shipping không huỷ được, khách có thể từ chối nhận hàng. | ||
|
|
||
| product: | ||
| - >- | ||
| Thông tin size, màu và chất liệu của từng sản phẩm được ghi trong phần mô tả | ||
| trên trang sản phẩm. Nếu khách phân vân giữa hai size, shop khuyên chọn size | ||
| lớn hơn. | ||
| - >- | ||
| Tình trạng còn hàng thay đổi liên tục trong ngày. Shop cần kiểm tra kho trước | ||
| khi xác nhận cho khách. | ||
|
|
||
| complaint: | ||
| - >- | ||
| Mọi khiếu nại đều được chuyển cho nhân viên phụ trách xử lý trong vòng | ||
| 24 giờ làm việc. Nhân viên sẽ liên hệ lại với khách qua số điện thoại đặt hàng. | ||
| - >- | ||
| Phương án đền bù (hoàn tiền, đổi hàng, tặng voucher) chỉ do nhân viên quyết định | ||
| sau khi kiểm tra đơn — không cam kết trước với khách. |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.