Files
kamidso-back/tests/kamidso/test_didar_semantics.py
T
javadandCursor edbaa2acec Add native Didar connectors so chats can query CRM data reliably.
Connector calls, semantic rules, and DuckDB memory keep long runs stable, and stream retry stops chats from stalling.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-12 19:26:47 +03:30

586 lines
21 KiB
Python

"""Didar semantic accuracy: status, dates, money, and report routing."""
from __future__ import annotations
import json
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
import httpx
import jdatetime
import pytest
from kamidso.engine.app.openapi.api_v1.tools.connector_call import make_connector_call
from kamidso.engine.core.agent.resource.connector.credential import CredentialStore
from kamidso.native_connectors.executor import NativeConnectorExecutor
from kamidso.native_connectors.prompt import render_native_prompt
from kamidso.native_connectors.registry import get_registry, reset_registry
from kamidso.native_connectors.semantic.aggregates import count_for_question
from kamidso.native_connectors.semantic.catalog import (
disclosure_for,
find_operation,
operation_enabled,
provider_dir,
route_question,
)
from kamidso.native_connectors.semantic.dates import resolve_period
from kamidso.native_connectors.semantic.golden import reconciliation_verdict
from kamidso.native_connectors.semantic.money import summarize_amounts
from kamidso.native_connectors.semantic.status import canonical_status
_TEHRAN = ZoneInfo("Asia/Tehran")
def _enable(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("KAMIDSO_NATIVE_CONNECTORS_V1", "1")
monkeypatch.setenv("KAMIDSO_DIDAR_CONNECTOR", "1")
monkeypatch.delenv("KAMIDSO_ENV", raising=False)
reset_registry()
def _row(store: CredentialStore) -> dict:
salt = store.generate_salt()
return {
"connector_id": "didar-a",
"connector_type": "didar",
"display_name": "دیدار فروش",
"status": "active",
"encrypted_credentials": store.encrypt({"api_key": "super-secret-key"}, salt),
"encryption_salt": salt,
"user_name": "usr_test",
"config": {},
}
def _client(handler, config=None):
store = CredentialStore()
row = _row(store)
if config is not None:
row["config"] = config
transport = httpx.MockTransport(handler)
client = httpx.AsyncClient(transport=transport)
executor = NativeConnectorExecutor(
client=client,
credential_store=store,
row_loader=lambda connector_id: row if connector_id == "didar-a" else None,
)
return executor, client
def _content(text: str) -> str:
payload = json.loads(text)
return payload["chunks"][0]["content"]
def _state():
return {
"conv_id": "conv-semantic",
"user_name": "usr_test",
"native_connectors": {
"didar-a": {
"connector_id": "didar-a",
"connector_type": "didar",
"display_name": "دیدار فروش",
}
},
}
def _won(when: str, amount: str, currency: int, owner: str = "owner-a") -> dict:
return {
"Status": "Won",
"ChangeToWonTime": when,
"FinalAdjustmentPrice": amount,
"CurrencyId": currency,
"OwnerId": owner,
"IsDeleted": False,
}
def test_contract_file_locks_status_counts_and_routing(monkeypatch: pytest.MonkeyPatch):
_enable(monkeypatch)
document = json.loads(
(provider_dir() / "semantic_contract_tests.json").read_text(encoding="utf-8")
)
for case in document["tests"]:
if case["id"] == "deal_status_authoritative":
assert canonical_status(case["input_record"]) == case["expected"]["canonical_status"]
elif case["id"] == "search_deals_page_count_not_total":
source, count = count_for_question(case["response"], case["question"])
assert source == case["expected_count_source"]
assert count == case["expected_count"]
assert count != len(case["response"]["Response"]["List"])
elif case["id"] == "sales_period_uses_won_time":
name = route_question(case["question"])
assert name == case["expected_operation"]
operation = find_operation("didar", name)
assert operation is not None
assert operation["date_basis"] == case["expected_date_basis"]
assert operation["query_template"]["Criteria"]["Status"] == case["expected_status"]
elif case["id"] == "generic_deals_period_uses_register_time":
name = route_question(case["question"])
assert name == case["expected_operation"]
assert find_operation("didar", name)["date_basis"] == case["expected_date_basis"]
assert case["required_disclosure"] in disclosure_for(name)
elif case["id"] == "multi_currency_no_raw_sum":
summary = summarize_amounts(case["records"], {})
assert summary.mixed is True
assert summary.combined_total is None
assert case["expected"]["raw_cross_currency_sum_allowed"] is False
elif case["id"] == "unknown_currency_no_label_guess":
summary = summarize_amounts(case["records"], case["calibration"])
label = summary.groups[0]["label"]
assert label == "CurrencyId=96107"
assert "تومان" not in label
assert "ریال" not in label
assert case["expected"]["may_say_toman"] is False
elif case["id"] == "lead_function_not_invented":
assert route_question(case["question"]) == "lead_count"
assert operation_enabled("lead_count") is False
enabled = {item.name for item in get_registry().get("didar").enabled_reads()}
for blocked in case["expected"]["must_not_call"]:
assert blocked not in enabled
def test_accuracy_patch_adds_sort_without_dropping_the_page_cap(monkeypatch: pytest.MonkeyPatch):
_enable(monkeypatch)
deals = get_registry().get("didar").function("search_deals")
criteria = deals.input_schema["properties"]["Criteria"]["properties"]
assert criteria["Sort"]["maximum"] == 10
assert criteria["Status"]["enum"] == ["Pending", "Won", "Lost"]
assert deals.input_schema["properties"]["Limit"]["maximum"] == 100
assert "Response.WonDealsTotalPrice" in deals.summary_fields
assert any("Sort=9" in hint for hint in deals.semantic_hints)
users = get_registry().get("didar").function("get_users")
assert users.records_path == "Response"
def test_jalali_month_is_half_open_tehran_and_handles_esfand():
mehr = resolve_period("مهر ۱۴۰۵")
same = resolve_period("مهر 1405")
assert mehr.start_utc == same.start_utc == "2026-09-22T20:30:00Z"
assert mehr.end_utc_exclusive == "2026-10-22T20:30:00Z"
assert mehr.start_local.startswith("1405-07-01T00:00:00")
assert mehr.end_local_exclusive.startswith("1405-08-01T00:00:00")
assert mehr.contains(mehr.start) is True
assert mehr.contains(mehr.end) is False
assert mehr.contains(mehr.end - timedelta(seconds=1)) is True
esfand = resolve_period("اسفند ۱۴۰۳")
assert esfand.start_local.startswith("1403-12-01")
assert esfand.end_local_exclusive.startswith("1404-01-01")
jdatetime.date(1403, 12, 30)
with pytest.raises(ValueError):
jdatetime.date(1404, 12, 30)
now = datetime(2026, 10, 11, 12, 0, tzinfo=_TEHRAN)
omitted = resolve_period("مهر", now=now)
assert omitted.start_utc == mehr.start_utc
month = resolve_period("این ماه", now=now)
today = jdatetime.date.fromgregorian(date=now.date())
assert month.start_local.startswith(f"{today.year:04d}-{today.month:02d}-01")
week = resolve_period("این هفته", now=now)
start = datetime.fromisoformat(week.start_utc.replace("Z", "+00:00")).astimezone(_TEHRAN)
assert start.weekday() == 5
assert start.hour == 0
assert week.end - week.start == timedelta(days=7)
def test_shipped_golden_template_is_not_verified():
document = json.loads(
(provider_dir() / "golden_reconciliation_template.json").read_text(encoding="utf-8")
)
verdict = reconciliation_verdict(document)
assert verdict["verified"] is False
assert verdict["panel_parity_verified"] is False
passed = reconciliation_verdict(
{
"periods": [
{
"result": "pass",
"panel_expected": {
"sales_won_count": 1,
"sales_amount": 1,
"currency_display": "IRR",
"won_count": 1,
"lost_count": 0,
"pending_count_snapshot": 0,
"sales_by_owner": [],
},
"kamidso_actual": {"sales_won_count": 1},
}
]
}
)
assert passed["verified"] is True
def test_prompt_prefers_semantic_operations(monkeypatch: pytest.MonkeyPatch):
_enable(monkeypatch)
text = render_native_prompt(
[{"connector_id": "didar-a", "connector_type": "didar", "display_name": "دیدار فروش"}]
)
assert "sales_summary" in text
assert "deals_created" in text
assert "get_leads" in text
assert "additionalProperties" not in text
assert "/api/" not in text
@pytest.mark.asyncio
async def test_sales_summary_verifies_won_time_and_refuses_cross_currency_sum(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
period = resolve_period("مهر ۱۴۰۵")
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(json.loads(request.content.decode()))
return httpx.Response(
200,
json={
"Response": {
"TotalCount": 2,
"WonDealsCount": 2,
"List": [
_won(period.start_utc, "100", 1),
_won(period.start_utc, "200", 2),
],
}
},
)
executor, client = _client(handler)
try:
tool = make_connector_call(_state(), executor=executor)
text = _content(await tool(name="sales_summary", body={"period": "مهر ۱۴۰۵"}))
finally:
await client.aclose()
assert len(calls) == 1
criteria = calls[0]["Criteria"]
assert criteria["Status"] == "Won"
assert criteria["Sort"] == 9
assert criteria["SearchFromTime"] == period.start_utc
assert criteria["SearchToTime"] == period.end_utc_exclusive
assert criteria["IsDeleted"] is False
assert "From" not in criteria
assert "\ncount: 2\n" in text
assert "date_basis: won_at" in text
assert "مبنای بازه: تاریخ موفق‌شدن" in text
assert "CurrencyId=1" in text
assert "CurrencyId=2" in text
assert "300" not in text
assert "تومان" not in text
assert "ریال" not in text
assert "panel_parity_verified: false" in text
assert "mode: safe" in text
@pytest.mark.asyncio
async def test_pending_timestamp_does_not_count_as_won(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
period = resolve_period("مهر 1405")
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={
"Response": {
"TotalCount": 1,
"WonDealsCount": 1,
"List": [
{
"Status": "Pending",
"ChangeToWonTime": period.start_utc,
"Probability": 100,
"FinalAdjustmentPrice": "40",
"CurrencyId": 96107,
}
],
}
},
)
executor, client = _client(handler)
try:
tool = make_connector_call(_state(), executor=executor)
text = _content(await tool(name="sales_summary", body={"period": "مهر 1405"}))
finally:
await client.aclose()
assert "\ncount: 0\n" in text
assert "\ncount: 1\n" not in text
assert "was not used" in text
assert "تومان" not in text
assert "ریال" not in text
@pytest.mark.asyncio
async def test_deals_created_uses_registration_total_not_the_page(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
period = resolve_period("مهر ۱۴۰۵")
calls = []
def handler(request: httpx.Request) -> httpx.Response:
calls.append(json.loads(request.content.decode()))
return httpx.Response(
200,
json={
"Response": {
"TotalCount": 12,
"List": [
{
"Status": "Pending",
"ChangeToWonTime": period.start_utc,
"RegisterTime": period.start_utc,
"CurrencyId": 96107,
"FinalAdjustmentPrice": "10",
}
],
}
},
)
executor, client = _client(handler)
try:
tool = make_connector_call(_state(), executor=executor)
text = _content(await tool(name="deals_created", body={"period": "مهر ۱۴۰۵"}))
finally:
await client.aclose()
assert len(calls) == 1
assert calls[0]["Criteria"]["Sort"] == 0
assert "Status" not in calls[0]["Criteria"]
assert "\ncount: 12\n" in text
assert "count_source: Response.TotalCount" in text
assert "مبنای بازه: تاریخ ثبت" in text
assert "date_basis: created_at" in text
assert "Amounts: withheld" in text
@pytest.mark.asyncio
async def test_sales_summary_pages_past_the_first_hundred(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
period = resolve_period("مهر 1405")
offsets = []
def handler(request: httpx.Request) -> httpx.Response:
body = json.loads(request.content.decode())
offsets.append(body["From"])
if body["From"] == 0:
rows = [_won(period.start_utc, "1", 1) for _ in range(100)]
else:
rows = [_won(period.start_utc, "1", 1)]
return httpx.Response(
200,
json={"Response": {"TotalCount": 101, "WonDealsCount": 101, "List": rows}},
)
executor, client = _client(handler)
try:
tool = make_connector_call(_state(), executor=executor)
text = _content(await tool(name="sales_summary", body={"period": "مهر 1405"}))
finally:
await client.aclose()
assert offsets == [0, 100]
assert "\ncount: 101\n" in text
assert "\ncount: 100\n" not in text
@pytest.mark.asyncio
async def test_over_cap_period_returns_accuracy_guard_without_a_total(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
calls = {"n": 0}
def handler(request: httpx.Request) -> httpx.Response:
calls["n"] += 1
return httpx.Response(
200,
json={
"Response": {
"TotalCount": 5001,
"WonDealsCount": 5001,
"List": [_won("2026-09-23T00:00:00Z", "9", 1)],
}
},
)
executor, client = _client(handler)
try:
tool = make_connector_call(_state(), executor=executor)
text = await tool(name="sales_summary", body={"period": "مهر ۱۴۰۵"})
finally:
await client.aclose()
assert calls["n"] == 1
assert "accuracy_guard" in text
assert "5001" not in text
assert "count:" not in text
@pytest.mark.asyncio
async def test_disabled_lead_count_and_malformed_body_send_no_http(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
calls = {"n": 0}
def handler(request: httpx.Request) -> httpx.Response:
calls["n"] += 1
return httpx.Response(200, json={"Response": []})
executor, client = _client(handler)
try:
tool = make_connector_call(_state(), executor=executor)
disabled = await tool(name="lead_count", body={"period": "مهر ۱۴۰۵"})
invalid = await tool(name="sales_summary", body={"Criteria": {"Status": "Won"}})
unknown = await tool(name="get_leads", body={})
finally:
await client.aclose()
assert calls["n"] == 0
assert "function_disabled" in disabled
assert "validation_error" in invalid
assert "http_sent" in invalid
assert "unknown_function" in unknown
assert "search_persons" in unknown
assert "search_deals" in unknown
@pytest.mark.asyncio
async def test_sales_by_owner_resolves_names_and_hides_raw_ids(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
period = resolve_period("مهر 1405")
owner_id = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee"
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path.endswith("/User/List"):
return httpx.Response(
200,
json={"Response": [{"UserId": owner_id, "DisplayName": "سارا"}]},
)
return httpx.Response(
200,
json={
"Response": {
"TotalCount": 1,
"WonDealsCount": 1,
"List": [_won(period.start_utc, "80", 96107, owner_id)],
}
},
)
executor, client = _client(handler)
try:
tool = make_connector_call(_state(), executor=executor)
text = _content(await tool(name="sales_by_owner", body={"period": "مهر 1405"}))
finally:
await client.aclose()
assert "سارا" in text
assert owner_id not in text
assert "CurrencyId=96107" in text
assert "تومان" not in text
assert "ریال" not in text
@pytest.mark.asyncio
async def test_calibrated_account_can_be_marked_verified(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
period = resolve_period("مهر 1405")
pipeline_id = "11111111-2222-3333-4444-555555555555"
def handler(request: httpx.Request) -> httpx.Response:
if request.url.path.endswith("/pipeline/list/0"):
return httpx.Response(
200,
json={"Response": [{"Id": pipeline_id, "Title": "فروش سازمانی", "Stages": []}]},
)
return httpx.Response(
200,
json={
"Response": {
"TotalCount": 1,
"WonDealsCount": 1,
"List": [_won(period.start_utc, "80", 1)],
}
},
)
executor, client = _client(
handler,
{
"semantic_calibration": {
"panel_parity_verified": True,
"dashboard_aggregate_currency_semantics_verified": True,
"currency_map": {"1": {"code": "IRR"}},
}
},
)
try:
tool = make_connector_call(_state(), executor=executor)
text = _content(
await tool(
name="sales_summary",
body={"period": "مهر 1405", "pipeline_id": pipeline_id},
)
)
finally:
await client.aclose()
assert "mode: verified" in text
assert "currency_verified: true" in text
assert "panel_parity_verified: true" in text
assert "Amount: IRR 80" in text
assert "pipeline: فروش سازمانی" in text
assert pipeline_id not in text
@pytest.mark.asyncio
async def test_raw_search_tells_the_model_not_to_count_the_page(
monkeypatch: pytest.MonkeyPatch, tmp_path
):
_enable(monkeypatch)
monkeypatch.setattr("kamidso.engine.core.configs.model_config.PILOT_PATH", str(tmp_path))
def handler(request: httpx.Request) -> httpx.Response:
return httpx.Response(
200,
json={
"Response": {
"TotalCount": 651,
"WonDealsCount": 651,
"List": [{"Title": "Deal", "Status": "Won"}],
}
},
)
executor, client = _client(handler)
try:
result = await executor.execute(
connector_id="didar-a",
function_name="search_deals",
body={"Criteria": {}, "From": 0, "Limit": 100},
react_state={"conv_id": "conv-semantic"},
display_name="دیدار فروش",
)
finally:
await client.aclose()
assert "WonDealsCount: 651" in result["observation"]
assert "only the current page" in result["observation"]