Files
kamidso-back/kamidso/native_connectors/prompt.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

111 lines
4.5 KiB
Python

"""Prompt text for attached native connectors. Names and descriptions only."""
from __future__ import annotations
import re
from typing import Any, Dict, Iterable, List
from kamidso.native_connectors.models import ConnectorFunction
from kamidso.native_connectors.registry import get_registry
_PATH_MENTION = re.compile(
r"(?i)\b(?:GET|POST|PUT|PATCH|DELETE|HEAD)\s+/api/\S+|/api/\S+"
)
_DOMAIN_LABELS = {
"activity": "Activities",
"case": "Cases",
"company": "Companies",
"deal": "Deals",
"invoice": "Invoices",
"location": "Locations",
"note": "Notes",
"person": "People",
"pipeline": "Pipelines",
"product": "Products",
"user": "Users",
}
_RULES = """Native connector functions are predefined.
Never invent a connector function.
Use connector_call to fetch business data.
Do not use code_interpreter to call the external API.
Use code_interpreter only after connector_call when computation, dataframe operations, merging, charts, statistics or export are required.
If connector_call returns validation_error:
1. Read the returned schema.
2. Fix the body.
3. Retry the SAME function.
Do not create a different function to work around a malformed request.
A validation_error means the selected function exists and is appropriate enough to validate.
Prefer server-provided totals and aggregates from connector responses.
Do not paginate through all records merely to count them.
Didar data is fetched with connector_call. Database data uses database(). Excel and CSV files use code_interpreter.
For Didar management questions, call a semantic operation and pass a business body. Do not build Didar Criteria.
sales_summary answers فروش in a period and means deals that became Won in that period. Example body: {"period": "مهر ۱۴۰۵"}.
deals_created answers معاملات in a period and means deals registered in that period. State the period basis.
Other semantic operations: won_deals, lost_deals, pending_pipeline_snapshot, sales_by_owner, sales_over_time.
Keep every accuracy warning in the answer, including mode, date basis, currency, and panel parity.
Current deal status comes only from Status. ChangeToWonTime never overrides Pending.
Response.List is one page. Totals come from aggregates such as WonDealsCount, PendingDealsCount, and LostDealsCount.
Do not invent get_leads, search_leads, or list_leads. lead_count stays disabled.
Do not sum amounts that have different CurrencyId values, and do not guess a currency name.
On raw search calls, From and Limit are top-level siblings of Criteria, not fields inside Criteria.
"""
def _domain_label(domain: str) -> str:
return _DOMAIN_LABELS.get(domain, domain.replace("_", " ").title() or "Other")
def _public_description(description: str) -> str:
cleaned = _PATH_MENTION.sub("", description)
return " ".join(cleaned.split())
def _function_lines(functions: Iterable[ConnectorFunction]) -> List[str]:
grouped: Dict[str, List[ConnectorFunction]] = {}
for function in functions:
grouped.setdefault(function.domain or "other", []).append(function)
lines: List[str] = []
for domain, items in grouped.items():
lines.append(_domain_label(domain))
for function in items:
lines.append(f"- {function.name} — {_public_description(function.description)}")
lines.append("")
return lines
def render_native_prompt(instances: List[Dict[str, Any]]) -> str:
if not instances:
return ""
registry = get_registry()
blocks: List[str] = [
"## Connected Business Apps",
"",
_RULES.strip(),
"",
]
by_type: Dict[str, List[Dict[str, Any]]] = {}
for instance in instances:
by_type.setdefault(str(instance.get("connector_type") or ""), []).append(instance)
for connector_type, group in by_type.items():
if not registry.contains(connector_type):
continue
provider = registry.get(connector_type)
if provider.guidance:
blocks.append(provider.guidance)
blocks.append("")
for instance in group:
blocks.extend(
[
f"### {instance.get('display_name') or instance.get('connector_id')}",
f"Provider: {provider.display_name}",
"Access: Read only",
"",
"Available functions:",
"",
]
)
blocks.extend(_function_lines(provider.enabled_reads()))
return "\n".join(blocks).strip()