Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion bot/demo/werewolf/start_werewolf_demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,11 @@ def prepare_workspace(workspace: Path) -> None:


def validate_assets() -> None:
missing = [str(path) for path in (SOUL_GOD_PATH, SOUL_PLAYER_PATH, WEREWOLF_SERVER_PATH) if not path.exists()]
missing = [
str(path)
for path in (SOUL_GOD_PATH, SOUL_PLAYER_PATH, WEREWOLF_SERVER_PATH)
if not path.exists()
]
if missing:
raise FileNotFoundError("Missing required demo files: " + ", ".join(missing))

Expand Down
1 change: 0 additions & 1 deletion bot/tests/test_chat_functionality.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,6 @@ def test_non_deepseek_history_omits_reasoning_content(self):


class TestChatChannel:

def test_chat_channel_initialization(self, message_bus, temp_workspace):
"""Test that ChatChannel can be initialized correctly."""
config = ChatChannelConfig()
Expand Down
5 changes: 4 additions & 1 deletion bot/vikingbot/agent/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,10 @@ async def _build_user_memory(
# Use provided memory_users or fall back to [sender_id]
search_user_ids = memory_users if memory_users else [sender_id]
viking_memory = await self.memory.get_viking_memory_context(
current_message=current_message, workspace_id=workspace_id, sender_id=sender_id, user_ids=search_user_ids
current_message=current_message,
workspace_id=workspace_id,
sender_id=sender_id,
user_ids=search_user_ids,
)
logger.info(f"viking_memory={viking_memory}")
cost = round(_time.time() - start, 2)
Expand Down
11 changes: 6 additions & 5 deletions bot/vikingbot/agent/memory.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,7 @@ def get_uri(m):
def get_abstract(m):
return m.get("abstract", "") if isinstance(m, dict) else getattr(m, "abstract", "")

filtered_memories = [
memory for memory in result if get_score(memory) >= min_score
]
filtered_memories = [memory for memory in result if get_score(memory) >= min_score]
filtered_memories.sort(key=get_score, reverse=True)

user_memories = []
Expand Down Expand Up @@ -162,7 +160,10 @@ async def get_viking_memory_context(
if not client:
return ""
result = await client.search_memory(
query=current_message, user_ids=search_user_ids, agent_user_id=admin_user_id, limit=30
query=current_message,
user_ids=search_user_ids,
agent_user_id=admin_user_id,
limit=30,
)
if not result:
return ""
Expand Down Expand Up @@ -273,4 +274,4 @@ async def fetch_profile(user_id: str) -> tuple[str, str]:
try:
await client.close()
except Exception as e:
logger.warning(f"Error closing VikingClient: {e}")
logger.warning(f"Error closing VikingClient: {e}")
5 changes: 2 additions & 3 deletions bot/vikingbot/agent/tools/mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,7 @@ def _normalize_schema_for_openai(schema: Any) -> dict[str, Any]:

if "properties" in normalized and isinstance(normalized["properties"], dict):
normalized["properties"] = {
name: _normalize_schema_for_openai(prop)
if isinstance(prop, dict)
else prop
name: _normalize_schema_for_openai(prop) if isinstance(prop, dict) else prop
for name, prop in normalized["properties"].items()
}

Expand Down Expand Up @@ -174,6 +172,7 @@ async def connect_mcp_servers(
)
read, write = await stack.enter_async_context(stdio_client(params))
elif transport_type == "sse":

def httpx_client_factory(
headers: dict[str, str] | None = None,
timeout: httpx.Timeout | None = None,
Expand Down
39 changes: 28 additions & 11 deletions bot/vikingbot/agent/tools/ov_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ async def _get_client(self, tool_context: ToolContext):
self._client = await VikingClient.create(tool_context.workspace_id)
return self._client


class VikingListTool(OVFileTool):
"""Tool to list Viking resources."""

Expand Down Expand Up @@ -86,9 +87,11 @@ def name(self) -> str:

@property
def description(self) -> str:
return ("Using query to search for resources (knowledge, code, files, workflow, etc.) in OpenViking. "
"This operation performs semantic retrieval, not full character matching. Please avoid repeated calls with similar queries as much as possible."
"bad-case: after searching with ‘Nate Joanna dog playdate 3:00 pm', another search was performed using 'Nate Joanna dog playdate'.")
return (
"Using query to search for resources (knowledge, code, files, workflow, etc.) in OpenViking. "
"This operation performs semantic retrieval, not full character matching. Please avoid repeated calls with similar queries as much as possible."
"bad-case: after searching with ‘Nate Joanna dog playdate 3:00 pm', another search was performed using 'Nate Joanna dog playdate'."
)

@property
def parameters(self) -> dict[str, Any]:
Expand Down Expand Up @@ -128,7 +131,11 @@ def _extract_search_items(results: Any) -> list[dict[str, Any]]:
items.append({**item, "type": item.get("type", item_type)})
return items

if hasattr(results, "memories") or hasattr(results, "resources") or hasattr(results, "skills"):
if (
hasattr(results, "memories")
or hasattr(results, "resources")
or hasattr(results, "skills")
):
for key, item_type in group_map.items():
for item in getattr(results, key, []) or []:
items.append(
Expand Down Expand Up @@ -178,7 +185,9 @@ def _to_float(value: Any) -> float:
except (TypeError, ValueError):
return 0.0

def _filter_search_items(self, results: Any, min_score: float) -> dict[str, list[dict[str, Any]]]:
def _filter_search_items(
self, results: Any, min_score: float
) -> dict[str, list[dict[str, Any]]]:
grouped: dict[str, list[dict[str, Any]]] = {
"memory": [],
"resource": [],
Expand Down Expand Up @@ -216,7 +225,9 @@ def _build_group_json(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
)
return group_items

def _format_search_items_json(self, grouped_items: dict[str, list[dict[str, Any]]], min_score: float) -> str:
def _format_search_items_json(
self, grouped_items: dict[str, list[dict[str, Any]]], min_score: float
) -> str:
memories = self._build_group_json(grouped_items.get("memory", []))
resources = self._build_group_json(grouped_items.get("resource", []))
skills = self._build_group_json(grouped_items.get("skill", []))
Expand Down Expand Up @@ -348,8 +359,10 @@ def name(self) -> str:

@property
def description(self) -> str:
return ("Search Viking resources using regex patterns (like grep). Supports multiple patterns to search concurrently."
"Please avoid repeated calls with similar queries as much as possible.")
return (
"Search Viking resources using regex patterns (like grep). Supports multiple patterns to search concurrently."
"Please avoid repeated calls with similar queries as much as possible."
)

@property
def parameters(self) -> dict[str, Any]:
Expand Down Expand Up @@ -433,7 +446,9 @@ async def run_grep(p: str) -> tuple[str, list[Any]]:
return f"No matches found for patterns: {pattern_str}"

# Format output
result_lines = [f"Found {total_matches} match{'es' if total_matches != 1 else ''} across {len(patterns)} pattern{'s' if len(patterns) != 1 else ''}:"]
result_lines = [
f"Found {total_matches} match{'es' if total_matches != 1 else ''} across {len(patterns)} pattern{'s' if len(patterns) != 1 else ''}:"
]

for match_uri, matches in merged_results.items():
# Sort matches by line number
Expand Down Expand Up @@ -504,6 +519,7 @@ async def execute(
except Exception as e:
return f"Error searching Viking with glob: {str(e)}"


class VikingMemoryCommitTool(OVFileTool):
"""Tool to commit messages to OpenViking session."""

Expand Down Expand Up @@ -553,6 +569,7 @@ async def execute(
logger.exception(f"Error processing message: {e}")
return f"Error committing to Viking: {str(e)}"


class VikingMultiReadTool(OVFileTool):
"""Tool to read content from multiple Viking resources concurrently."""

Expand All @@ -572,7 +589,7 @@ def parameters(self) -> dict[str, Any]:
"uris": {
"type": "array",
"items": {"type": "string"},
"description": "List of Viking file URIs to read from (e.g., [\"viking://resources/path/123.md\", \"viking://resources/path/456.md\"])",
"description": 'List of Viking file URIs to read from (e.g., ["viking://resources/path/123.md", "viking://resources/path/456.md"])',
},
},
"required": ["uris"],
Expand Down Expand Up @@ -633,4 +650,4 @@ async def read_single_uri(uri: str) -> dict:

except Exception as e:
logger.exception(f"Error in VikingMultiReadTool: {e}")
return f"Error multi-reading Viking resources: {str(e)}"
return f"Error multi-reading Viking resources: {str(e)}"
23 changes: 18 additions & 5 deletions bot/vikingbot/channels/feishu.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,9 @@ def __init__(self, config: FeishuChannelConfig, bus: MessageBus, **kwargs):
self._chat_mode_cache: dict[str, str] = {} # 缓存群类型:group(普通群)/thread(话题群)
self._user_name_cache: OrderedDict[str, str] = OrderedDict() # LRU缓存用户ID到姓名的映射
self._bot_name_cache: dict[str, str] = {} # 缓存机器人open_id到名称的映射
self._chat_member_cache: OrderedDict[str, dict[str, Any]] = OrderedDict() # chat_id -> {members, expires_at, last_error_at}
self._chat_member_cache: OrderedDict[str, dict[str, Any]] = (
OrderedDict()
) # chat_id -> {members, expires_at, last_error_at}
self._MAX_USER_CACHE_SIZE = 1000 # 最大缓存1000个用户
self._CHAT_MEMBER_CACHE_TTL_SEC = 300
self._CHAT_MEMBER_CACHE_MAX_CHATS = 30
Expand Down Expand Up @@ -801,13 +803,19 @@ def _get_cached_user_name(self, open_id: str) -> str | None:
self._user_name_cache[open_id] = name
return name

def _save_chat_member_cache(self, chat_id: str, members: dict[str, str], last_error_at: float = 0) -> None:
def _save_chat_member_cache(
self, chat_id: str, members: dict[str, str], last_error_at: float = 0
) -> None:
if chat_id in self._chat_member_cache:
self._chat_member_cache.pop(chat_id)
elif len(self._chat_member_cache) >= self._CHAT_MEMBER_CACHE_MAX_CHATS:
self._chat_member_cache.popitem(last=False)

ttl = self._CHAT_MEMBER_FETCH_COOLDOWN_SEC if last_error_at else self._CHAT_MEMBER_CACHE_TTL_SEC
ttl = (
self._CHAT_MEMBER_FETCH_COOLDOWN_SEC
if last_error_at
else self._CHAT_MEMBER_CACHE_TTL_SEC
)
self._chat_member_cache[chat_id] = {
"members": members,
"expires_at": time.time() + ttl,
Expand Down Expand Up @@ -862,7 +870,10 @@ async def _get_group_member_name(self, chat_id: str, open_id: str) -> str | None
members = entry.get("members", {})
if entry.get("expires_at", 0) > now:
return members.get(open_id)
if now - float(entry.get("last_error_at", 0) or 0) < self._CHAT_MEMBER_FETCH_COOLDOWN_SEC:
if (
now - float(entry.get("last_error_at", 0) or 0)
< self._CHAT_MEMBER_FETCH_COOLDOWN_SEC
):
return members.get(open_id)

try:
Expand Down Expand Up @@ -924,7 +935,9 @@ async def _get_bot_name(self, open_id: str) -> str | None:
self._bot_name_cache[open_id] = bot_name
return bot_name

async def _batch_get_user_names(self, open_ids: list[str], chat_id: str | None = None) -> dict[str, str]:
async def _batch_get_user_names(
self, open_ids: list[str], chat_id: str | None = None
) -> dict[str, str]:
"""
Get user names from Feishu API by open_ids (fetches individually with LRU cache).
Returns a dict mapping open_id to user name.
Expand Down
14 changes: 4 additions & 10 deletions bot/vikingbot/channels/feishu_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,19 @@

def main():
# 创建client
client = lark.Client.builder() \
.app_id("") \
.app_secret("") \
.log_level(lark.LogLevel.DEBUG) \
.build()
client = lark.Client.builder().app_id("").app_secret("").log_level(lark.LogLevel.DEBUG).build()

# 构造请求对象
request: GetUserRequest = GetUserRequest.builder() \
.user_id("") \
.user_id_type("open_id") \
.build()
request: GetUserRequest = GetUserRequest.builder().user_id("").user_id_type("open_id").build()

# 发起请求
response: GetUserResponse = client.contact.v3.user.get(request)

# 处理失败返回
if not response.success():
lark.logger.error(
f"client.contact.v3.user.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}")
f"client.contact.v3.user.get failed, code: {response.code}, msg: {response.msg}, log_id: {response.get_log_id()}, resp: \n{json.dumps(json.loads(response.raw.content), indent=4, ensure_ascii=False)}"
)
return

# 处理业务结果
Expand Down
2 changes: 1 addition & 1 deletion bot/vikingbot/channels/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,7 @@ async def _dispatch_outbound(self) -> None:
for ch in self.channels.values():
# Check if channel has a send method and can handle bot_api messages
# OpenAPIChannel identifies itself with name "openapi" and handles bot_api
if hasattr(ch, 'name') and ch.name == "openapi":
if hasattr(ch, "name") and ch.name == "openapi":
try:
await ch.send(msg)
handled = True
Expand Down
13 changes: 11 additions & 2 deletions bot/vikingbot/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ def _get_gateway_token(config) -> str:
return ""
return getattr(gateway, "token", "") or ""


# ---------------------------------------------------------------------------
# CLI input: prompt_toolkit for editing, paste, history, and display
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -333,7 +334,13 @@ async def run():
)
server = uvicorn.Server(config_uvicorn)

tasks = [cron.start(), heartbeat.start(), channels.start_all(), agent_loop.run(), server.serve()]
tasks = [
cron.start(),
heartbeat.start(),
channels.start_all(),
agent_loop.run(),
server.serve(),
]
# if enable_console:
# tasks.append(start_console(console_port))

Expand Down Expand Up @@ -660,7 +667,9 @@ def chat(
if session_id is None:
session_id = get_or_create_machine_id()
cron = prepare_cron(bus, quiet=is_single_turn)
channels = prepare_agent_channel(config, bus, message, session_id, markdown, logs, eval, sender, memory_user)
channels = prepare_agent_channel(
config, bus, message, session_id, markdown, logs, eval, sender, memory_user
)
agent_loop = prepare_agent_loop(
config, bus, session_manager, cron, quiet=is_single_turn, eval=eval
)
Expand Down
9 changes: 5 additions & 4 deletions bot/vikingbot/heartbeat/service.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ class HeartbeatService:
def __init__(
self,
workspace: Path,
on_heartbeat: Callable[[str, str | None, dict[str, Any] | None], Coroutine[Any, Any, str]] | None = None,
on_heartbeat: Callable[[str, str | None, dict[str, Any] | None], Coroutine[Any, Any, str]]
| None = None,
interval_s: int = DEFAULT_HEARTBEAT_INTERVAL_S,
enabled: bool = True,
sandbox_mode: str = "shared",
Expand All @@ -108,9 +109,9 @@ def __init__(

def _is_session_stale(self, session_info: dict[str, Any]) -> bool:
"""Check whether a session has been inactive for too long."""
reference = _parse_session_timestamp(session_info.get("updated_at")) or _parse_session_timestamp(
session_info.get("created_at")
)
reference = _parse_session_timestamp(
session_info.get("updated_at")
) or _parse_session_timestamp(session_info.get("created_at"))
if reference is None:
return False

Expand Down
11 changes: 7 additions & 4 deletions bot/vikingbot/hooks/builtins/openviking_hooks.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ async def _get_client(self, workspace_id: str) -> VikingClient:
# Use global singleton client
return await get_global_client()


async def execute(self, context: HookContext, **kwargs) -> Any:
vikingbot_session: Session = kwargs.get("session", {})
session_id = context.session_key.safe_name()
Expand All @@ -51,7 +50,9 @@ async def execute(self, context: HookContext, **kwargs) -> Any:
client = await self._get_client(context.workspace_id)

# 1. 提交全部的 message 到 admin
admin_result = await client.commit(session_id, vikingbot_session.messages, admin_user_id)
admin_result = await client.commit(
session_id, vikingbot_session.messages, admin_user_id
)

# 2. 根据 message 里的 sender_id 进行分组
messages_by_sender = defaultdict(list)
Expand All @@ -68,7 +69,9 @@ async def execute(self, context: HookContext, **kwargs) -> Any:

async def commit_with_semaphore(user_id: str, user_messages: list):
async with semaphore:
return await client.commit(f"{session_id}_{user_id}", user_messages, user_id)
return await client.commit(
f"{session_id}_{user_id}", user_messages, user_id
)

user_tasks = []
for user_id, user_messages in messages_by_sender.items():
Expand All @@ -82,7 +85,7 @@ async def commit_with_semaphore(user_id: str, user_messages: list):
"success": True,
"admin_result": admin_result,
"user_results": user_results,
"users_count": len(messages_by_sender)
"users_count": len(messages_by_sender),
}
except Exception as e:
logger.exception(f"Failed to add message to OpenViking: {e}")
Expand Down
1 change: 0 additions & 1 deletion bot/vikingbot/openviking_mount/ov_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -488,7 +488,6 @@ async def main_test():


async def account_test():

client = ov.AsyncHTTPClient(
url="http://localhost:1933",
api_key="",
Expand Down
Loading
Loading