repositories
loading repo index
repositories
loading repo index
repository
loading code, commits, and activity
Mirrored from https://github.com/yingqi-z20/Agent-libOS
stars
latest
clone command
git clone gitlawb://did:key:z6MkqRzA...RfoM/yingqi-z20-Agen...git clone gitlawb://did:key:z6MkqRzA.../yingqi-z20-Agen...d98dd2c9IPC1d ago| #1 | from __future__ import annotations |
| #2 | |
| #3 | import json |
| #4 | import threading |
| #5 | from typing import Any |
| #6 | |
| #7 | from pydantic import BaseModel, Field |
| #8 | |
| #9 | from agent_libos.config import DEFAULT_CONFIG |
| #10 | from agent_libos.models.exceptions import HumanResponseRequired |
| #11 | from agent_libos.tools.base import SyncAgentTool, ToolContext, ToolErrorCode, ToolExecutionError, ToolPolicy |
| #12 | |
| #13 | _RUNTIME_DEFAULTS = DEFAULT_CONFIG.runtime |
| #14 | _TOOL_DEFAULTS = DEFAULT_CONFIG.tools |
| #15 | |
| #16 | |
| #17 | class HumanOutputArgs(BaseModel): |
| #18 | message: str = Field(description="Message to present to the human operator.") |
| #19 | channel: str = Field(default=_RUNTIME_DEFAULTS.terminal_channel, description="Output channel. MVP supports terminal.") |
| #20 | |
| #21 | |
| #22 | class HumanOutputResult(BaseModel): |
| #23 | delivered: bool |
| #24 | channel: str |
| #25 | chars: int |
| #26 | |
| #27 | |
| #28 | class AskHumanArgs(BaseModel): |
| #29 | question: str = Field(description="Question to ask the human operator.") |
| #30 | context: dict[str, Any] = Field( |
| #31 | default_factory=dict, |
| #32 | description="Optional structured context shown with the question.", |
| #33 | ) |
| #34 | human: str = Field(default=_RUNTIME_DEFAULTS.default_human, description="Human recipient name.") |
| #35 | |
| #36 | |
| #37 | class AskHumanResult(BaseModel): |
| #38 | request_id: str |
| #39 | answer: str |
| #40 | status: str |
| #41 | |
| #42 | |
| #43 | class HumanOutputTool(SyncAgentTool[HumanOutputArgs]): |
| #44 | name = "human_output" |
| #45 | description = ( |
| #46 | "Present a message to the human operator. MVP implementation writes to the terminal. " |
| #47 | "This is a Skills/Tools Layer wrapper around the libOS HumanObject output primitive; " |
| #48 | "the primitive enforces human write capability, audit, and events." |
| #49 | ) |
| #50 | args_schema = HumanOutputArgs |
| #51 | output_schema = HumanOutputResult |
| #52 | policy = ToolPolicy( |
| #53 | side_effects=True, |
| #54 | idempotent=False, |
| #55 | requires_confirmation=False, |
| #56 | permissions={"human.output"}, |
| #57 | timeout_s=_TOOL_DEFAULTS.interactive_timeout_s, |
| #58 | ) |
| #59 | tags = ["human", "terminal", "output"] |
| #60 | |
| #61 | def run(self, args: HumanOutputArgs, ctx: ToolContext) -> HumanOutputResult: |
| #62 | runtime = ctx.runtime |
| #63 | if runtime is None: |
| #64 | raise ToolExecutionError("Runtime is unavailable.", code=ToolErrorCode.EXECUTION_ERROR) |
| #65 | result = runtime.human.output( |
| #66 | pid=ctx.pid, |
| #67 | message=args.message, |
| #68 | human=_RUNTIME_DEFAULTS.default_human, |
| #69 | channel=args.channel, |
| #70 | ) |
| #71 | return HumanOutputResult(**result) |
| #72 | |
| #73 | |
| #74 | class AskHumanTool(SyncAgentTool[AskHumanArgs]): |
| #75 | name = "ask_human" |
| #76 | description = ( |
| #77 | "Ask the human operator a question and return the human's answer. " |
| #78 | "This blocks the process through the libOS HumanObject queue until the answer is available." |
| #79 | ) |
| #80 | args_schema = AskHumanArgs |
| #81 | output_schema = AskHumanResult |
| #82 | policy = ToolPolicy( |
| #83 | side_effects=True, |
| #84 | idempotent=False, |
| #85 | requires_confirmation=False, |
| #86 | permissions={"human.ask"}, |
| #87 | timeout_s=_TOOL_DEFAULTS.interactive_timeout_s, |
| #88 | ) |
| #89 | tags = ["human", "terminal", "question"] |
| #90 | |
| #91 | def __init__(self) -> None: |
| #92 | self._lock = threading.RLock() |
| #93 | self._pending_by_key: dict[str, str] = {} |
| #94 | |
| #95 | def run(self, args: AskHumanArgs, ctx: ToolContext) -> AskHumanResult: |
| #96 | runtime = ctx.runtime |
| #97 | if runtime is None: |
| #98 | raise ToolExecutionError("Runtime is unavailable.", code=ToolErrorCode.EXECUTION_ERROR) |
| #99 | key = self._pending_key(ctx.pid, args) |
| #100 | with self._lock: |
| #101 | request_id = self._pending_by_key.get(key) |
| #102 | if request_id is None: |
| #103 | # The first call queues the question and raises; the resumed call |
| #104 | # with the same arguments returns the already-recorded answer. |
| #105 | request_id = runtime.human.ask( |
| #106 | pid=ctx.pid, |
| #107 | human=args.human, |
| #108 | question=args.question, |
| #109 | context=args.context, |
| #110 | blocking=True, |
| #111 | ) |
| #112 | with self._lock: |
| #113 | self._pending_by_key[key] = request_id |
| #114 | raise HumanResponseRequired( |
| #115 | request_id=request_id, |
| #116 | message=f"{ctx.pid} is waiting for human answer to {request_id}", |
| #117 | ) |
| #118 | |
| #119 | try: |
| #120 | answer = runtime.human.answer_for_request(request_id) |
| #121 | except HumanResponseRequired: |
| #122 | raise |
| #123 | except Exception: |
| #124 | with self._lock: |
| #125 | self._pending_by_key.pop(key, None) |
| #126 | raise |
| #127 | with self._lock: |
| #128 | self._pending_by_key.pop(key, None) |
| #129 | return AskHumanResult(request_id=request_id, answer=answer, status="answered") |
| #130 | |
| #131 | def _pending_key(self, pid: str, args: AskHumanArgs) -> str: |
| #132 | payload = { |
| #133 | "pid": pid, |
| #134 | "human": args.human, |
| #135 | "question": args.question, |
| #136 | "context": args.context, |
| #137 | } |
| #138 | return json.dumps(payload, sort_keys=True, ensure_ascii=True, default=str) |
| #139 |