mirror of
https://github.com/hansjone/oclaw.git
synced 2026-10-09 03:13:19 +08:00
本次迁移将网关/通道/工具/技能/脚本与协议资源集中到新结构,统一路径常量与脚本转发机制,减少顶层噪音并保证运行与测试行为一致。 Made-with: Cursor
450 lines
16 KiB
Python
450 lines
16 KiB
Python
"""根据存储与配置构建 Agent(不依赖 Streamlit)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import os
|
||
from typing import Any
|
||
|
||
from oclaw.runtime.agents.agent_scope import resolve_default_agent_id
|
||
from oclaw.runtime.agents.network_ops_agent import NetworkOpsAgent
|
||
from oclaw.runtime.agents.specialist_agent import SpecialistProfile
|
||
from oclaw.runtime.agents.specialists import (
|
||
AGENT_PROFILE_BINDINGS_KEY,
|
||
AGENT_ROLE_IDS,
|
||
MANAGER_AGENT_ID,
|
||
SPECIALIST_IDS,
|
||
default_system_prefix_for_specialist,
|
||
default_tool_tags_for_specialist,
|
||
dump_agent_profile_bindings,
|
||
expert_name_for_specialist,
|
||
parse_agent_profile_bindings,
|
||
)
|
||
from oclaw.runtime.chat.agent import Agent
|
||
from oclaw.runtime.orchestration.inventory import inventory_snapshot
|
||
from oclaw.runtime.orchestration.memory import upsert_knowledge_chunks
|
||
from oclaw.platform.llm.chat_models import GoogleGeminiChatModel, OpenAIChatModel, RuleBasedChatModel, StaticTextChatModel
|
||
from oclaw.platform.llm.transports.anthropic_messages import AnthropicMessagesModel
|
||
from oclaw.platform.llm.transports.openai_responses import OpenAIResponsesModel
|
||
from oclaw.platform.persistence.sqlite_store import (
|
||
SqliteStore,
|
||
active_llm_profile_setting_key,
|
||
agent_profile_bindings_setting_key,
|
||
is_administrator_model_pool,
|
||
)
|
||
from oclaw.prompts import render_prompt
|
||
from oclaw.runtime.tools.catalog import default_registry
|
||
from oclaw.runtime.tools.plugin_loader import sync_plugin_metadata
|
||
|
||
|
||
def _openai_missing_key_user_message(lang: str) -> str:
|
||
prompt_id = "fallback/openai_missing_key_user.en.md" if (lang or "zh").startswith("en") else "fallback/openai_missing_key_user.zh.md"
|
||
return render_prompt(prompt_id, strict=True)
|
||
|
||
|
||
DEFAULT_OLLAMA_BASE_URL = (
|
||
(os.getenv("OLLAMA_BASE_URL") or os.getenv("OPENAI_BASE_URL_OLLAMA") or "").strip()
|
||
or "http://127.0.0.1:11434/v1"
|
||
)
|
||
DEFAULT_OLLAMA_MODEL = (os.getenv("OLLAMA_MODEL") or "qwen2.5:7b").strip()
|
||
_OLLAMA_DUMMY_KEY = "ollama"
|
||
|
||
|
||
def _build_executor_components(
|
||
store: SqliteStore,
|
||
*,
|
||
lang: str = "zh",
|
||
profile_id: str | None = None,
|
||
openai_api_key: str | None = None,
|
||
llm_mode: str | None = None,
|
||
model: str | None = None,
|
||
base_url: str | None = None,
|
||
viewer_user_id: str | None = None,
|
||
viewer_username: str | None = None,
|
||
viewer_tenant_id: str | None = None,
|
||
) -> tuple[
|
||
NetworkOpsAgent,
|
||
dict[str, SpecialistProfile],
|
||
object,
|
||
str,
|
||
dict[str, object],
|
||
dict[str, str],
|
||
]:
|
||
lang = (lang or "zh").strip().lower()
|
||
uid_scoped = str(viewer_user_id or "").strip()
|
||
personal = bool(uid_scoped) and not is_administrator_model_pool(viewer_username)
|
||
if personal:
|
||
active_key = active_llm_profile_setting_key(uid_scoped, viewer_username)
|
||
bindings_key = agent_profile_bindings_setting_key(uid_scoped, viewer_username)
|
||
list_kw: dict[str, Any] = {"viewer_user_id": uid_scoped, "viewer_username": viewer_username}
|
||
tid = str(viewer_tenant_id or "").strip()
|
||
if tid:
|
||
list_kw["viewer_tenant_id"] = tid
|
||
else:
|
||
active_key = "active_llm_profile_id"
|
||
bindings_key = AGENT_PROFILE_BINDINGS_KEY
|
||
list_kw = {}
|
||
active_pid = (profile_id or store.get_setting(active_key) or "").strip()
|
||
|
||
def _normalize_mode(raw: str | None) -> str:
|
||
m = (raw or "").strip().lower()
|
||
return m if m in ("openai", "openai_responses", "anthropic", "ollama", "rule", "google") else "rule"
|
||
|
||
def _build_chat_model_for_profile(
|
||
target_profile_id: str | None,
|
||
*,
|
||
allow_runtime_overrides: bool = False,
|
||
) -> tuple[object, str]:
|
||
pid = (target_profile_id or "").strip()
|
||
profile = store.get_llm_profile(pid) if pid else None
|
||
mode = _normalize_mode(
|
||
(llm_mode if allow_runtime_overrides else None)
|
||
or (profile.get("mode") if profile else None)
|
||
or os.getenv("AIA_ASSISTANT_MODE")
|
||
or "openai"
|
||
)
|
||
|
||
raw_model = (model if allow_runtime_overrides else None) or (profile.get("model") if profile else None) or ""
|
||
raw_model = str(raw_model).strip()
|
||
if not raw_model:
|
||
raw_model = (
|
||
(os.getenv("OLLAMA_MODEL") or "").strip()
|
||
if mode == "ollama"
|
||
else (os.getenv("OPENAI_MODEL") or "").strip()
|
||
)
|
||
model_name = raw_model or (DEFAULT_OLLAMA_MODEL if mode == "ollama" else "gpt-4o-mini")
|
||
|
||
bu = (base_url if allow_runtime_overrides else None) or (profile.get("base_url") if profile else None) or os.getenv("OPENAI_BASE_URL") or ""
|
||
bu = str(bu).strip()
|
||
stored_key = store.get_llm_profile_secret(pid) if pid else None
|
||
api_key = (openai_api_key if allow_runtime_overrides else None) or stored_key or os.getenv("OPENAI_API_KEY")
|
||
api_key = (api_key or "").strip()
|
||
|
||
if mode == "openai_responses":
|
||
if not api_key:
|
||
return StaticTextChatModel(_openai_missing_key_user_message(lang)), mode
|
||
return OpenAIResponsesModel(model=model_name, api_key=api_key, base_url=bu or None), mode
|
||
if mode == "anthropic":
|
||
akey = (
|
||
(openai_api_key if allow_runtime_overrides else None)
|
||
or stored_key
|
||
or os.getenv("ANTHROPIC_API_KEY")
|
||
or os.getenv("OPENAI_API_KEY")
|
||
or ""
|
||
)
|
||
akey = str(akey or "").strip()
|
||
if not akey:
|
||
return StaticTextChatModel(_openai_missing_key_user_message(lang)), mode
|
||
return AnthropicMessagesModel(model=model_name, api_key=akey, base_url=bu or None), mode
|
||
if mode == "google":
|
||
gkey = (
|
||
(openai_api_key if allow_runtime_overrides else None)
|
||
or stored_key
|
||
or os.getenv("GOOGLE_API_KEY")
|
||
or os.getenv("GEMINI_API_KEY")
|
||
or os.getenv("OPENAI_API_KEY")
|
||
or ""
|
||
)
|
||
gkey = str(gkey or "").strip()
|
||
if not gkey:
|
||
return StaticTextChatModel(_openai_missing_key_user_message(lang)), mode
|
||
return GoogleGeminiChatModel(model=model_name, api_key=gkey, base_url=bu or None), mode
|
||
|
||
if mode == "rule":
|
||
return RuleBasedChatModel(), mode
|
||
if mode == "ollama":
|
||
ollama_base = (bu or DEFAULT_OLLAMA_BASE_URL).strip() or DEFAULT_OLLAMA_BASE_URL
|
||
ollama_key = api_key or _OLLAMA_DUMMY_KEY
|
||
return OpenAIChatModel(model=model_name, api_key=ollama_key, base_url=ollama_base), mode
|
||
if not api_key:
|
||
return StaticTextChatModel(_openai_missing_key_user_message(lang)), mode
|
||
return OpenAIChatModel(model=model_name, api_key=api_key, base_url=bu or None), mode
|
||
|
||
valid_profile_ids = {p["id"] for p in store.list_llm_profiles(visible_only=True, **list_kw)}
|
||
if active_pid and active_pid not in valid_profile_ids:
|
||
active_pid = ""
|
||
active_model, active_mode = _build_chat_model_for_profile(active_pid, allow_runtime_overrides=True)
|
||
|
||
raw_bindings = parse_agent_profile_bindings(store.get_setting(bindings_key))
|
||
normalized_bindings: dict[str, str] = {}
|
||
for rid in AGENT_ROLE_IDS:
|
||
pid = (raw_bindings.get(rid) or "").strip()
|
||
normalized_bindings[rid] = pid if pid in valid_profile_ids else ""
|
||
if dump_agent_profile_bindings(normalized_bindings) != dump_agent_profile_bindings(raw_bindings):
|
||
store.set_setting(bindings_key, dump_agent_profile_bindings(normalized_bindings))
|
||
|
||
def _pick_model_for_role(role_id: str) -> tuple[object, str]:
|
||
bound_pid = (normalized_bindings.get(role_id) or "").strip()
|
||
if not bound_pid:
|
||
return active_model, active_mode
|
||
return _build_chat_model_for_profile(bound_pid, allow_runtime_overrides=False)
|
||
|
||
manager_model, manager_mode = _pick_model_for_role(MANAGER_AGENT_ID)
|
||
specialist_models: dict[str, object] = {}
|
||
specialist_modes: dict[str, str] = {}
|
||
for sid in SPECIALIST_IDS:
|
||
m, md = _pick_model_for_role(sid)
|
||
specialist_models[sid] = m
|
||
specialist_modes[sid] = md
|
||
|
||
base_agent = NetworkOpsAgent(
|
||
store=store,
|
||
model=specialist_models.get("ops") or active_model,
|
||
lang=lang,
|
||
llm_profile_mode=specialist_modes.get("ops") or active_mode,
|
||
)
|
||
try:
|
||
store.set_setting("agent_inventory_snapshot", str(inventory_snapshot()))
|
||
except Exception:
|
||
pass
|
||
try:
|
||
sync_plugin_metadata(store)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
upsert_knowledge_chunks(
|
||
store,
|
||
source="builtin:src",
|
||
chunks=[
|
||
"Use tools for route lookup, path search, config diff, device ping, and log analysis.",
|
||
"High-risk actions require explicit confirmation by user before execution.",
|
||
"Prefer citing tool outputs and avoid fabricating external facts.",
|
||
],
|
||
)
|
||
except Exception:
|
||
pass
|
||
specialist_profiles = {
|
||
"ops": SpecialistProfile(
|
||
name="ops",
|
||
system_prefix=default_system_prefix_for_specialist("ops", lang),
|
||
tool_tags=default_tool_tags_for_specialist("ops"),
|
||
),
|
||
"generalist": SpecialistProfile(
|
||
name="generalist",
|
||
system_prefix=default_system_prefix_for_specialist("generalist", lang),
|
||
tool_tags=default_tool_tags_for_specialist("generalist"),
|
||
),
|
||
"image": SpecialistProfile(
|
||
name="image",
|
||
system_prefix=default_system_prefix_for_specialist("image", lang),
|
||
tool_tags=default_tool_tags_for_specialist("image"),
|
||
),
|
||
"memory_curator": SpecialistProfile(
|
||
name="memory_curator",
|
||
system_prefix=default_system_prefix_for_specialist("memory_curator", lang),
|
||
tool_tags=default_tool_tags_for_specialist("memory_curator"),
|
||
),
|
||
}
|
||
return (
|
||
base_agent,
|
||
specialist_profiles,
|
||
manager_model,
|
||
manager_mode,
|
||
specialist_models,
|
||
specialist_modes,
|
||
)
|
||
|
||
|
||
def build_ops_agent(
|
||
store: SqliteStore,
|
||
*,
|
||
lang: str = "zh",
|
||
profile_id: str | None = None,
|
||
openai_api_key: str | None = None,
|
||
llm_mode: str | None = None,
|
||
model: str | None = None,
|
||
base_url: str | None = None,
|
||
viewer_user_id: str | None = None,
|
||
viewer_username: str | None = None,
|
||
viewer_tenant_id: str | None = None,
|
||
) -> Any:
|
||
del viewer_user_id, viewer_username, viewer_tenant_id
|
||
return build_gateway_executor(
|
||
store,
|
||
lang=lang,
|
||
specialist="ops",
|
||
profile_id=profile_id,
|
||
openai_api_key=openai_api_key,
|
||
llm_mode=llm_mode,
|
||
model=model,
|
||
base_url=base_url,
|
||
)
|
||
|
||
|
||
def build_gateway_executor(
|
||
store: SqliteStore,
|
||
*,
|
||
lang: str = "zh",
|
||
specialist: str | None = None,
|
||
profile_id: str | None = None,
|
||
openai_api_key: str | None = None,
|
||
llm_mode: str | None = None,
|
||
model: str | None = None,
|
||
base_url: str | None = None,
|
||
viewer_user_id: str | None = None,
|
||
viewer_username: str | None = None,
|
||
viewer_tenant_id: str | None = None,
|
||
policy_session_id: str | None = None,
|
||
path_policy_tenant_id: str | None = None,
|
||
path_policy_user_id: str | None = None,
|
||
) -> Any:
|
||
base_agent, specialist_profiles, _, _, specialist_models, specialist_modes = _build_executor_components(
|
||
store,
|
||
lang=lang,
|
||
profile_id=profile_id,
|
||
openai_api_key=openai_api_key,
|
||
llm_mode=llm_mode,
|
||
model=model,
|
||
base_url=base_url,
|
||
viewer_user_id=viewer_user_id,
|
||
viewer_username=viewer_username,
|
||
viewer_tenant_id=viewer_tenant_id,
|
||
)
|
||
sid = str(specialist or "").strip().lower() or "generalist"
|
||
if sid not in specialist_profiles:
|
||
sid = "generalist"
|
||
prof = specialist_profiles.get(sid) or specialist_profiles["generalist"]
|
||
chosen_model = specialist_models.get(prof.name) or base_agent.model
|
||
chosen_mode = specialist_modes.get(prof.name) or getattr(base_agent, "llm_profile_mode", None)
|
||
if prof.name == "ops":
|
||
return NetworkOpsAgent(
|
||
store=store,
|
||
model=chosen_model,
|
||
lang=(lang or "zh").strip().lower(),
|
||
llm_profile_mode=chosen_mode,
|
||
system_prompt=prof.system_prefix,
|
||
policy_session_id=policy_session_id,
|
||
path_policy_tenant_id=path_policy_tenant_id,
|
||
path_policy_user_id=path_policy_user_id,
|
||
)
|
||
tools = default_registry(
|
||
expert=expert_name_for_specialist(prof.name),
|
||
specialist=prof.name,
|
||
policy_session_id=policy_session_id,
|
||
path_policy_tenant_id=path_policy_tenant_id,
|
||
path_policy_user_id=path_policy_user_id,
|
||
store=store,
|
||
)
|
||
return Agent(
|
||
store=store,
|
||
tools=tools,
|
||
model=chosen_model,
|
||
system_prompt=prof.system_prefix,
|
||
lang=(lang or "zh").strip().lower(),
|
||
llm_profile_mode=chosen_mode,
|
||
)
|
||
|
||
|
||
def build_gateway_executors(
|
||
store: SqliteStore,
|
||
*,
|
||
lang: str = "zh",
|
||
profile_id: str | None = None,
|
||
openai_api_key: str | None = None,
|
||
llm_mode: str | None = None,
|
||
model: str | None = None,
|
||
base_url: str | None = None,
|
||
viewer_user_id: str | None = None,
|
||
viewer_username: str | None = None,
|
||
viewer_tenant_id: str | None = None,
|
||
policy_session_id: str | None = None,
|
||
path_policy_tenant_id: str | None = None,
|
||
path_policy_user_id: str | None = None,
|
||
) -> dict[str, Any]:
|
||
manager = build_gateway_executor(
|
||
store,
|
||
lang=lang,
|
||
specialist="generalist",
|
||
profile_id=profile_id,
|
||
openai_api_key=openai_api_key,
|
||
llm_mode=llm_mode,
|
||
model=model,
|
||
base_url=base_url,
|
||
viewer_user_id=viewer_user_id,
|
||
viewer_username=viewer_username,
|
||
viewer_tenant_id=viewer_tenant_id,
|
||
policy_session_id=policy_session_id,
|
||
path_policy_tenant_id=path_policy_tenant_id,
|
||
path_policy_user_id=path_policy_user_id,
|
||
)
|
||
specialists: dict[str, Any] = {}
|
||
for sid in SPECIALIST_IDS:
|
||
specialists[sid] = build_gateway_executor(
|
||
store,
|
||
lang=lang,
|
||
specialist=sid,
|
||
profile_id=profile_id,
|
||
openai_api_key=openai_api_key,
|
||
llm_mode=llm_mode,
|
||
model=model,
|
||
base_url=base_url,
|
||
viewer_user_id=viewer_user_id,
|
||
viewer_username=viewer_username,
|
||
viewer_tenant_id=viewer_tenant_id,
|
||
policy_session_id=policy_session_id,
|
||
path_policy_tenant_id=path_policy_tenant_id,
|
||
path_policy_user_id=path_policy_user_id,
|
||
)
|
||
return {"manager": manager, "specialists": specialists}
|
||
|
||
|
||
def build_ephemeral_executor(
|
||
store: SqliteStore,
|
||
*,
|
||
lang: str = "zh",
|
||
system_prompt: str,
|
||
tool_policy: dict[str, Any] | None = None,
|
||
profile_id: str | None = None,
|
||
openai_api_key: str | None = None,
|
||
llm_mode: str | None = None,
|
||
model: str | None = None,
|
||
base_url: str | None = None,
|
||
viewer_user_id: str | None = None,
|
||
viewer_username: str | None = None,
|
||
viewer_tenant_id: str | None = None,
|
||
policy_session_id: str | None = None,
|
||
path_policy_tenant_id: str | None = None,
|
||
path_policy_user_id: str | None = None,
|
||
) -> Any:
|
||
base_agent, _, _, _, _, _ = _build_executor_components(
|
||
store,
|
||
lang=lang,
|
||
profile_id=profile_id,
|
||
openai_api_key=openai_api_key,
|
||
llm_mode=llm_mode,
|
||
model=model,
|
||
base_url=base_url,
|
||
viewer_user_id=viewer_user_id,
|
||
viewer_username=viewer_username,
|
||
viewer_tenant_id=viewer_tenant_id,
|
||
)
|
||
declared = tool_policy if isinstance(tool_policy, dict) else {}
|
||
allow_tags = [str(x) for x in (declared.get("allow_tags") or []) if str(x or "").strip()]
|
||
allow_tools = [str(x) for x in (declared.get("allow_tools") or []) if str(x or "").strip()]
|
||
tools = default_registry(
|
||
expert="generalist+workspace+productivity",
|
||
specialist="generalist",
|
||
policy_session_id=policy_session_id,
|
||
path_policy_tenant_id=path_policy_tenant_id,
|
||
path_policy_user_id=path_policy_user_id,
|
||
store=store,
|
||
allow_tags=allow_tags,
|
||
allow_tools=allow_tools,
|
||
)
|
||
return Agent(
|
||
store=store,
|
||
tools=tools,
|
||
model=base_agent.model,
|
||
system_prompt=str(system_prompt or "").strip(),
|
||
lang=(lang or "zh").strip().lower(),
|
||
llm_profile_mode=getattr(base_agent, "llm_profile_mode", None),
|
||
)
|
||
|
||
|
||
__all__ = [
|
||
"DEFAULT_OLLAMA_BASE_URL",
|
||
"DEFAULT_OLLAMA_MODEL",
|
||
"build_ops_agent",
|
||
"build_gateway_executor",
|
||
"build_gateway_executors",
|
||
"build_ephemeral_executor",
|
||
]
|