mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-11-04 14:52:12 +08:00
feat(plugins): consolidate official plugins with independent releases (LANG-180) (#2797)
Co-authored-by: multica-agent <[email protected]>
This commit is contained in:
co-authored by
multica-agent
parent
d099033d79
commit
3a77306c3c
@@ -0,0 +1,64 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from langbot_plugin.api.definition.components.tool.tool import Tool
|
||||
from langbot_plugin.api.entities.builtin.provider import session as provider_session
|
||||
from langbot_plugin.api.proxies.query_based_api import QueryBasedAPIProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Forget(Tool):
|
||||
async def call(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
session: provider_session.Session,
|
||||
query_id: int,
|
||||
) -> str:
|
||||
store = self.plugin.memory_store
|
||||
api = QueryBasedAPIProxy(
|
||||
query_id=query_id,
|
||||
plugin_runtime_handler=self.plugin.plugin_runtime_handler,
|
||||
)
|
||||
bot_uuid = await api.get_bot_uuid()
|
||||
query_vars = await api.get_query_vars()
|
||||
session_key, user_key, kb_id, _, config = await store.resolve_user_context(
|
||||
session, bot_uuid
|
||||
)
|
||||
|
||||
if not kb_id:
|
||||
return "Error: no memory knowledge base configured."
|
||||
|
||||
episode_id = params.get("episode_id", "")
|
||||
if not episode_id:
|
||||
return "Error: episode_id is required."
|
||||
|
||||
logger.info(
|
||||
"[LongTermMemory] forget called: query_id=%s kb_id=%s user_key=%s episode_id=%s",
|
||||
query_id,
|
||||
kb_id,
|
||||
user_key,
|
||||
episode_id,
|
||||
)
|
||||
|
||||
deleted = await store.delete_episode_by_id(
|
||||
collection_id=kb_id,
|
||||
episode_id=episode_id,
|
||||
user_key=user_key,
|
||||
)
|
||||
await store.append_audit_entry(
|
||||
scope_key=session_key,
|
||||
user_key=user_key,
|
||||
operation="forget",
|
||||
target_type="episode",
|
||||
target_id=str(episode_id),
|
||||
summary=f"Deleted episode {episode_id}",
|
||||
sender_id=str(query_vars.get("sender_id", "") or ""),
|
||||
sender_name=str(query_vars.get("sender_name", "") or ""),
|
||||
query_id=query_id,
|
||||
metadata={"kb_id": kb_id, "deleted": deleted},
|
||||
)
|
||||
|
||||
return f"Deleted episode {episode_id}."
|
||||
@@ -0,0 +1,36 @@
|
||||
apiVersion: v1
|
||||
kind: Tool
|
||||
metadata:
|
||||
name: forget
|
||||
label:
|
||||
en_US: Forget
|
||||
zh_Hans: 遗忘
|
||||
description:
|
||||
en_US: Delete a specific episodic memory by its ID.
|
||||
zh_Hans: 通过 ID 删除一条特定的情景记忆。
|
||||
|
||||
spec:
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
episode_id:
|
||||
type: string
|
||||
description: The ID of the episode to delete.
|
||||
required:
|
||||
- episode_id
|
||||
|
||||
llm_prompt: |
|
||||
Delete a specific episodic memory by its ID.
|
||||
Use this tool when the user explicitly asks to forget or remove a specific memory.
|
||||
The episode_id can be obtained from recall_memory search results or the !memory list / !memory search commands.
|
||||
|
||||
Only call this tool when the user clearly wants to delete a memory.
|
||||
Do NOT proactively delete memories without user consent.
|
||||
|
||||
Parameters:
|
||||
- episode_id (required): The ID of the episode to delete.
|
||||
|
||||
execution:
|
||||
python:
|
||||
path: forget.py
|
||||
attr: Forget
|
||||
@@ -0,0 +1,157 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from langbot_plugin.api.definition.components.tool.tool import Tool
|
||||
from langbot_plugin.api.entities.builtin.provider import session as provider_session
|
||||
from langbot_plugin.api.proxies.query_based_api import QueryBasedAPIProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RecallMemory(Tool):
|
||||
@staticmethod
|
||||
def _preview_text(value: str, max_len: int = 120) -> str:
|
||||
text = value.strip().replace("\n", " ")
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return f"{text[:max_len]}..."
|
||||
|
||||
async def call(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
session: provider_session.Session,
|
||||
query_id: int,
|
||||
) -> str:
|
||||
store = self.plugin.memory_store
|
||||
api = QueryBasedAPIProxy(
|
||||
query_id=query_id,
|
||||
plugin_runtime_handler=self.plugin.plugin_runtime_handler,
|
||||
)
|
||||
logger.info(
|
||||
"[LongTermMemory] recall_memory called: query_id=%s params_keys=%s",
|
||||
query_id,
|
||||
sorted(params.keys()),
|
||||
)
|
||||
bot_uuid = await api.get_bot_uuid()
|
||||
_, user_key, kb_id, _isolation, config = await store.resolve_user_context(
|
||||
session, bot_uuid
|
||||
)
|
||||
|
||||
if not kb_id:
|
||||
return "Error: no memory knowledge base configured. Create one first."
|
||||
|
||||
pipeline_kbs = await api.list_pipeline_knowledge_bases()
|
||||
if not any(kb.get("uuid") == kb_id for kb in pipeline_kbs):
|
||||
return "Error: memory knowledge base is not configured for the current pipeline."
|
||||
|
||||
embedding_model_uuid = config.get("embedding_model_uuid", "")
|
||||
if not embedding_model_uuid:
|
||||
return "Error: no embedding model configured in knowledge base."
|
||||
|
||||
query = params.get("query", "")
|
||||
if not isinstance(query, str) or not query.strip():
|
||||
return "Error: query is required."
|
||||
|
||||
top_k = params.get("top_k", 5)
|
||||
if not isinstance(top_k, int) or top_k <= 0:
|
||||
return "Error: top_k must be a positive integer."
|
||||
|
||||
speaker_id = params.get("speaker_id", "")
|
||||
if speaker_id is None:
|
||||
speaker_id = ""
|
||||
if not isinstance(speaker_id, str):
|
||||
return "Error: speaker_id must be a string."
|
||||
|
||||
speaker_name = params.get("speaker_name", "")
|
||||
if speaker_name is None:
|
||||
speaker_name = ""
|
||||
if not isinstance(speaker_name, str):
|
||||
return "Error: speaker_name must be a string."
|
||||
|
||||
time_after = params.get("time_after", "")
|
||||
if time_after is None:
|
||||
time_after = ""
|
||||
if not isinstance(time_after, str):
|
||||
return "Error: time_after must be a string."
|
||||
|
||||
time_before = params.get("time_before", "")
|
||||
if time_before is None:
|
||||
time_before = ""
|
||||
if not isinstance(time_before, str):
|
||||
return "Error: time_before must be a string."
|
||||
|
||||
source = params.get("source", "")
|
||||
if source is None:
|
||||
source = ""
|
||||
if not isinstance(source, str):
|
||||
return "Error: source must be a string."
|
||||
|
||||
status = params.get("status", "active")
|
||||
if status is None:
|
||||
status = "active"
|
||||
if not isinstance(status, str):
|
||||
return "Error: status must be a string."
|
||||
status = status.strip().lower() or "active"
|
||||
if status not in store.EPISODE_STATUSES:
|
||||
return "Error: status must be one of active, superseded, archived, or deleted."
|
||||
|
||||
importance_min = params.get("importance_min")
|
||||
if importance_min is not None and (
|
||||
not isinstance(importance_min, int) or importance_min < 1 or importance_min > 5
|
||||
):
|
||||
return "Error: importance_min must be an integer between 1 and 5."
|
||||
|
||||
logger.info(
|
||||
"[LongTermMemory] recall_memory searching: query_id=%s kb_id=%s user_key=%s top_k=%s speaker_id=%s speaker_name=%s source=%s status=%s importance_min=%s time_after=%s time_before=%s query_len=%s",
|
||||
query_id,
|
||||
kb_id,
|
||||
user_key,
|
||||
top_k,
|
||||
speaker_id.strip(),
|
||||
speaker_name.strip(),
|
||||
source.strip(),
|
||||
status,
|
||||
importance_min,
|
||||
time_after.strip(),
|
||||
time_before.strip(),
|
||||
len(query),
|
||||
)
|
||||
try:
|
||||
episodes = await store.search_episodes(
|
||||
collection_id=kb_id,
|
||||
embedding_model_uuid=embedding_model_uuid,
|
||||
query=query.strip(),
|
||||
user_key=user_key,
|
||||
top_k=top_k,
|
||||
sender_id=speaker_id.strip(),
|
||||
sender_name=speaker_name.strip(),
|
||||
time_after=time_after.strip(),
|
||||
time_before=time_before.strip(),
|
||||
importance_min=importance_min,
|
||||
source=source.strip(),
|
||||
include_statuses=[status],
|
||||
retrieval_strategy=config.get("retrieval_strategy", "auto"),
|
||||
vector_weight=config.get("vector_weight", 0.7),
|
||||
exact_match_boost=bool(config.get("exact_match_boost", True)),
|
||||
)
|
||||
except ValueError as exc:
|
||||
return f"Error: {exc}"
|
||||
|
||||
if not episodes:
|
||||
logger.info(
|
||||
"[LongTermMemory] recall_memory no results: query_id=%s kb_id=%s",
|
||||
query_id,
|
||||
kb_id,
|
||||
)
|
||||
return "No relevant memories found."
|
||||
|
||||
logger.info(
|
||||
"[LongTermMemory] recall_memory completed: query_id=%s kb_id=%s result_count=%s",
|
||||
query_id,
|
||||
kb_id,
|
||||
len(episodes),
|
||||
)
|
||||
return json.dumps(episodes, ensure_ascii=False)
|
||||
@@ -0,0 +1,83 @@
|
||||
apiVersion: v1
|
||||
kind: Tool
|
||||
metadata:
|
||||
name: recall_memory
|
||||
label:
|
||||
en_US: RecallMemory
|
||||
zh_Hans: 主动回忆
|
||||
description:
|
||||
en_US: Search long-term episodic memories with controlled filters such as speaker and time range.
|
||||
zh_Hans: 使用受控过滤条件(如说话人、时间范围)主动搜索长期情景记忆。
|
||||
|
||||
spec:
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
query:
|
||||
type: string
|
||||
description: Semantic query for the memory search.
|
||||
top_k:
|
||||
type: integer
|
||||
description: Number of memories to return. Optional, default is 5.
|
||||
speaker_id:
|
||||
type: string
|
||||
description: Optional speaker ID filter. Use when you need memories from a specific participant.
|
||||
speaker_name:
|
||||
type: string
|
||||
description: Optional speaker name filter. Use when the participant name is known.
|
||||
time_after:
|
||||
type: string
|
||||
description: Optional inclusive lower bound for timestamp filtering, preferably in ISO-8601 format.
|
||||
time_before:
|
||||
type: string
|
||||
description: Optional inclusive upper bound for timestamp filtering, preferably in ISO-8601 format.
|
||||
importance_min:
|
||||
type: integer
|
||||
description: Optional minimum importance from 1 to 5.
|
||||
source:
|
||||
type: string
|
||||
description: Optional source filter, for example "agent" or "import".
|
||||
status:
|
||||
type: string
|
||||
enum:
|
||||
- active
|
||||
- superseded
|
||||
- archived
|
||||
- deleted
|
||||
description: Optional lifecycle status filter. Default is "active"; only use other values when explicitly inspecting old or hidden memories.
|
||||
required:
|
||||
- query
|
||||
|
||||
llm_prompt: |
|
||||
Search long-term episodic memories when the default automatic memory recall is not sufficient.
|
||||
|
||||
Use this tool when you need to actively inspect memory with a specific retrieval intent, such as:
|
||||
- recalling what a specific person said
|
||||
- searching memories within a time range
|
||||
- focusing on more important memories
|
||||
- checking imported memories versus agent-written memories
|
||||
|
||||
Available controlled filters:
|
||||
- speaker_id
|
||||
- speaker_name
|
||||
- time_after
|
||||
- time_before
|
||||
- importance_min
|
||||
- source
|
||||
- status
|
||||
|
||||
Important behavior:
|
||||
- This is a read-only lookup tool. Do not call remember or update_profile just because recall returned no result.
|
||||
- These filters narrow results rather than broaden them.
|
||||
- By default, recall only returns active memories. Use status=superseded or status=archived only when the user asks to inspect older hidden records.
|
||||
- When multiple filters are provided together, they are typically combined as restrictive conditions.
|
||||
- Do not add filters unless they are clearly needed.
|
||||
- Start with only the semantic query whenever possible, then add at most one or two filters if you need to narrow the result set.
|
||||
- When you are investigating conflicting memories, pay attention to timestamps and explicit correction/update records. Prefer newer explicit corrections over older conflicting statements.
|
||||
|
||||
Do not invent raw metadata filters outside these parameters.
|
||||
|
||||
execution:
|
||||
python:
|
||||
path: recall_memory.py
|
||||
attr: RecallMemory
|
||||
@@ -0,0 +1,110 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from langbot_plugin.api.definition.components.tool.tool import Tool
|
||||
from langbot_plugin.api.entities.builtin.provider import session as provider_session
|
||||
from langbot_plugin.api.proxies.query_based_api import QueryBasedAPIProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Remember(Tool):
|
||||
@staticmethod
|
||||
def _preview_text(value: str, max_len: int = 120) -> str:
|
||||
text = value.strip().replace("\n", " ")
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return f"{text[:max_len]}..."
|
||||
|
||||
async def call(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
session: provider_session.Session,
|
||||
query_id: int,
|
||||
) -> str:
|
||||
store = self.plugin.memory_store
|
||||
api = QueryBasedAPIProxy(
|
||||
query_id=query_id,
|
||||
plugin_runtime_handler=self.plugin.plugin_runtime_handler,
|
||||
)
|
||||
logger.info(
|
||||
"[LongTermMemory] remember called: query_id=%s params_keys=%s",
|
||||
query_id,
|
||||
sorted(params.keys()),
|
||||
)
|
||||
bot_uuid = await api.get_bot_uuid()
|
||||
query_vars = await api.get_query_vars()
|
||||
session_key, user_key, kb_id, _, config = await store.resolve_user_context(
|
||||
session, bot_uuid
|
||||
)
|
||||
|
||||
if not kb_id:
|
||||
return "Error: no memory knowledge base configured. Create one first."
|
||||
|
||||
pipeline_kbs = await api.list_pipeline_knowledge_bases()
|
||||
if not any(kb.get("uuid") == kb_id for kb in pipeline_kbs):
|
||||
return "Error: memory knowledge base is not configured for the current pipeline."
|
||||
|
||||
embedding_model_uuid = config.get("embedding_model_uuid", "")
|
||||
if not embedding_model_uuid:
|
||||
return "Error: no embedding model configured in knowledge base."
|
||||
|
||||
content = params.get("content", "")
|
||||
if not content:
|
||||
return "Error: content is required."
|
||||
|
||||
tags = params.get("tags", [])
|
||||
importance = params.get("importance", 2)
|
||||
sender_id = str(query_vars.get("sender_id", "") or "")
|
||||
sender_name = str(query_vars.get("sender_name", "") or "")
|
||||
logger.info(
|
||||
"[LongTermMemory] remember storing episode: query_id=%s kb_id=%s user_key=%s sender_id=%s importance=%s tags=%s content_len=%s",
|
||||
query_id,
|
||||
kb_id,
|
||||
user_key,
|
||||
sender_id,
|
||||
importance,
|
||||
tags,
|
||||
len(str(content)),
|
||||
)
|
||||
|
||||
episode = await store.add_episode(
|
||||
collection_id=kb_id,
|
||||
embedding_model_uuid=embedding_model_uuid,
|
||||
user_key=user_key,
|
||||
content=content,
|
||||
tags=tags,
|
||||
importance=importance,
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
bot_uuid=bot_uuid,
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[LongTermMemory] remember stored: query_id=%s episode_id=%s user_key=%s content_len=%s",
|
||||
query_id,
|
||||
episode["id"],
|
||||
user_key,
|
||||
len(str(content)),
|
||||
)
|
||||
await store.append_audit_entry(
|
||||
scope_key=session_key,
|
||||
user_key=user_key,
|
||||
operation="remember",
|
||||
target_type="episode",
|
||||
target_id=episode["id"],
|
||||
summary=self._preview_text(str(content)),
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
query_id=query_id,
|
||||
metadata={
|
||||
"kb_id": kb_id,
|
||||
"tags": tags,
|
||||
"importance": importance,
|
||||
"status": episode.get("status", "active"),
|
||||
},
|
||||
)
|
||||
|
||||
return f"Remembered: {content}"
|
||||
@@ -0,0 +1,82 @@
|
||||
apiVersion: v1
|
||||
kind: Tool
|
||||
metadata:
|
||||
name: remember
|
||||
label:
|
||||
en_US: Remember
|
||||
zh_Hans: 记住
|
||||
description:
|
||||
en_US: Remember an event, plan, or situational fact for future conversations.
|
||||
zh_Hans: 记住一个事件、计划或情境信息,供未来对话使用。
|
||||
|
||||
spec:
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
description: The information to remember. Be specific and factual.
|
||||
tags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: Short category tags for this memory (e.g. "travel", "work", "schedule").
|
||||
importance:
|
||||
type: integer
|
||||
description: How important this memory is, from 1 (low) to 5 (critical). Default 2.
|
||||
minimum: 1
|
||||
maximum: 5
|
||||
required:
|
||||
- content
|
||||
|
||||
llm_prompt: |
|
||||
Store an episodic memory — events, plans, experiences, or situational facts.
|
||||
Episodic memory is L2: use it for history and future recall, not for the current stable profile.
|
||||
|
||||
Call this tool when the user mentions something that may be relevant later, such as:
|
||||
- Events and experiences ("I just got back from Tokyo", "had a fight with my boss")
|
||||
- Plans and schedules ("I'm traveling to Paris next month", "deadline is Friday")
|
||||
- Project context ("working on a migration from MySQL to PostgreSQL")
|
||||
- Situational facts ("my cat is sick", "just moved to a new apartment")
|
||||
- Group decisions ("The project group agreed to release on Friday")
|
||||
|
||||
IMPORTANT: Always include the speaker's name or identifier in the content.
|
||||
For example, write "Alice is traveling to Paris next month" instead of "traveling to Paris next month".
|
||||
This ensures memories are correctly attributed when multiple people share the same conversation.
|
||||
|
||||
Do NOT use this for stable identity information — use "update_profile" instead:
|
||||
- Name, job title, personality traits → update_profile
|
||||
- Communication preferences → update_profile
|
||||
|
||||
Do NOT store:
|
||||
- one-off greetings, jokes, or small talk
|
||||
- secrets, credentials, API keys, one-time codes, or private tokens
|
||||
- unconfirmed sensitive information or third-party private gossip
|
||||
- harmful inferred labels about a person
|
||||
- facts that are only useful for the current immediate answer
|
||||
|
||||
If the user explicitly corrects, retracts, or revises an earlier statement:
|
||||
- store the correction as an episodic memory with clear wording
|
||||
- describe the new statement as a correction, not as an unrelated new fact
|
||||
- when appropriate, add tags such as ["correction"], ["profile-update"], or ["preference-change"]
|
||||
- if the correction changes the user's current stable profile, also update that profile through "update_profile"
|
||||
- when updating a stable preference or trait that may change later, prefer giving update_profile a reusable fact_key such as "coffee_preference"
|
||||
- if the correction explicitly replaces an earlier stable fact, and the earlier value is known, also pass it as previous_value to update_profile
|
||||
|
||||
Example correction memory:
|
||||
- "Alice corrected a previous preference statement and said she actually does not like coffee."
|
||||
|
||||
Examples:
|
||||
- Private chat L2: "Alice said her flight to Tokyo is next Tuesday."
|
||||
- Group chat L2: "In the design group, Bob decided to own the database migration this week."
|
||||
- Not L2: "Alice prefers concise answers." Use update_profile instead.
|
||||
|
||||
Parameters:
|
||||
- content (required): What to remember. Be concise and factual. Always attribute to the speaker.
|
||||
- tags (optional): Category tags like ["travel", "work"].
|
||||
- importance (optional): 1-5 scale, default 2. Use 4-5 for critical facts.
|
||||
|
||||
execution:
|
||||
python:
|
||||
path: remember.py
|
||||
attr: Remember
|
||||
@@ -0,0 +1,182 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from langbot_plugin.api.definition.components.tool.tool import Tool
|
||||
from langbot_plugin.api.entities.builtin.provider import session as provider_session
|
||||
from langbot_plugin.api.proxies.query_based_api import QueryBasedAPIProxy
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class UpdateProfile(Tool):
|
||||
@staticmethod
|
||||
def _preview_text(value: str, max_len: int = 80) -> str:
|
||||
text = value.strip().replace("\n", " ")
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return f"{text[:max_len]}..."
|
||||
|
||||
@staticmethod
|
||||
def _normalize_scope(scope: Any) -> str:
|
||||
if scope is None or scope == "":
|
||||
return ""
|
||||
if not isinstance(scope, str):
|
||||
return "__invalid__"
|
||||
scope = scope.strip().lower()
|
||||
if scope in ("session", "speaker"):
|
||||
return scope
|
||||
return "__invalid__"
|
||||
|
||||
@staticmethod
|
||||
def _infer_scope(field: str, explicit_scope: str) -> str:
|
||||
if explicit_scope:
|
||||
return explicit_scope
|
||||
# Default to speaker-scoped profiles for stable person facts,
|
||||
# and session-scoped notes for shared conversational context.
|
||||
if field in ("name", "traits", "preferences"):
|
||||
return "speaker"
|
||||
return "session"
|
||||
|
||||
@staticmethod
|
||||
def _normalize_fact_key(value: Any) -> str:
|
||||
if value is None or value == "":
|
||||
return ""
|
||||
if not isinstance(value, str):
|
||||
return "__invalid__"
|
||||
return " ".join(value.strip().split())
|
||||
|
||||
async def call(
|
||||
self,
|
||||
params: dict[str, Any],
|
||||
session: provider_session.Session,
|
||||
query_id: int,
|
||||
) -> str:
|
||||
store = self.plugin.memory_store
|
||||
|
||||
field = params.get("field", "")
|
||||
action = params.get("action", "")
|
||||
value = params.get("value", "")
|
||||
scope = self._normalize_scope(params.get("scope", ""))
|
||||
fact_key = self._normalize_fact_key(params.get("fact_key", ""))
|
||||
previous_value = params.get("previous_value", "")
|
||||
logger.info(
|
||||
"[LongTermMemory] update_profile called: query_id=%s field=%s action=%s scope=%s fact_key=%s has_previous_value=%s",
|
||||
query_id,
|
||||
field,
|
||||
action,
|
||||
scope,
|
||||
fact_key,
|
||||
bool(previous_value),
|
||||
)
|
||||
|
||||
if not all([field, action, value]):
|
||||
return "Error: field, action, and value are all required."
|
||||
|
||||
if field not in ("name", "traits", "preferences", "notes"):
|
||||
return f"Error: invalid field '{field}'."
|
||||
|
||||
if action not in ("set", "add", "remove"):
|
||||
return f"Error: invalid action '{action}'."
|
||||
|
||||
if scope == "__invalid__":
|
||||
return "Error: invalid scope. Use 'session' or 'speaker'."
|
||||
|
||||
if fact_key == "__invalid__":
|
||||
return "Error: fact_key must be a string."
|
||||
|
||||
if fact_key and field not in ("traits", "preferences"):
|
||||
return "Error: fact_key is only supported for 'traits' and 'preferences'."
|
||||
|
||||
if previous_value is None:
|
||||
previous_value = ""
|
||||
if not isinstance(previous_value, str):
|
||||
return "Error: previous_value must be a string."
|
||||
|
||||
api = QueryBasedAPIProxy(
|
||||
query_id=query_id,
|
||||
plugin_runtime_handler=self.plugin.plugin_runtime_handler,
|
||||
)
|
||||
bot_uuid = await api.get_bot_uuid()
|
||||
query_vars = await api.get_query_vars()
|
||||
sender_id = str(query_vars.get("sender_id", "") or "")
|
||||
sender_name = str(query_vars.get("sender_name", "") or "")
|
||||
|
||||
session_key, user_key, kb_id, _isolation, _ = await store.resolve_user_context(
|
||||
session, bot_uuid
|
||||
)
|
||||
if not kb_id:
|
||||
return "Error: no memory knowledge base configured. Create one first."
|
||||
|
||||
pipeline_kbs = await api.list_pipeline_knowledge_bases()
|
||||
if not any(kb.get("uuid") == kb_id for kb in pipeline_kbs):
|
||||
return "Error: memory knowledge base is not configured for the current pipeline."
|
||||
|
||||
target_scope = self._infer_scope(field, scope)
|
||||
logger.info(
|
||||
"[LongTermMemory] update_profile resolved scope: query_id=%s target_scope=%s session_key=%s sender_id=%s value_len=%s",
|
||||
query_id,
|
||||
target_scope,
|
||||
session_key,
|
||||
sender_id,
|
||||
len(str(value)),
|
||||
)
|
||||
|
||||
if target_scope == "speaker":
|
||||
if not sender_id:
|
||||
return "Error: current speaker is unavailable."
|
||||
profile = await store.update_speaker_profile_field(
|
||||
scope_key=session_key,
|
||||
sender_id=sender_id,
|
||||
field=field,
|
||||
action=action,
|
||||
value=value,
|
||||
fact_key=fact_key,
|
||||
previous_value=previous_value.strip(),
|
||||
)
|
||||
else:
|
||||
profile = await store.update_session_profile_field(
|
||||
scope_key=session_key,
|
||||
field=field,
|
||||
action=action,
|
||||
value=value,
|
||||
fact_key=fact_key,
|
||||
previous_value=previous_value.strip(),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"[LongTermMemory] update_profile stored: query_id=%s target_scope=%s field=%s action=%s session_key=%s",
|
||||
query_id,
|
||||
target_scope,
|
||||
field,
|
||||
action,
|
||||
session_key,
|
||||
)
|
||||
target_id = session_key if target_scope == "session" else f"{session_key}:{sender_id}"
|
||||
await store.append_audit_entry(
|
||||
scope_key=session_key,
|
||||
user_key=user_key,
|
||||
operation="update_profile",
|
||||
target_type=f"{target_scope}_profile",
|
||||
target_id=target_id,
|
||||
summary=(
|
||||
f"{action} {target_scope} profile {field}"
|
||||
f"{f' ({fact_key})' if fact_key else ''}: {self._preview_text(str(value))}"
|
||||
),
|
||||
sender_id=sender_id,
|
||||
sender_name=sender_name,
|
||||
query_id=query_id,
|
||||
metadata={
|
||||
"field": field,
|
||||
"action": action,
|
||||
"target_scope": target_scope,
|
||||
"fact_key": fact_key,
|
||||
"has_previous_value": bool(previous_value),
|
||||
},
|
||||
)
|
||||
|
||||
return (
|
||||
f"{target_scope.capitalize()} profile updated.\n"
|
||||
+ store.format_profile_text(profile)
|
||||
)
|
||||
@@ -0,0 +1,104 @@
|
||||
apiVersion: v1
|
||||
kind: Tool
|
||||
metadata:
|
||||
name: update_profile
|
||||
label:
|
||||
en_US: UpdateProfile
|
||||
zh_Hans: 更新画像
|
||||
description:
|
||||
en_US: Update the session's core profile information.
|
||||
zh_Hans: 更新当前会话的核心画像信息。
|
||||
|
||||
spec:
|
||||
parameters:
|
||||
type: object
|
||||
properties:
|
||||
scope:
|
||||
type: string
|
||||
enum:
|
||||
- session
|
||||
- speaker
|
||||
description: "session: shared profile for the whole chat. speaker: profile for the current speaker."
|
||||
field:
|
||||
type: string
|
||||
enum:
|
||||
- name
|
||||
- traits
|
||||
- preferences
|
||||
- notes
|
||||
description: Which profile field to update.
|
||||
action:
|
||||
type: string
|
||||
enum:
|
||||
- set
|
||||
- add
|
||||
- remove
|
||||
description: "set: replace the value. add: append to the list. remove: remove from the list."
|
||||
value:
|
||||
type: string
|
||||
description: The value to set/add/remove.
|
||||
fact_key:
|
||||
type: string
|
||||
description: Optional logical slot key for traits/preferences, such as "coffee_preference", "occupation", or "reply_language". Use this when the fact may be corrected later.
|
||||
previous_value:
|
||||
type: string
|
||||
description: Optional previous value for the same fact when the user is explicitly correcting or revising it. Useful for preserving history when moving from an old fact to a new one.
|
||||
required:
|
||||
- field
|
||||
- action
|
||||
- value
|
||||
|
||||
llm_prompt: |
|
||||
Update the session profile — shared context about this conversation (group chat or private chat).
|
||||
Profile memory is L1: use it for stable, low-frequency, currently valid facts.
|
||||
This tool can update either:
|
||||
- the shared session profile for the whole chat
|
||||
- the current speaker's profile for person-specific stable facts
|
||||
|
||||
Fields:
|
||||
- name: A label for this session, or a participant's preferred name.
|
||||
- traits: Key stable facts about a participant or the session.
|
||||
- preferences: Communication preferences for the current speaker or the whole session.
|
||||
- notes: Free-form notes, usually shared session context.
|
||||
|
||||
fact_key:
|
||||
- Optional for traits/preferences.
|
||||
- Use a stable logical key when the same fact may change over time.
|
||||
- Good examples: "coffee_preference", "occupation", "reply_language", "timezone".
|
||||
- Reusing the same fact_key lets the system keep the current value and preserve previous values as history.
|
||||
|
||||
previous_value:
|
||||
- Optional, mainly for correction scenarios.
|
||||
- Use it when the user explicitly replaces an earlier stable fact and you know the old value.
|
||||
- Example: fact_key="coffee_preference", previous_value="Alice likes coffee", value="Alice does not like coffee".
|
||||
|
||||
Scopes:
|
||||
- speaker: Use for stable facts about the current speaker, such as "Alice likes coffee" or "Alice is a software engineer".
|
||||
- session: Use for shared context about the whole chat, such as "group for project X" or "respond in Chinese here".
|
||||
|
||||
Actions:
|
||||
- set: Replace the field value entirely.
|
||||
- add: Add an item to a list field (traits/preferences) or append to notes.
|
||||
- remove: Remove an item from a list field or clear notes.
|
||||
|
||||
Important:
|
||||
- When adding facts about a person in a group chat, prefer scope="speaker".
|
||||
- When adding shared group context, use scope="session".
|
||||
- When adding traits about specific people, include their name (e.g. "Alice likes coffee" not just "likes coffee").
|
||||
- Do not write short-lived plans, deadlines, recent events, or one-off states into L1; use remember for those.
|
||||
- Do not store secrets, credentials, one-time codes, unconfirmed sensitive claims, or harmful inferred labels.
|
||||
- For traits/preferences that may later be corrected, prefer providing fact_key so the plugin can keep one current value plus history.
|
||||
- When a user corrects, retracts, or negates a previously stored stable fact, do NOT keep contradictory current profile values in parallel.
|
||||
- Prefer replacing the old current value. In practice, use the same fact_key with action="set" for the corrected current fact. If the old value is known, also provide previous_value so it is preserved as history.
|
||||
- If the correction itself is historically important, also store it with the "remember" tool so the timeline is preserved.
|
||||
|
||||
Examples:
|
||||
- Private chat L1 speaker profile: "Alice prefers concise technical answers."
|
||||
- Group chat L1 session profile: "This group coordinates the LangBot release."
|
||||
- Correction: if Alice says "Actually I prefer English replies, not Chinese", set the same reply_language fact_key to the new current value and pass the old value as previous_value if known.
|
||||
- Not L1: "Alice has a meeting tomorrow at 10." Use remember instead.
|
||||
|
||||
execution:
|
||||
python:
|
||||
path: update_profile.py
|
||||
attr: UpdateProfile
|
||||
Reference in New Issue
Block a user