Skip to content

feat(packs): EcomOps workflow + run connector/pack tests in CI#19

Merged
kienbui1995 merged 2 commits into
mainfrom
claude/magic-work-continue-mi1sf9
Jul 25, 2026
Merged

feat(packs): EcomOps workflow + run connector/pack tests in CI#19
kienbui1995 merged 2 commits into
mainfrom
claude/magic-work-continue-mi1sf9

Conversation

@kienbui1995

Copy link
Copy Markdown
Owner

What does this PR do?

Tuần 2 of the Month-1 roadmap (MAGIC_MONTH1_TASK_LIST.md): the EcomOps customer-service pipeline for Vietnamese online shops, plus a CI gap fix.

Nhận tin → Classify Intent → Extract Order Info → Fetch Order Status
        → Draft Response → Guardrail Check → Send hoặc Handoff

packs/ecomops/ (new)

  • text.py — Vietnamese normalization. Customers routinely type without diacritics, so both the message and the keyword lists fold to a diacritics-free form; "đơn hàng" and "don hang" hit the same rule. đ/Đ are mapped explicitly because they don't decompose under NFD.
  • intents.py — 7 EcomOps intents. RuleBasedIntentClassifier matches keyword tokens in order allowing one filler word between them, because Vietnamese slots pronouns inside otherwise-fixed phrases ("khi nào **em** nhận được hàng" must still match the keyword "khi nao nhan"). A greeting only wins when it's the entire message, so "chào shop, đơn em đâu" classifies as order_status, not greeting. LLMIntentClassifier goes through MagiC's own /api/v1/llm/chat gateway (cheapest routing) rather than a provider SDK, so token spend lands in the Cost Controller. HybridIntentClassifier only pays for the LLM when the rules aren't confident, and keeps the rule guess if the gateway is down.
  • extraction.py — order code + phone. Phones are masked out before the bare-numeric order-code pattern runs; otherwise "sdt 0901234567" returns the phone as the order id and every lookup silently misses.
  • guardrails.py — an LLM draft is not automatically safe to send from a shop's official account. Blocks leaked third-party contacts, hard delivery guarantees, unapproved refunds/vouchers, and system-prompt/credential leaks. Runs on normalized text, so it can't be evaded by dropping diacritics.
  • prompts.py — Vietnamese prompt library, kept as data so shops can retune tone without touching pipeline code.
  • workflow.py — the pipeline. Every dependency is injected, so it makes no network calls and knows nothing about Zalo or Sheets. A guardrail-blocked draft is never returned to the caller: reply falls back to a safe message and action becomes "handoff". Complaints skip drafting entirely.
  • example_worker.py — end-to-end demo (Zalo webhook → Sheet lookup → guarded reply), which is the stated Tuần 2 deliverable: "AI trả lời được câu hỏi trạng thái đơn hàng qua Zalo, dùng dữ liệu từ Google Sheet".

CI gap (worth flagging)

The python job only ever ran sdk/python. The 44 connector tests added in #17/#18 have never actually executed on CI, and neither would these 76. Fixing that surfaced two more things:

  • pytest connectors packs from the repo root failed outright — asyncio_mode lived only in per-directory pytest.ini files. Added a root pytest.ini; each connector/pack keeps its own for standalone runs.
  • ruff was weaker outside sdk/python: E501/I aren't in ruff's default rule set, so those directories were effectively unlinted for line length and import order. Added a root ruff.toml mirroring sdk/python's settings, and fixed the 4 import-order issues it surfaced.

CLAUDE.md updated so the documented structure and build commands match reality (connectors/, packs/, and how to run their tests).

Related issue

Tracks Month 1, Tuần 2 (EcomOps Workflow cơ bản) of the technical roadmap in magic-business/technical-planning.

Checklist

  • Tests pass — 76 new (packs) + 44 (connectors) + 36 (sdk) = 156, all passing; cd core && go build ./... clean (Go core untouched)
  • New code has tests — packs/ecomops/tests/ covers intents (incl. diacritic-free spellings and LLM-down fallback), extraction (incl. the phone/order-code collision), every guardrail rule, and each workflow handoff path
  • Documentation updated — packs/README.md, packs/ecomops/README.md (Vietnamese), CLAUDE.md

Generated by Claude Code

Tuần 2 of the Month-1 roadmap: the customer-service pipeline for Vietnamese
online shops.

    Nhận tin -> Classify Intent -> Extract Order Info -> Fetch Order Status
             -> Draft Response -> Guardrail Check -> Send hoặc Handoff

packs/ecomops/ (new):
- text.py — Vietnamese normalization. Customers routinely type without
  diacritics, so both the message and the keyword lists are folded to a
  diacritics-free form; "đơn hàng" and "don hang" match the same rule. đ/Đ
  are handled explicitly since they don't decompose under NFD.
- intents.py — 7 EcomOps intents. RuleBasedIntentClassifier matches keyword
  tokens in order allowing one filler word between them, because Vietnamese
  slots pronouns inside fixed phrases ("khi nào EM nhận được hàng" must still
  match "khi nao nhan"). A greeting only wins when it's the whole message, so
  "chào shop, đơn em đâu" classifies as order_status. LLMIntentClassifier goes
  through MagiC's own /api/v1/llm/chat gateway (cheapest routing) rather than a
  provider SDK, so spend lands in the Cost Controller. HybridIntentClassifier
  only pays for the LLM when the rules aren't confident, and keeps the rule
  guess if the gateway is down.
- extraction.py — order code + phone. Phones are masked out before the bare
  numeric order-code pattern runs, otherwise "sdt 0901234567" returns the phone
  as the order id and every lookup silently misses.
- guardrails.py — an LLM draft is not automatically safe to send from a shop's
  official account. Blocks leaked third-party contacts, hard delivery
  guarantees, unapproved refunds/vouchers, and system-prompt/credential leaks.
  Runs on normalized text so it can't be evaded by dropping diacritics.
- prompts.py — Vietnamese prompt library, kept as data so shops can retune tone
  without touching pipeline code.
- workflow.py — the pipeline. Every dependency is injected, so it makes no
  network calls and knows nothing about Zalo or Sheets. A blocked draft is
  never returned to the caller: reply falls back to a safe message and action
  becomes "handoff". Complaints skip drafting entirely.
- example_worker.py — end-to-end demo (Zalo webhook -> Sheet lookup -> guarded
  reply), which is the stated Tuần 2 deliverable.

CI (this is the part that was quietly broken): the python job only ever ran
sdk/python. The 44 connector tests added in #17/#18 have never executed on CI,
and neither would these 76. Running `pytest connectors packs` from the root
failed outright because asyncio_mode lived only in per-directory pytest.ini
files — hence the new root pytest.ini. ruff was also weaker outside sdk/python
(E501/I aren't in ruff's default rule set), so a root ruff.toml now mirrors
sdk/python's settings; the 4 import-order issues that surfaced are fixed.

Tests: 76 new (packs) + 44 (connectors) + 36 (sdk) = 156, all passing. Go core
untouched, builds clean.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@kienbui1995, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7b7eaaf5-1044-4ab6-85c6-24873ae3ba46

📥 Commits

Reviewing files that changed from the base of the PR and between f4d5b16 and e655c7c.

📒 Files selected for processing (26)
  • .github/workflows/ci.yml
  • CLAUDE.md
  • connectors/google_sheet/connector.py
  • connectors/google_sheet/tests/test_connector.py
  • connectors/zalo/connector.py
  • connectors/zalo/tests/test_connector.py
  • packs/README.md
  • packs/conftest.py
  • packs/ecomops/README.md
  • packs/ecomops/__init__.py
  • packs/ecomops/example_worker.py
  • packs/ecomops/extraction.py
  • packs/ecomops/guardrails.py
  • packs/ecomops/intents.py
  • packs/ecomops/prompts.py
  • packs/ecomops/pytest.ini
  • packs/ecomops/requirements.txt
  • packs/ecomops/tests/__init__.py
  • packs/ecomops/tests/test_extraction.py
  • packs/ecomops/tests/test_guardrails.py
  • packs/ecomops/tests/test_intents.py
  • packs/ecomops/tests/test_workflow.py
  • packs/ecomops/text.py
  • packs/ecomops/workflow.py
  • pytest.ini
  • ruff.toml
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/magic-work-continue-mi1sf9

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

…rail

Found while PR #19's CI was running: a shop that configures its hotline as
"+84987654321" (or "84987654321") had its OWN number flagged as a leaked
third-party contact, so any reply quoting the hotline was blocked and pushed to
a human. The rule compared raw digit strings, so the +84 and 0-prefixed
spellings of the same number never matched.

Adds `canonical_phone()` — folds "+84 987-654-321", "84987654321",
"987654321" and "0987-654-321" all to "0987654321" — and compares canonical
forms on both sides. Detection is unaffected: a genuinely foreign number is
still blocked (covered by a new test that allowlists the hotline and checks a
different number is still caught).

Also NOSONAR the `pip install ruff` lint line. Editing that line to widen the
lint scope made SonarCloud treat it as new code and re-apply the same two
supply-chain hotspots (S4830/S5445) already suppressed on the install lines
above it — that's what dropped this PR's Security Rating to C.

Tests: 14 new (canonical_phone table + hotline spellings + allowlist scoping),
170 total, all passing.
@sonarqubecloud

Copy link
Copy Markdown

@kienbui1995
kienbui1995 merged commit 7d3034a into main Jul 25, 2026
16 of 18 checks passed
kienbui1995 pushed a commit that referenced this pull request Jul 25, 2026
Closes a gap in what #19 shipped: the workflow accepted a `knowledge_lookup`
but nothing implemented it, and the system prompt forbids answering from
anything outside the "Dữ liệu" block. So shipping_fee and return_exchange —
two of the seven intents — could never actually be answered; the bot could only
promise to check. That's the safe failure mode, but it isn't a useful one.

- knowledge.py — two interchangeable async lookups:
  * MagicKnowledgeLookup reads MagiC's Knowledge Hub (GET /api/v1/knowledge),
    so policies live alongside every other MagiC workload.
  * StaticKnowledgeLookup reads a YAML file, for shops running the pack against
    an in-memory core with no Hub.
  Entries carry two tags (`ecomops` + topic), and lookups require both. The Hub
  has no server-side tag filter, so without that a shipping question would get
  answered with the return policy whenever the keyword search overlapped — or
  worse, with unrelated org knowledge that merely shared a word.
  A Hub outage degrades to "no policy data" rather than dropping the message;
  the guardrails still catch anything the model invents to fill the gap.

- sample_knowledge.yaml + seed_knowledge.py — Vietnamese starter KB (shipping
  fees, 7-day return policy, order statuses, product/complaint handling) and a
  seeder. Both the file and the seeder log loudly that this is sample data to be
  replaced with the shop's real policy, since wrong content here becomes a wrong
  answer to a customer.

- CI: the install step now picks up every requirements.txt under connectors/ and
  packs/ instead of naming one file. Adding pyyaml for this feature would
  otherwise have gone missing on CI (it passed locally only because my venv
  happened to have it), and a future connector's deps would hit the same trap.

Intentionally NOT included: the Nhanh.vn connector, which is the other Tuần 3-4
item. BUSINESS_MODEL.md and OPEN_CORE_STRATEGY.md both list the production
Nhanh.vn connector as Commercial ("điểm khác biệt lớn"), and this is the public
repo — that needs a deliberate decision, not a drive-by commit.

Tests: 21 new (topic mapping, tag filtering, top_k, no-topic short-circuit,
Hub-down degradation, malformed payloads, static YAML, seeding round-trip,
plus a check that every topic in the sample YAML is reachable from some intent
so the file can't drift into dead data). 191 total, all passing.
kienbui1995 added a commit that referenced this pull request Jul 25, 2026
* feat(ecomops): knowledge base — shop policies for draft replies

Closes a gap in what #19 shipped: the workflow accepted a `knowledge_lookup`
but nothing implemented it, and the system prompt forbids answering from
anything outside the "Dữ liệu" block. So shipping_fee and return_exchange —
two of the seven intents — could never actually be answered; the bot could only
promise to check. That's the safe failure mode, but it isn't a useful one.

- knowledge.py — two interchangeable async lookups:
  * MagicKnowledgeLookup reads MagiC's Knowledge Hub (GET /api/v1/knowledge),
    so policies live alongside every other MagiC workload.
  * StaticKnowledgeLookup reads a YAML file, for shops running the pack against
    an in-memory core with no Hub.
  Entries carry two tags (`ecomops` + topic), and lookups require both. The Hub
  has no server-side tag filter, so without that a shipping question would get
  answered with the return policy whenever the keyword search overlapped — or
  worse, with unrelated org knowledge that merely shared a word.
  A Hub outage degrades to "no policy data" rather than dropping the message;
  the guardrails still catch anything the model invents to fill the gap.

- sample_knowledge.yaml + seed_knowledge.py — Vietnamese starter KB (shipping
  fees, 7-day return policy, order statuses, product/complaint handling) and a
  seeder. Both the file and the seeder log loudly that this is sample data to be
  replaced with the shop's real policy, since wrong content here becomes a wrong
  answer to a customer.

- CI: the install step now picks up every requirements.txt under connectors/ and
  packs/ instead of naming one file. Adding pyyaml for this feature would
  otherwise have gone missing on CI (it passed locally only because my venv
  happened to have it), and a future connector's deps would hit the same trap.

Intentionally NOT included: the Nhanh.vn connector, which is the other Tuần 3-4
item. BUSINESS_MODEL.md and OPEN_CORE_STRATEGY.md both list the production
Nhanh.vn connector as Commercial ("điểm khác biệt lớn"), and this is the public
repo — that needs a deliberate decision, not a drive-by commit.

Tests: 21 new (topic mapping, tag filtering, top_k, no-topic short-circuit,
Hub-down degradation, malformed payloads, static YAML, seeding round-trip,
plus a check that every topic in the sample YAML is reachable from some intent
so the file can't drift into dead data). 191 total, all passing.

* fix(ci): put NOSONAR on the pip line, not the block-scalar key

My own mistake from the previous commit. Switching the install step to a
`run: |` block moved the pip invocation onto a continuation line, but the
NOSONAR marker stayed up on the `run: |` key — so it suppressed nothing and
SonarCloud re-applied the two supply-chain hotspots (S4830/S5445), dropping the
Security Rating to C.

Back to a single-line `run:`, matching the neighbouring install/lint steps where
this pattern is already proven: the marker is a YAML comment on the same line as
the command, stripped before the shell ever sees it.

Behaviour is unchanged — Python Tests passed on the previous commit, confirming
the find/xargs install and the new pyyaml dependency both work.

* fix(security): validate knowledge file paths before reading them

SonarCloud flagged a high-severity path-injection vulnerability in the code I
added in this PR — and it was right, so this fixes it rather than suppressing it.

`seed_knowledge.py` passes sys.argv[1] straight into
`knowledge_entries_for_seeding()`, which did `Path(path).read_text()` with no
validation, and everything it reads is POSTed into the Knowledge Hub. That turns
"seed the knowledge base" into "copy any file this process can read into a
searchable store" — an arbitrary-file read whose output lands somewhere it can
be queried back out. `StaticKnowledgeLookup.from_yaml` had the same hole.

Both now go through `_resolve_yaml_path()`, which requires an existing regular
file with a .yaml/.yml suffix. It calls `resolve()` first, deliberately: that
collapses `..` and follows symlinks, so a `policy.yaml` symlinked at a private
key is judged by its real name and fails the suffix check instead of sailing
through it. The seeder CLI now exits 2 with a clear message instead of
propagating a traceback.

Verified by hand that /etc/hostname, a suffix-less file and a .yaml symlink to a
key file are all refused, and that a genuine policy YAML still loads.

Note on the previous commit: it moved a NOSONAR marker in ci.yml on the theory
that the block-scalar placement was what dropped the Security Rating. That
placement was genuinely wrong and is worth keeping fixed, but it was not the
cause — this was. The rule key only became visible once the finding surfaced as
a code-scanning review comment on knowledge.py:134.

Tests: 7 new (non-YAML suffix, missing file, directory, symlink disguise,
traversal, valid file, and the same validation on StaticKnowledgeLookup).
198 total, all passing.

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants