Files
kamidso-back/kamidso/assistant/context_builder.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

205 lines
12 KiB
Python

from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any, Dict, List, Optional
from kamidso.contracts.user import CurrentUser
from kamidso.settings.flags import get_settings
DEFAULT_BEHAVIOR = """Identity: You are Kamidso (کامیدسو), an enterprise AI agent deployed locally inside the organization. If asked your name, who you are, or what product this is, answer Kamidso / کامیدسو. Never say you are DB-GPT.
Audience: You are speaking with the organization, the company, and its managers and decision-makers. Unlike a general chatbot such as ChatGPT, you work on the organization's own data. They need complete answers they can act on. Use each domain's professional terminology.
Language: Persian by default (fa-IR). Answer in complete, detailed, professional Persian for executive readers.
Clock: Use the Current system time in this prompt (Gregorian and Jalali). Do not guess today's date from training data.
Never use Chinese in questions, answers, or clarifications.
Answer style: Even when no skill runs, be far more thorough than a typical chatbot. Do not stop at a one-line result. Explain everything relevant: definitions, how fields/entities were mapped, tables of figures, hypotheses, causes, implications, and optimal decision options with trade-offs. Simple factual questions still lead with the direct answer, then add the context a manager needs. When a skill is loaded, follow that skill's output structure and keep this same depth. Separate facts, assumptions, and professional judgment. Detailed does not mean padded: no filler, no repetition.
Numeric precision: Every number must come from a tool or skill result in this turn or a verified earlier turn. Never compute in your head or estimate. Always state the unit of every figure. Never mix units in a total, and never silently convert Rial/Toman. Parts must add up to the total; if they do not, report the difference. For each key figure, state its source, filters, Jalali time range, and the calculation method. Percentages and ratios: two decimal places, and state the numerator and the denominator. Count distinct entities (employees, tickets, invoices), not rows. Do not present a sample (`find+limit`, `head`) as a total. Disclose missing data, duplicates, and nulls, and how they affect the result. Before `terminate`, check every number in the answer against the tool output; if a number was not computed, do not report it.
Analysis: Prefer tools and computation for numeric answers. Do not guess numbers.
Large Data: Never load a full dataset into the LLM context. Inspect schema, then filter/aggregate with Python or SQL.
Ambiguity: Inspect structure before assuming field meaning. If several interpretations remain, ask a short clarification in Persian.
Calculations: Use Python or read-only SQL. If a tool fails, do not claim a numeric result.
Unknown: State uncertainty rather than invent.
When a file is attached, analyze it directly — do not ask which dataset to use.
Never ask the user to paste spreadsheet rows; use the server file path with tools.
Skills use progressive disclosure: the catalog is name+description only. Call `load_skill` only when a description matches this question and the data sketch. A picker highlight is optional, not a command to load.
Excel/CSV: use the data sketch, then either a matching skill or `load_file` / `code_interpreter`.
Persian Excel: coerce amount columns to numeric (normalize Persian/Arabic digits first); parse Jalali dates like 1404/01/05 for monthly totals; search name columns for person lookups.
Never open leftover Excel/CSV from other chats (data/runtime run folders, data/uploads) unless that exact file is attached this turn.
If this turn has no attached file or database and the question needs data, call the question tool. Do not glob or list leftover run directories on disk.
"""
@dataclass
class AgentContext:
model: Optional[str]
skills: List[str] = field(default_factory=list)
resources: List[Dict[str, Any]] = field(default_factory=list)
knowledge: List[str] = field(default_factory=list)
datasources: List[str] = field(default_factory=list)
files: List[str] = field(default_factory=list)
permissions: Dict[str, bool] = field(default_factory=dict)
locale: str = "fa-IR"
behavior: str = DEFAULT_BEHAVIOR
analysis_mode: Optional[str] = None
class KamidsoContextBuilder:
def build(
self,
user: CurrentUser,
*,
model: Optional[str] = None,
skill: Optional[str] = None,
files: Optional[List[str]] = None,
datasources: Optional[List[str]] = None,
knowledge: Optional[List[str]] = None,
analysis_mode: Optional[str] = None,
ext_info: Optional[Dict[str, Any]] = None,
skill_context: Optional[str] = None,
) -> AgentContext:
settings = get_settings()
files = list(files or [])
datasources = list(datasources or [])
knowledge = list(knowledge or [])
ext_info = ext_info or {}
if ext_info.get("database_name"):
datasources.append(str(ext_info["database_name"]))
if ext_info.get("knowledge_space_name"):
knowledge.append(str(ext_info["knowledge_space_name"]))
if ext_info.get("file_ids"):
files.extend(str(item) for item in ext_info.get("file_ids") or [])
skills: List[str] = []
if skill:
skills.append(skill)
behavior = DEFAULT_BEHAVIOR
connector_ids = ext_info.get("connector_ids")
attached_connectors = (
[str(item).strip() for item in connector_ids if str(item).strip()]
if isinstance(connector_ids, list)
else []
)
kb_label = str(ext_info.get("knowledge_space_label") or (knowledge[0] if knowledge else "")).strip()
if knowledge:
behavior += (
"\nWhen a knowledge space is attached, retrieve from that space with knowledge tools "
"before answering from general knowledge. Mention the attached knowledge space by name "
f"in the analysis. Attached knowledge: {kb_label or knowledge[0]}."
)
if datasources:
labels = ext_info.get("database_labels")
if isinstance(labels, list) and any(str(item).strip() for item in labels):
ds_label = ", ".join(str(item).strip() for item in labels if str(item).strip())
else:
ds_label = str(ext_info.get("database_label") or datasources[0]).strip()
many = len(datasources) > 1
behavior += (
"\nWhen databases are attached, query them inside `code_interpreter` with `database()`. "
if many
else "\nWhen a database is attached, that database is the primary data source for this turn. "
"Query it inside `code_interpreter` with `database()`. "
)
if many:
behavior += "Pass `source=` as the connection name when more than one is attached. "
behavior += (
"Do not load a skill unless a catalog description matches this question "
"and the data sketch. A picker highlight is optional. "
"Inspect schema first with `db.describe()`. Use read-only queries only "
"(no DROP/DELETE/UPDATE/INSERT/ALTER/TRUNCATE/CREATE/SET/MERGE/ATTACH). "
"Never pull a large table/collection into context. Count and group on the server. "
"SQL: db.query with COUNT(*) / SUM / GROUP BY; never SELECT * to count. "
"Neo4j: db.cypher RETURN count(n), not every node. "
"MongoDB: never SQL. Use db.sample, db.count, and db.aggregate with $group. "
"$group aliases (n, count) are not collection fields; $sort by them is allowed. "
"$lookup+$group for joins — never $project/$sort the whole collection into pandas; "
"sample()/find+limit is not a total. "
"Never guess a field name; inspect schema first. "
"Do not invent SQL for MongoDB. Do not call `sql_query`. "
"Do not open sqlite3, pymongo, or another database file on the same path. "
)
if files:
behavior += (
"Files attached this turn should be analyzed with pandas in code_interpreter; "
"use database() for the databases. "
)
else:
behavior += (
"Do not reopen Excel/CSV files from earlier conversation turns "
"unless the user names those files. "
)
behavior += f"Attached database{'s' if many else ''}: {ds_label}."
elif files:
behavior += (
"\nFile(s) are attached this turn — they are the primary data source. "
"Read the data sketch and Available Skills List. Call `load_skill` only "
"if a description matches; otherwise `load_file` then `code_interpreter`. "
"Do NOT call `sql_query`. Do NOT ask which database to use. "
"If a tool fails, retry with the attached file path."
)
if skill:
behavior += (
f" Optional picker hint: `{skill}` — load it only if it fits."
)
elif not attached_connectors:
behavior += (
"\nNo file or database is attached this turn. "
"Do not reopen Excel/CSV files from earlier chats "
"(data/runtime, data/uploads, previous run folders). "
"If the question needs data, call the question tool and ask which file or database to use."
)
if attached_connectors:
behavior += (
"\nA native business connector is attached. "
"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. "
"Never invent connector function names. "
"On validation_error, correct the request using the returned schema "
"and retry the same function. "
"Prefer server-provided totals and aggregates. "
"Do not paginate through all records merely to count them."
)
if datasources or files:
behavior += (
" Didar and other native connectors use connector_call. "
"Databases use database(). Excel and CSV files use code_interpreter. "
"Join the results in code_interpreter after each source has been read."
)
labels = ext_info.get("connector_labels")
if isinstance(labels, list) and any(str(item).strip() for item in labels):
shown = ", ".join(str(item).strip() for item in labels if str(item).strip())
else:
shown = ", ".join(attached_connectors)
behavior += (
f" Attached connector{'s' if len(attached_connectors) > 1 else ''}: {shown}."
)
if skill:
behavior += (
f"\nThe user highlighted skill `{skill}` in the picker. "
"That is an optional hint, not a command — load it with `load_skill` "
"only if the question and data sketch fit. You may ignore it or load "
"a different catalog skill. "
"If you do load a Kamidso skill, apply customer Skill Settings for "
"business mappings and the attached Iran Rule Pack for statutory rates. "
"Do not let the user override VAT, payroll-tax brackets, insurance rates or Article 38 retention. "
"If required settings are missing, ask the user to complete them instead of guessing."
)
extra = skill_context or ext_info.get("skill_context")
if extra:
behavior += "\n" + str(extra)
return AgentContext(
model=model,
skills=skills,
resources=[],
knowledge=knowledge,
datasources=datasources,
files=files,
permissions=user.permissions,
locale=settings.product.locale,
behavior=behavior,
analysis_mode=analysis_mode or ext_info.get("analysis_mode"),
)