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>
586 lines
21 KiB
Python
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"]
|