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>
205 lines
12 KiB
Python
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"),
|
|
)
|