-
Notifications
You must be signed in to change notification settings - Fork 80
Expand file tree
/
Copy path_heart_service.py
More file actions
148 lines (130 loc) · 4.86 KB
/
Copy path_heart_service.py
File metadata and controls
148 lines (130 loc) · 4.86 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# Tencent is pleased to support the open source community by making tRPC-Agent-Python available.
#
# Copyright (C) 2026 Tencent. All rights reserved.
#
# This file is part of tRPC-Agent-Python and is licensed under Apache-2.0.
#
# Portions of this file are derived from HKUDS/nanobot (MIT License):
# https://github.com/HKUDS/nanobot.git
#
# Copyright (c) 2025 nanobot contributors
#
# See the project LICENSE / third-party attribution notices for details.
#
"""Heartbeat service - periodic agent wake-up to check for tasks."""
from __future__ import annotations
from pathlib import Path
from typing import Any
from typing import Callable
from typing import Coroutine
from nanobot.heartbeat import service as heartbeat_service_package
from trpc_agent_sdk.models import LLMModel
from trpc_agent_sdk.models import LlmRequest
from trpc_agent_sdk.types import Content
from trpc_agent_sdk.types import FunctionDeclaration
from trpc_agent_sdk.types import GenerateContentConfig
from trpc_agent_sdk.types import Part
from trpc_agent_sdk.types import Schema
from trpc_agent_sdk.types import Tool
from trpc_agent_sdk.types import Type
_HEARTBEAT_SYSTEM: str = "You are a heartbeat agent. Call the heartbeat tool to report your decision."
_HEARTBEAT_DECLARATION: FunctionDeclaration = FunctionDeclaration(
name="heartbeat",
description="Report heartbeat decision after reviewing tasks.",
parameters=Schema(
type=Type.OBJECT,
properties={
"action":
Schema(
type=Type.STRING,
enum=["skip", "run"],
description="skip = nothing to do, run = has active tasks",
),
"tasks":
Schema(
type=Type.STRING,
description="Natural-language summary of active tasks (required for run)",
),
},
required=["action"],
),
)
class ClawHeartbeatService(heartbeat_service_package.HeartbeatService):
"""trpc_claw heartbeat service.
Replaces the nanobot LLMProvider.chat() call in the parent _decide
with a direct LLMModel.generate_async() call so the service works
inside the trpc_claw framework.
"""
def __init__(
self,
workspace: Path,
provider: LLMModel,
model: str,
on_execute: Callable[[str], Coroutine[Any, Any, str]] | None = None,
on_notify: Callable[[str], Coroutine[Any, Any, None]] | None = None,
interval_s: int = 30 * 60,
enabled: bool = True,
):
"""Initialize the heartbeat service.
Args:
workspace: The workspace path.
provider: The LLM provider.
model: The model name.
on_execute: The on_execute callback.
on_notify: The on_notify callback.
interval_s: The interval in seconds.
enabled: Whether the service is enabled.
"""
super().__init__(
workspace=workspace,
provider=provider,
model=model,
on_execute=on_execute,
on_notify=on_notify,
interval_s=interval_s,
enabled=enabled,
)
async def _decide(self, content: str) -> tuple[str, str]:
"""Phase 1: ask LLM to decide skip/run via virtual tool call.
Mirrors HeartbeatService._decide but drives LLMModel instead of
LLMProvider.
Args:
content: The content to review.
Returns:
tuple[str, str]: The action and tasks.
- action: The action to take.
- tasks: The tasks to review.
- skip: Nothing to do.
- run: Has active tasks.
"""
request = LlmRequest(
model=self.model,
contents=[
Content(
role="user",
parts=[
Part(text=("Review the following HEARTBEAT.md and decide "
"whether there are active tasks.\n\n"
f"{content}"))
],
)
],
config=GenerateContentConfig(
system_instruction=_HEARTBEAT_SYSTEM,
tools=[Tool(function_declarations=[_HEARTBEAT_DECLARATION])],
),
)
response = None
model: LLMModel = self.provider
async for resp in model.generate_async(request, stream=False):
if resp.content:
response = resp
break
if response is None or response.content is None:
return "skip", ""
for part in response.content.parts or []:
fc = getattr(part, "function_call", None)
if fc and getattr(fc, "name", None) == "heartbeat":
args: dict[str, Any] = dict(fc.args) if fc.args else {}
return args.get("action", "skip"), args.get("tasks", "")
return "skip", ""