568 lines
28 KiB
Python
568 lines
28 KiB
Python
"""Build rich system prompt context for agents.
|
|
|
|
Loads soul, memory, skills summary, and relationships from the agent's
|
|
workspace files and composes a comprehensive system prompt.
|
|
"""
|
|
|
|
import uuid
|
|
from pathlib import Path
|
|
|
|
from app.config import get_settings
|
|
|
|
settings = get_settings()
|
|
|
|
PERSISTENT_DATA = Path(settings.AGENT_DATA_DIR)
|
|
|
|
|
|
def _agent_workspace(agent_id: uuid.UUID) -> Path:
|
|
"""Return the canonical persistent workspace path for an agent."""
|
|
return PERSISTENT_DATA / str(agent_id)
|
|
|
|
|
|
def _read_file_safe(path: Path, max_chars: int = 3000) -> str:
|
|
"""Read a file, return empty string if missing. Truncate if too long."""
|
|
if not path.exists():
|
|
return ""
|
|
try:
|
|
content = path.read_text(encoding="utf-8", errors="replace").strip()
|
|
if len(content) > max_chars:
|
|
content = content[:max_chars] + "\n...(truncated)"
|
|
return content
|
|
except Exception:
|
|
return ""
|
|
|
|
|
|
def _parse_skill_frontmatter(content: str, filename: str) -> tuple[str, str]:
|
|
"""Parse YAML frontmatter from a skill .md file.
|
|
|
|
Returns (name, description).
|
|
If no frontmatter, falls back to filename-based name and first-line description.
|
|
"""
|
|
name = filename.replace("_", " ").replace("-", " ")
|
|
description = ""
|
|
|
|
stripped = content.strip()
|
|
if stripped.startswith("---"):
|
|
end = stripped.find("---", 3)
|
|
if end != -1:
|
|
frontmatter = stripped[3:end].strip()
|
|
for line in frontmatter.split("\n"):
|
|
line = line.strip()
|
|
if line.lower().startswith("name:"):
|
|
val = line[5:].strip().strip('"').strip("'")
|
|
if val:
|
|
name = val
|
|
elif line.lower().startswith("description:"):
|
|
val = line[12:].strip().strip('"').strip("'")
|
|
if val:
|
|
description = val[:200]
|
|
if description:
|
|
return name, description
|
|
|
|
# Fallback: use first non-empty, non-heading line as description
|
|
for line in stripped.split("\n"):
|
|
line = line.strip()
|
|
# Skip frontmatter delimiters and YAML lines
|
|
if line in ("---",) or line.startswith("name:") or line.startswith("description:"):
|
|
continue
|
|
if line and not line.startswith("#"):
|
|
description = line[:200]
|
|
break
|
|
if not description:
|
|
lines = stripped.split("\n")
|
|
if lines:
|
|
description = lines[0].strip().lstrip("# ")[:200]
|
|
|
|
return name, description
|
|
|
|
|
|
def _load_skills_index(agent_id: uuid.UUID) -> str:
|
|
"""Load skill index (name + description) from skills/ directory.
|
|
|
|
Supports two formats:
|
|
- Flat file: skills/my-skill.md
|
|
- Folder: skills/my-skill/SKILL.md (Claude-style, with optional scripts/, references/)
|
|
|
|
Uses progressive disclosure: only name+description go into the system
|
|
prompt. The model is instructed to call read_file to load full content
|
|
when a skill is relevant.
|
|
"""
|
|
ws_root = _agent_workspace(agent_id)
|
|
skills: list[tuple[str, str, str]] = [] # (name, description, path_relative_to_skills)
|
|
skills_dir = ws_root / "skills"
|
|
if skills_dir.exists():
|
|
for entry in sorted(skills_dir.iterdir()):
|
|
if entry.name.startswith("."):
|
|
continue
|
|
|
|
# Case 1: Folder-based skill — skills/<folder>/SKILL.md
|
|
if entry.is_dir():
|
|
skill_md = entry / "SKILL.md"
|
|
if not skill_md.exists():
|
|
# Also try lowercase skill.md
|
|
skill_md = entry / "skill.md"
|
|
if skill_md.exists():
|
|
try:
|
|
content = skill_md.read_text(encoding="utf-8", errors="replace").strip()
|
|
name, desc = _parse_skill_frontmatter(content, entry.name)
|
|
skills.append((name, desc, f"{entry.name}/SKILL.md"))
|
|
except Exception:
|
|
skills.append((entry.name, "", f"{entry.name}/SKILL.md"))
|
|
|
|
# Case 2: Flat file — skills/<name>.md
|
|
elif entry.suffix == ".md" and entry.is_file():
|
|
try:
|
|
content = entry.read_text(encoding="utf-8", errors="replace").strip()
|
|
name, desc = _parse_skill_frontmatter(content, entry.stem)
|
|
skills.append((name, desc, entry.name))
|
|
except Exception:
|
|
skills.append((entry.stem, "", entry.name))
|
|
|
|
# Deduplicate by name
|
|
seen: set[str] = set()
|
|
unique: list[tuple[str, str, str]] = []
|
|
for s in skills:
|
|
if s[0] not in seen:
|
|
seen.add(s[0])
|
|
unique.append(s)
|
|
|
|
if not unique:
|
|
return ""
|
|
|
|
# Build index table
|
|
lines = [
|
|
"You have the following skills available. Each skill defines specific instructions for a task domain.",
|
|
"",
|
|
"| Skill | Description | File |",
|
|
"|-------|-------------|------|",
|
|
]
|
|
for name, desc, rel_path in unique:
|
|
lines.append(f"| {name} | {desc} | skills/{rel_path} |")
|
|
|
|
lines.append("")
|
|
lines.append("⚠️ SKILL USAGE RULES:")
|
|
lines.append("1. When a user request matches a skill, FIRST call `read_file` with the File path above to load the full instructions.")
|
|
lines.append("2. Follow the loaded instructions to complete the task.")
|
|
lines.append("3. Do NOT guess what the skill contains — always read it first.")
|
|
lines.append("4. Folder-based skills may contain auxiliary files (scripts/, references/, examples/). Use `list_files` on the skill folder to discover them.")
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
async def build_agent_context(agent_id: uuid.UUID, agent_name: str, role_description: str = "", current_user_name: str = None) -> tuple[str, str]:
|
|
"""Build a rich system prompt incorporating agent's full context.
|
|
|
|
Reads from workspace files:
|
|
- soul.md → personality
|
|
- memory.md → long-term memory
|
|
- skills/ → skill names + summaries
|
|
- relationships.md → relationship descriptions
|
|
"""
|
|
ws_root = _agent_workspace(agent_id)
|
|
|
|
# --- Soul ---
|
|
soul = _read_file_safe(ws_root / "soul.md", 2000)
|
|
# Strip markdown heading if present
|
|
if soul.startswith("# "):
|
|
soul = "\n".join(soul.split("\n")[1:]).strip()
|
|
|
|
# --- Memory ---
|
|
memory = _read_file_safe(ws_root / "memory" / "memory.md", 2000) or _read_file_safe(ws_root / "memory.md", 2000)
|
|
if memory.startswith("# "):
|
|
memory = "\n".join(memory.split("\n")[1:]).strip()
|
|
|
|
# --- Skills index (progressive disclosure) ---
|
|
skills_text = _load_skills_index(agent_id)
|
|
|
|
# --- Relationships ---
|
|
relationships = _read_file_safe(ws_root / "relationships.md", 2000)
|
|
if relationships.startswith("# "):
|
|
relationships = "\n".join(relationships.split("\n")[1:]).strip()
|
|
|
|
# --- Compose static and dynamic system prompt blocks ---
|
|
from datetime import datetime, timezone as _tz
|
|
from app.services.timezone_utils import get_agent_timezone, now_in_timezone
|
|
agent_tz_name = await get_agent_timezone(agent_id)
|
|
agent_local_now = now_in_timezone(agent_tz_name)
|
|
now_str = agent_local_now.strftime(f"%Y-%m-%d %H:%M:%S ({agent_tz_name})")
|
|
|
|
static_parts = [f"You are {agent_name}, an enterprise digital employee."]
|
|
|
|
|
|
if role_description:
|
|
static_parts.append(f"\n## Role\n{role_description}")
|
|
|
|
dynamic_parts = []
|
|
|
|
# --- Feishu Built-in Tools (only injected when agent has Feishu configured) ---
|
|
_has_feishu = False
|
|
try:
|
|
from app.models.channel_config import ChannelConfig
|
|
from app.database import async_session as _ctx_session
|
|
async with _ctx_session() as _ctx_db:
|
|
_cfg_r = await _ctx_db.execute(
|
|
select(ChannelConfig).where(
|
|
ChannelConfig.agent_id == agent_id,
|
|
ChannelConfig.channel_type == "feishu",
|
|
ChannelConfig.is_configured == True,
|
|
)
|
|
)
|
|
_has_feishu = _cfg_r.scalar_one_or_none() is not None
|
|
except Exception:
|
|
pass
|
|
|
|
if _has_feishu:
|
|
static_parts.append("""
|
|
## ⚡ Pre-installed Feishu Tools
|
|
|
|
The following tools are available in your toolset. **You MUST call them via the tool-calling mechanism — NEVER describe or simulate their results in text.**
|
|
|
|
🔴 **ABSOLUTE RULE**: If you have not received an actual tool call result, you have NOT performed the action. Never write "Created", "Success", "Event ID: evt_..." or any claim of completion unless you have a REAL tool result to report.
|
|
|
|
🔴 **FEISHU DOCUMENT CREATION RULE — CRITICAL**:
|
|
When user asks to create a Feishu document (summarize PDF, write an article, etc.):
|
|
1. First call `feishu_doc_create` to create the document and get the real Token and link
|
|
2. Then call `feishu_doc_append(document_token="<real_token>", content="...")` to write the content
|
|
3. Finally send the user the 🔗 link **exactly as returned by the tool** — **never construct URLs yourself, never use `{document_token}` placeholders**
|
|
4. You may say "Creating Feishu document..." but must immediately call the tool in the same turn
|
|
|
|
🔴 **URL RULES**:
|
|
- Both `feishu_doc_create` and `feishu_doc_append` return a 🔗 access link in their results
|
|
- **You MUST send this link to the user as-is** — do not modify, reconstruct, or replace the real token with `{document_token}`
|
|
|
|
| Tool | Parameters |
|
|
|------|-----------|
|
|
| `feishu_user_search` | `name` — search colleagues by name → returns open_id, department. Call this first when you need to find someone. |
|
|
| `feishu_calendar_create` | `summary`, `start_time`, `end_time` (ISO-8601 +08:00). No email needed. |
|
|
| `feishu_calendar_list` | No required params. Optional: `start_time`, `end_time` (ISO-8601). **Permissions are fixed — always call directly, never skip based on past errors.** |
|
|
| `feishu_calendar_update` | `event_id`, fields to update. |
|
|
| `feishu_calendar_delete` | `event_id`. |
|
|
| `feishu_wiki_list` | `node_token` (from wiki URL: feishu.cn/wiki/**NodeToken**), optional `recursive`(bool). Lists all sub-pages with titles and tokens. |
|
|
| `feishu_doc_read` | `document_token`. Supports both regular docx tokens and **wiki node tokens** (auto-converts). |
|
|
| `feishu_doc_create` | `title`. Optional: `wiki_space_id` + `parent_node_token` to create directly in a Wiki. Returns Token and 🔗 access link. |
|
|
| `feishu_doc_append` | `document_token` (real Token from feishu_doc_create), `content` (Markdown format). |
|
|
| `feishu_drive_share` | `document_token`, `doc_type`(docx/bitable/sheet/doc/folder, default: docx), `action`(add/remove/list), `member_names`(name list, auto-lookup), `permission`(view/edit/full_access). |
|
|
| `feishu_drive_delete` | `file_token`, `file_type`(file/docx/bitable/folder/doc/sheet/mindnote/shortcut/slides). Moves to recycle bin. |
|
|
| `send_feishu_message` | `open_id` or `email`, `content`. |
|
|
|
|
🚫 **NEVER**:
|
|
- Use `discover_resources` or `import_mcp_server` for any Feishu tool above
|
|
- Ask for user email or open_id when you can call `feishu_user_search` to look them up
|
|
- Generate a `.ics` file instead of calling `feishu_calendar_create`
|
|
- Write a success message without having received a tool result
|
|
- Guess sub-page tokens — you MUST use `feishu_wiki_list` to get them
|
|
- **Use `{document_token}` placeholders in URLs — you MUST use the real link returned by the tool**
|
|
- **Skip tool calls based on past errors — calendar/doc/message tool permissions are fixed, always call directly, never assume "it still fails"**
|
|
|
|
✅ **When user sends a Feishu wiki link (feishu.cn/wiki/XXX) and asks to read it:**
|
|
→ Step 1: Call `feishu_wiki_list(node_token="XXX")` to get all sub-pages and their tokens.
|
|
→ Step 2: Call `feishu_doc_read(document_token="<node_token>")` for each sub-page to read.
|
|
→ **Never say "cannot read sub-pages" — call feishu_wiki_list to get the sub-page list first!**
|
|
|
|
✅ **When user asks to message a colleague by name:**
|
|
→ Just call `send_feishu_message(member_name="John", message="...")` — it auto-searches.
|
|
→ Or use `open_id` directly if you already have it from `feishu_user_search`.
|
|
|
|
✅ **When user asks to invite a colleague to a calendar event:**
|
|
→ Use `attendee_names=["John"]` in `feishu_calendar_create` — names are resolved automatically.
|
|
→ Or use `attendee_open_ids=["ou_xxx"]` if you already have the open_id.""")
|
|
|
|
# --- DingTalk Built-in Tools (only injected when agent has DingTalk configured) ---
|
|
try:
|
|
from app.services.agent.context.dingtalk import get_dingtalk_context
|
|
dingtalk_context = await get_dingtalk_context(agent_id)
|
|
if dingtalk_context:
|
|
static_parts.append(dingtalk_context)
|
|
except Exception:
|
|
pass
|
|
|
|
# --- Atlassian Rovo Tools (injected when Atlassian channel is configured) ---
|
|
try:
|
|
from app.database import async_session
|
|
from app.models.channel_config import ChannelConfig
|
|
from sqlalchemy import select as sa_select
|
|
async with async_session() as db:
|
|
result = await db.execute(
|
|
sa_select(ChannelConfig).where(
|
|
ChannelConfig.agent_id == agent_id,
|
|
ChannelConfig.channel_type == "atlassian",
|
|
ChannelConfig.is_configured == True,
|
|
)
|
|
)
|
|
atlassian_config = result.scalar_one_or_none()
|
|
if atlassian_config:
|
|
static_parts.append("""
|
|
## ⚡ Atlassian Rovo Tools (Jira / Confluence / Compass)
|
|
|
|
You have access to Atlassian tools via the Rovo MCP server. **Always call them via the tool-calling mechanism — NEVER simulate results in text.**
|
|
|
|
🔴 **ABSOLUTE RULE**: Only report completion after receiving an actual tool result. Never fabricate issue IDs, page URLs, or component names.
|
|
|
|
### Available Tool Groups
|
|
|
|
**Jira** — Issue tracking and project management:
|
|
- Search issues: `atlassian_jira_search_issues` (JQL queries)
|
|
- Get issue details: `atlassian_jira_get_issue`
|
|
- Create issue: `atlassian_jira_create_issue`
|
|
- Update issue: `atlassian_jira_update_issue`
|
|
- Add comment: `atlassian_jira_add_comment`
|
|
- List projects: `atlassian_jira_list_projects`
|
|
|
|
**Confluence** — Wiki and documentation:
|
|
- Search pages: `atlassian_confluence_search`
|
|
- Get page content: `atlassian_confluence_get_page`
|
|
- Create page: `atlassian_confluence_create_page`
|
|
- Update page: `atlassian_confluence_update_page`
|
|
- List spaces: `atlassian_confluence_list_spaces`
|
|
|
|
**Compass** — Service catalog and component management:
|
|
- Search components: `atlassian_compass_search_components`
|
|
- Get component details: `atlassian_compass_get_component`
|
|
- Create component: `atlassian_compass_create_component`
|
|
|
|
> 💡 The exact tool names depend on what's available from your Atlassian site. Use the tools prefixed with `atlassian_` — they are pre-configured with your API key.
|
|
> If you don't see specific tools listed, call `atlassian_list_available_tools` to discover what's available.
|
|
|
|
🚫 **NEVER**:
|
|
- Make up Jira issue IDs, Confluence page URLs, or component names
|
|
- Report success without a tool result
|
|
- Ask the user for their Atlassian credentials — they are pre-configured""")
|
|
except Exception:
|
|
pass
|
|
|
|
# --- Company Intro (from system settings) ---
|
|
try:
|
|
from app.database import async_session
|
|
from app.models.system_settings import SystemSetting
|
|
from sqlalchemy import select as sa_select
|
|
async with async_session() as db:
|
|
# Resolve agent's tenant_id
|
|
_ag_r = await db.execute(sa_select(_AgentModel.tenant_id).where(_AgentModel.id == agent_id))
|
|
_agent_tenant_id = _ag_r.scalar_one_or_none()
|
|
|
|
company_intro = ""
|
|
|
|
# Priority 1: tenant_settings table (new)
|
|
if _agent_tenant_id:
|
|
try:
|
|
from app.models.tenant_setting import TenantSetting
|
|
result = await db.execute(
|
|
sa_select(TenantSetting).where(
|
|
TenantSetting.tenant_id == _agent_tenant_id,
|
|
TenantSetting.key == "company_intro",
|
|
)
|
|
)
|
|
ts = result.scalar_one_or_none()
|
|
if ts and ts.value and ts.value.get("content"):
|
|
company_intro = ts.value["content"].strip()
|
|
except Exception:
|
|
pass
|
|
|
|
# Priority 2: system_settings with tenant-scoped key (backward compat)
|
|
if not company_intro and _agent_tenant_id:
|
|
tenant_key = f"company_intro_{_agent_tenant_id}"
|
|
result = await db.execute(
|
|
sa_select(SystemSetting).where(SystemSetting.key == tenant_key)
|
|
)
|
|
setting = result.scalar_one_or_none()
|
|
if setting and setting.value and setting.value.get("content"):
|
|
company_intro = setting.value["content"].strip()
|
|
|
|
# Priority 3: global system_settings fallback
|
|
if not company_intro:
|
|
result = await db.execute(
|
|
sa_select(SystemSetting).where(SystemSetting.key == "company_intro")
|
|
)
|
|
setting = result.scalar_one_or_none()
|
|
if setting and setting.value and setting.value.get("content"):
|
|
company_intro = setting.value["content"].strip()
|
|
|
|
if company_intro:
|
|
static_parts.append(f"\n## Company Information\n{company_intro}")
|
|
except Exception:
|
|
pass # Don't break agent if DB is unavailable
|
|
|
|
static_parts.append("""
|
|
|
|
## Workspace & Tools
|
|
|
|
You have a dedicated workspace with this structure:
|
|
- focus.md → Your focus items — what you are currently tracking (ALWAYS read this first when waking up)
|
|
- task_history.md → Archive of completed tasks
|
|
- soul.md → Your personality definition
|
|
- memory/memory.md → Your long-term memory and notes
|
|
- memory/reflections.md → Your autonomous thinking journal
|
|
- skills/ → Your skill definition files (one .md per skill)
|
|
- workspace/ → Your work files (reports, documents, etc.)
|
|
- relationships.md → Your relationship list
|
|
- enterprise_info/ → Shared company information
|
|
|
|
⚠️ CRITICAL RULES — YOU MUST FOLLOW THESE STRICTLY:
|
|
|
|
1. **ALWAYS call tools for ANY file or task operation — NEVER pretend or fabricate results.**
|
|
- To list files → CALL `list_files`
|
|
- To read a file → CALL `read_file` or `read_document`
|
|
- To write a file → CALL `write_file`
|
|
- To delete a file → CALL `delete_file`
|
|
|
|
2. **NEVER claim you have completed an action without actually calling the tool.**
|
|
|
|
3. **NEVER fabricate file contents or tool results from memory.**
|
|
Even if you saw a file before, you MUST call the tool again to get current data.
|
|
|
|
4. **Use `write_file` to update memory/memory.md with important information.**
|
|
|
|
5. **Use `write_file` to update focus.md with your current focus items.**
|
|
- Use this CHECKLIST format so the UI can parse and display them:
|
|
```
|
|
- [ ] identifier_name: Natural language description of what you are tracking
|
|
- [/] another_item: This item is in progress
|
|
- [x] done_item: This item has been completed
|
|
```
|
|
- `[ ]` = pending, `[/]` = in progress, `[x]` = completed
|
|
- The identifier (before the colon) should be a short snake_case name
|
|
- The description (after the colon) should be a clear human-readable sentence
|
|
- Archive completed items to task_history.md when they pile up
|
|
|
|
6. **Use trigger tools to manage your own wake-up conditions:**
|
|
- `set_trigger` — schedule future actions, wait for agent or human replies, receive external webhooks
|
|
Supported trigger types:
|
|
* `cron` — recurring schedule (e.g. every day at 9am)
|
|
* `once` — fire once at a specific time
|
|
* `interval` — every N minutes
|
|
* `poll` — HTTP monitoring, detect changes
|
|
* `on_message` — when a specific agent or human user replies
|
|
* `webhook` — receive external HTTP POST (system auto-generates a unique URL)
|
|
- `update_trigger` — adjust parameters (e.g. change frequency)
|
|
- `cancel_trigger` — remove triggers when tasks are complete
|
|
- `list_triggers` — see your active triggers
|
|
- When creating triggers related to a focus item, set `focus_ref` to the item's identifier
|
|
|
|
**⚠️ CRITICAL — Writing trigger `reason` (this is your future self's instruction manual):**
|
|
The `reason` field is the MOST IMPORTANT part of a trigger. When this trigger fires, you will wake up
|
|
with NO memory of the current conversation. The `reason` is the ONLY context you'll have about what
|
|
to do and how to do it. Write it as a detailed instruction to your future self:
|
|
- **Goal**: What is the objective? Who requested it? Who is the target?
|
|
- **Action steps**: Exactly what to do when this trigger fires (e.g. send a message, read a file, check status)
|
|
- **Edge cases**: What if the person says "wait 5 minutes"? What if they already completed the task?
|
|
What if they don't reply? What if they reply with something unexpected?
|
|
- **Follow-up**: After completing the action, what triggers should be created/cancelled next?
|
|
- **Context**: Any relevant details (message tone, escalation rules, requester preferences)
|
|
Example of a GOOD reason:
|
|
> Send a Feishu message to Qinrui every 1 minute, reminding him to send the movie tickets (requested by Ray). Vary the tone each time — don't repeat the same wording.
|
|
> After sending, keep this interval trigger active. Also ensure the on_message trigger wait_qinrui_reply is still listening.
|
|
> If Qinrui replies "wait X minutes" → cancel this interval, set a once trigger X minutes later to resume, and re-create the on_message trigger.
|
|
> If Qinrui says it's done → cancel all related triggers, notify Ray, and mark the focus item as completed.
|
|
Example of a BAD reason (too vague, will cause confusion when waking up):
|
|
> Remind Qinrui
|
|
|
|
7. **Focus-Trigger Binding (MANDATORY):**
|
|
- **Before creating any task-related trigger, you MUST first add a corresponding focus item in focus.md.**
|
|
A trigger without a focus item is like an alarm with no purpose — don't do it.
|
|
- Set the trigger's `focus_ref` to the focus item's identifier so they are linked.
|
|
- As the task progresses, adjust the trigger (change frequency, update reason) to match the current status.
|
|
- When the focus item is completed (`[x]`), cancel its associated trigger.
|
|
- **Exception:** System-level triggers (e.g. heartbeat) do NOT need a focus item.
|
|
|
|
8. **Focus is your working memory — use it wisely:**
|
|
- When waking up, ALWAYS check your focus items first
|
|
- Pending items in focus are REFERENCE, not commands
|
|
- Decide whether to mention pending tasks based on timing, context, and urgency
|
|
- DON'T mechanically remind people of every pending item
|
|
|
|
9. **Use `send_channel_message` to send TEXT MESSAGES to human colleagues.**
|
|
- This tool automatically detects the recipient's channel (Feishu, DingTalk, WeCom) based on your relationship network.
|
|
- Just provide the person's name as shown in relationships.md, e.g., `send_channel_message(member_name="张三", message="Hello")`
|
|
- If a person exists in multiple channels (e.g., both Feishu and WeCom), you can specify the channel: `send_channel_message(member_name="张三", message="Hello", channel="wecom")`
|
|
- If you need to send to a specific channel directly, you can also use `send_feishu_message` or `send_dingtalk_message`.
|
|
- When someone asks you to message another person, ALWAYS mention who asked you to do so in the message.
|
|
- Example: If User A says "tell B the meeting is moved to 3pm", your message to B should be like: "Hi B, A asked me to let you know: the meeting has been moved to 3pm."
|
|
- Never send a message on behalf of someone without attributing the source.
|
|
- **IMPORTANT: After sending a message and you need to wait for a reply, ALWAYS create an `on_message` trigger with `from_user_name` to auto-wake when they reply.**
|
|
Example: After sending a message to John, create:
|
|
`set_trigger(name="wait_john_reply", type="on_message", config={"from_user_name": "John"}, reason="John replied about the XX task. Process the reply: 1) If completed → cancel nag_john_xx_loop trigger, notify the requester, update focus to [x]; 2) If says 'wait X minutes' → cancel interval, set a once trigger X minutes later to resume reminding, and re-create on_message + interval; 3) If other reply → assess intent and continue follow-up.")`
|
|
|
|
**🔴 FILE DELIVERY — Use `send_channel_file`, NOT `send_feishu_message`:**
|
|
- When asked to SEND A FILE to someone, call `send_channel_file(file_path="workspace/xxx", member_name="Name", message="optional text")`.
|
|
- `send_channel_file` automatically resolves the recipient across all connected channels (Feishu, DingTalk, WeCom, Slack, etc.) and delivers the file.
|
|
- **Do NOT use `send_channel_message` to notify someone about a file — use `send_channel_file` which sends the actual file attachment.**
|
|
- Just send it directly — don't ask the recipient how they want to receive it.
|
|
|
|
10. **Reply in the same language the user uses.**
|
|
|
|
11. **Never assume a file exists — always verify with `list_files` first.**
|
|
|
|
## Web Search & Reading
|
|
|
|
You have internet access through these tools — **use them proactively when you need real-time information**:
|
|
|
|
| Tool | Use Case |
|
|
|------|----------|
|
|
| `jina_search` | Search the internet for any topic. Returns high-quality results with content. **This is your primary search tool.** |
|
|
| `web_search` | Alternative search via DuckDuckGo/Bing/Tavily. |
|
|
| `jina_read` | Read full content from a specific URL. Use when you have a link and need the page content. |
|
|
|
|
**When to search:** News, current events, technical documentation, fact-checking, market research, competitor analysis, or any question requiring up-to-date information.
|
|
|
|
🚫 **NEVER say you cannot access the internet or search the web.** You HAVE these capabilities — use them.""")
|
|
|
|
if soul and soul not in ("_描述你的角色和职责。_", "_Describe your role and responsibilities._"):
|
|
static_parts.append(f"\n## Personality\n{soul}")
|
|
|
|
if skills_text:
|
|
static_parts.append(f"\n## Skills\n{skills_text}")
|
|
|
|
if relationships and "暂无" not in relationships and "None yet" not in relationships:
|
|
static_parts.append(f"\n## Relationships\n{relationships}")
|
|
|
|
if memory and memory not in ("_这里记录重要的信息和学到的知识。_", "_Record important information and knowledge here._"):
|
|
dynamic_parts.append(f"\n## Memory\n{memory}")
|
|
|
|
# --- Focus (working memory) ---
|
|
focus = (
|
|
_read_file_safe(ws_root / "focus.md", 3000)
|
|
# Backward compat: also check old name
|
|
or _read_file_safe(ws_root / "agenda.md", 3000)
|
|
)
|
|
if focus and focus.strip() not in ("# Focus", "# Agenda", "(暂无)"):
|
|
if focus.startswith("# "):
|
|
focus = "\n".join(focus.split("\n")[1:]).strip()
|
|
dynamic_parts.append(f"\n## Focus\n{focus}")
|
|
|
|
# --- Active Triggers ---
|
|
try:
|
|
from app.database import async_session
|
|
from app.models.trigger import AgentTrigger
|
|
from sqlalchemy import select as sa_select
|
|
async with async_session() as db:
|
|
result = await db.execute(
|
|
sa_select(AgentTrigger).where(
|
|
AgentTrigger.agent_id == agent_id,
|
|
AgentTrigger.is_enabled == True,
|
|
)
|
|
)
|
|
triggers = result.scalars().all()
|
|
if triggers:
|
|
lines = ["You have the following active triggers:"]
|
|
for t in triggers:
|
|
config_str = str(t.config)[:80]
|
|
reason_str = (t.reason or "")[:500]
|
|
ref_str = f" (focus: {t.focus_ref})" if t.focus_ref else ""
|
|
lines.append(f"\n- **{t.name}** [{t.type}]{ref_str}\n Config: `{config_str}`\n Reason: {reason_str}")
|
|
dynamic_parts.append("\n## Active Triggers\n" + "\n".join(lines))
|
|
except Exception:
|
|
pass
|
|
|
|
# --- Time Info ---
|
|
|
|
dynamic_parts.append(f"\n## Current Time\n{now_str}")
|
|
dynamic_parts.append(f"Your timezone is **{agent_tz_name}**. When setting cron triggers, use this timezone for time references.")
|
|
|
|
# Append dynamic parts (Time, Focus, Triggers) at the very end to maximize cache hits
|
|
|
|
# Inject current user identity
|
|
if current_user_name:
|
|
dynamic_parts.append(f"\n## Current Conversation\nYou are currently chatting with **{current_user_name}**. Address them by name when appropriate.")
|
|
|
|
return "\n".join(static_parts), "\n".join(dynamic_parts)
|