feat: write-through sync of user profile + skills to EspoCRM entities
After SmartAssistant 2.10.0 introduced UserProfile and AssistantSkill entities, the Python side kept writing only to the filesystem. So an admin browsing the EspoCRM UI saw nothing — but the system prompt did pull from CRM via context['userProfile'] / context['availableSkills']. Result: edits made by Shira (via update_user_profile / skill_manage tool) were invisible to admins; edits made by admins took effect immediately because the prompt fetched from DB. Now the two sources agree. * memory.py — added _sync_user_profile_to_crm() that upserts a UserProfile row whenever update_user_profile writes the file. Best-effort with logging; HTTP failures don't break the tool. * skills.py — added _sync_skill_to_crm() that upserts an AssistantSkill row from the SKILL.md frontmatter (description, triggers, version) plus the full body in content. Called after every save_skill in skill_manage. Read path is unchanged: the EspoCRM CaseContextBuilder / SmartAssistantService populate context fields, and the Python prompt_builder consumes them with in-code defaults as a fallback. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -110,6 +110,58 @@ def save_skill(skill_name: str, content: str) -> None:
|
||||
logger.info("Saved skill: %s", safe_name)
|
||||
|
||||
|
||||
async def _sync_skill_to_crm(skill_name: str, content: str) -> None:
|
||||
"""Write-through to AssistantSkill entity in EspoCRM. Best-effort."""
|
||||
import os
|
||||
import httpx
|
||||
|
||||
base = (os.environ.get("ESPOCRM_URL") or "").rstrip("/")
|
||||
key = os.environ.get("ESPOCRM_API_KEY") or ""
|
||||
if not base or not key:
|
||||
return
|
||||
|
||||
# Parse description + triggers from frontmatter for the metadata columns.
|
||||
fm, body = _parse_frontmatter(content)
|
||||
description = (fm.get("description") or "")[:500]
|
||||
triggers = fm.get("triggers", "")
|
||||
if isinstance(triggers, list):
|
||||
triggers = ", ".join(str(t) for t in triggers)
|
||||
version = str(fm.get("version") or "1.0")[:20]
|
||||
|
||||
headers = {"X-Api-Key": key, "Content-Type": "application/json"}
|
||||
try:
|
||||
async with httpx.AsyncClient(timeout=15.0) as client:
|
||||
r = await client.get(
|
||||
f"{base}/api/v1/AssistantSkill",
|
||||
headers=headers,
|
||||
params={
|
||||
"where[0][type]": "equals",
|
||||
"where[0][attribute]": "name",
|
||||
"where[0][value]": skill_name,
|
||||
"maxSize": 1,
|
||||
},
|
||||
)
|
||||
existing = (r.json().get("list", []) if r.status_code == 200 else [])
|
||||
row = existing[0] if existing else None
|
||||
|
||||
payload = {
|
||||
"description": description,
|
||||
"triggers": triggers,
|
||||
"content": content,
|
||||
"version": version,
|
||||
"isActive": True,
|
||||
}
|
||||
if row:
|
||||
resp = await client.put(f"{base}/api/v1/AssistantSkill/{row['id']}", headers=headers, json=payload)
|
||||
else:
|
||||
payload["name"] = skill_name
|
||||
resp = await client.post(f"{base}/api/v1/AssistantSkill", headers=headers, json=payload)
|
||||
if resp.status_code >= 400:
|
||||
logger.warning("AssistantSkill sync HTTP %s: %s", resp.status_code, resp.text[:200])
|
||||
except Exception as e:
|
||||
logger.warning("AssistantSkill sync failed: %s", e)
|
||||
|
||||
|
||||
def register_skill_tools(tools: dict) -> None:
|
||||
"""Register skill-related tools into the tools dict."""
|
||||
|
||||
@@ -137,6 +189,7 @@ def register_skill_tools(tools: dict) -> None:
|
||||
if existing:
|
||||
return f'❌ Skill "{skill_name}" כבר קיים. השתמש ב-action "update" כדי לעדכן.'
|
||||
save_skill(skill_name, content)
|
||||
await _sync_skill_to_crm(skill_name, content)
|
||||
return f'✅ Skill "{skill_name}" נוצר בהצלחה.'
|
||||
elif action == "update":
|
||||
existing = view_skill(skill_name)
|
||||
@@ -145,6 +198,7 @@ def register_skill_tools(tools: dict) -> None:
|
||||
if not content:
|
||||
return "❌ צריך לספק תוכן מעודכן."
|
||||
save_skill(skill_name, content)
|
||||
await _sync_skill_to_crm(skill_name, content)
|
||||
return f'✅ Skill "{skill_name}" עודכן בהצלחה.'
|
||||
else:
|
||||
return f'❌ פעולה לא מוכרת: {action}. השתמש ב-"create" או "update".'
|
||||
|
||||
Reference in New Issue
Block a user