Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,15 @@ SECURITY__API_KEY_ENCRYPTION_KEY=
# MINIO__ACCESS_KEY=
# MINIO__SECRET_KEY=

# ------------------------------------------------------------
# Skill store(GitHub topic 市场,默认 enabled=false)
# 命名规则: 段__键 -> config["skill_store"]["key"]
# topic 标识 skill 仓库(仓库打该 topic 即上架);api_key 为 GitHub token(可选,提升搜索 rate limit)
# ------------------------------------------------------------
# SKILL_STORE__ENABLED=true
# SKILL_STORE__TOPIC=mindbase-skill
# SKILL_STORE__API_KEY=github_pat_xxx

# ------------------------------------------------------------
# RabbitMQ(默认 enabled=false,启用后填)
# ------------------------------------------------------------
Expand Down
15 changes: 13 additions & 2 deletions app/agent/chat/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,11 @@ def _has_tool_calls(msg: BaseMessage) -> bool:


async def inject_context(
state: ChatAgentState, *, deps: Any, runtime: AgentRuntime
state: ChatAgentState,
*,
deps: Any,
runtime: AgentRuntime,
skill_manager: Any = None,
) -> dict[str, Any]:
"""1/4. Resolve data scope + inject system prompt with conversation context.

Expand Down Expand Up @@ -86,13 +90,17 @@ async def inject_context(

from langchain_core.messages import HumanMessage, SystemMessage

skills_section = (
await skill_manager.index_text(state.uid) if skill_manager is not None else ""
)
system = SystemMessage(
content=build_system_prompt(
state.query,
has_data=has_data,
cloud_has_data=cloud_has_data,
conversation_context=conversation_context,
has_context_tools=has_context_tools,
skills_section=skills_section,
)
)
user = HumanMessage(content=state.query)
Expand Down Expand Up @@ -314,6 +322,7 @@ def build_chat_agent(
llm: Any,
deps: Any,
circuit_breaker: CircuitBreaker | None = None,
skill_manager: Any = None,
) -> object:
"""Build the ReAct Chat Agent graph.

Expand Down Expand Up @@ -344,7 +353,9 @@ def build_chat_agent(
async def inject_node(s: ChatAgentState) -> dict:
if circuit_breaker and circuit_breaker.is_tripped:
return {"result": "", "error": "circuit breaker open"}
return await _inject(s, deps=deps, runtime=runtime)
return await _inject(
s, deps=deps, runtime=runtime, skill_manager=skill_manager
)

async def agent_node(s: ChatAgentState) -> dict:
return await _agent(s, llm_with_tools=llm_with_tools)
Expand Down
4 changes: 4 additions & 0 deletions app/agent/chat/prompts.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@

{context_tools_section}

{skills_section}

## 决策流程(必须遵循)

```
Expand Down Expand Up @@ -174,6 +176,7 @@ def build_system_prompt(
cloud_has_data: bool = False,
conversation_context: str = "",
has_context_tools: bool = False,
skills_section: str = "",
) -> str:
"""Build the system prompt for the Chat Agent."""
if has_data and cloud_has_data:
Expand All @@ -195,4 +198,5 @@ def build_system_prompt(
date_status=date_status,
conversation_context=conversation_context or "(无历史对话上下文)",
context_tools_section=context_tools_section,
skills_section=skills_section,
)
8 changes: 8 additions & 0 deletions app/config/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,14 @@ minio:
# access_key / secret_key via env: MINIO__ACCESS_KEY, MINIO__SECRET_KEY


# ---- Skill store - external third-party skill registry -------------
skill_store:
enabled: false
topic: mindbase-skill # GitHub topic identifying skill repos (marketplace)
timeout: 30
# api_key (GitHub token, optional) via env: SKILL_STORE__API_KEY


# ---- Message queue — Celery + Redis Streams -----------------------
mq:
enabled: false
Expand Down
8 changes: 8 additions & 0 deletions app/harness/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from app.harness.orchestrator import AgentOrchestrator
from app.harness.runtime import AgentRuntime
from app.harness.scheduling.agent import AgentConfig, AgentScheduler
from app.skills import SkillManager
from app.tools import ToolDeps, ToolManager, ToolRegistry

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -83,6 +84,7 @@ def __init__(
self._lifecycle = AgentLifecycleManager()
self._orchestrator = AgentOrchestrator(llm=llm)
self._scheduler = AgentScheduler(self._lifecycle)
self._skill_manager = SkillManager(session_factory)
self._tool_manager: ToolManager | None = None
self._cleanup_task: asyncio.Task | None = None
self._started = False
Expand All @@ -103,6 +105,10 @@ def runtime(self) -> AgentRuntime:
def scheduler(self) -> AgentScheduler:
return self._scheduler

@property
def skill_manager(self) -> SkillManager:
return self._skill_manager

@property
def tool_registry(self) -> ToolRegistry:
"""Registered tool instances. Use ``runtime.execute()`` to invoke."""
Expand Down Expand Up @@ -342,6 +348,7 @@ def _register_tools(self) -> None:
llm=self._llm,
db_deps=db_deps,
lifecycle=self._lifecycle,
skill_manager=self._skill_manager,
)

manager = ToolManager(deps)
Expand Down Expand Up @@ -398,6 +405,7 @@ def _register_agents(self) -> None:
llm=self._llm,
deps=deps,
circuit_breaker=self._lifecycle.circuit,
skill_manager=self._skill_manager,
)
self._orchestrator.register(
"chat",
Expand Down
8 changes: 8 additions & 0 deletions app/infra/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,13 @@ class MinioSection(_Section):
public_endpoint: str = "" # e.g. https://example.com/minio-proxy


class SkillStoreSection(_Section):
enabled: bool = False
topic: str = "mindbase-skill" # GitHub topic identifying skill repos
api_key: SecretStr = SecretStr("") # optional GitHub token (raises search rate limit when unset)
timeout: int = 30


class MqSection(_Section):
enabled: bool = False
broker: str = "redis://localhost:6379/1"
Expand Down Expand Up @@ -373,6 +380,7 @@ class AppConfig(BaseSettings):
mongo: MongoSection = Field(default_factory=MongoSection)
redis: RedisSection = Field(default_factory=RedisSection)
minio: MinioSection = Field(default_factory=MinioSection)
skill_store: SkillStoreSection = Field(default_factory=SkillStoreSection)
mq: MqSection = Field(default_factory=MqSection)
llm: LlmSection = Field(default_factory=LlmSection)
embedding: EmbeddingSection = Field(default_factory=EmbeddingSection)
Expand Down
24 changes: 24 additions & 0 deletions app/infra/minio.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,30 @@ async def get_object(self, object_key: str) -> bytes:
raise FileNotFoundError(f"Object not found: {object_key}") from exc
raise

async def put_object(
self, object_key: str, data: bytes, content_type: str = "application/octet-stream"
) -> None:
"""Upload a small object (in-memory bytes) to MinIO.

Used for skill packs and other small artifacts. Large uploads should
use the multipart API instead.
"""
import io

self._ensure_client()
stream = io.BytesIO(data)
await _run_async(
self._client.put_object,
self.bucket,
object_key,
stream,
len(data),
content_type,
)
logger.info(
"[MINIO] put_object object_key={} size={}", object_key, len(data)
)


# ---------------------------------------------------------------------------
# module-level singleton
Expand Down
5 changes: 5 additions & 0 deletions app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -457,6 +457,11 @@ async def request_context_middleware(request: Request, call_next):

app.include_router(agent_runtime_router)

# Skills - install/list/uninstall (MinIO-backed, never local disk)
from app.routers.skills import router as skills_router # noqa: E402

app.include_router(skills_router)

# Plan 0021: Cloud drive router (with graceful degradation)
try:
from app.routers.cloud import router as cloud_router
Expand Down
33 changes: 33 additions & 0 deletions app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -924,3 +924,36 @@ class NoteShare(Base):
view_count = Column(Integer, default=0)
is_revoked = Column(Boolean, default=False)
created_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))


class InstalledSkill(Base):
"""Installed Skill metadata (per-user) - the skill *content* (zip) lives
in MinIO, never on the local filesystem. Rows here are the index each
user's agent sees.

Skills are downloaded from an external skill store (or uploaded) into
MinIO at ``minio_key`` (``skills/{uid}/{skill_id}.zip``) and lazily
loaded by ``SkillManager`` when the agent calls ``load_skill``.
"""

__tablename__ = "installed_skills"
__table_args__ = (
UniqueConstraint("uid", "skill_id", name="uq_installed_skills_uid_skill"),
)

id = Column(Integer, primary_key=True, autoincrement=True)
uid = Column(BigInteger, ForeignKey("users.uid"), nullable=False, index=True)
skill_id = Column(String(128), nullable=False, index=True)
name = Column(String(200), nullable=False)
description = Column(Text, nullable=True)
version = Column(String(64), nullable=True)
source_store = Column(String(128), nullable=True) # 'upload' | store base url
minio_key = Column(String(256), nullable=False) # skills/{uid}/{skill_id}.zip
manifest = Column(JSON, nullable=True) # {has_code_tools, resources, ...}
enabled = Column(Boolean, default=True, nullable=False)
installed_at = Column(DateTime, default=lambda: datetime.now(timezone.utc))
updated_at = Column(
DateTime,
default=lambda: datetime.now(timezone.utc),
onupdate=lambda: datetime.now(timezone.utc),
)
Loading
Loading