-
Notifications
You must be signed in to change notification settings - Fork 581
Expand file tree
/
Copy pathagent_context.py
More file actions
569 lines (471 loc) · 28.4 KB
/
agent_context.py
File metadata and controls
569 lines (471 loc) · 28.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
"""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
from sqlalchemy import select as _feishu_select
async with _ctx_session() as _ctx_db:
_cfg_r = await _ctx_db.execute(
_feishu_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 app.models.agent import Agent as _AgentModel
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)