Skip to content

Commit 88a6696

Browse files
committed
feat: add user phonon balance check callback to ensure sufficient balance before job creation
1 parent eaf3011 commit 88a6696

4 files changed

Lines changed: 134 additions & 62 deletions

File tree

agents/matmaster_agent/base_agents/callback.py

Lines changed: 104 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import os
55
import traceback
66
from functools import wraps
7-
from typing import Optional, Union
7+
from typing import Callable, Optional, Union
88

99
import aiohttp
1010
import litellm
@@ -27,6 +27,7 @@
2727
)
2828
from agents.matmaster_agent.prompt import get_params_check_info_prompt
2929
from agents.matmaster_agent.utils.auth import ak_to_ticket, ak_to_username
30+
from agents.matmaster_agent.utils.finance import get_user_photon_balance
3031
from agents.matmaster_agent.utils.helper_func import (
3132
check_None_wrapper,
3233
function_calls_to_str,
@@ -164,6 +165,100 @@ async def default_before_tool_callback(tool, args, tool_context):
164165
return
165166

166167

168+
def default_cost_func() -> int:
169+
return 300
170+
171+
172+
def check_user_phonon_balance(
173+
func: BeforeToolCallback, cost_func: Callable[[], int] = default_cost_func
174+
) -> BeforeToolCallback:
175+
@wraps(func)
176+
async def wrapper(
177+
tool: BaseTool, args: dict, tool_context: ToolContext
178+
) -> Optional[dict]:
179+
# 两步操作:
180+
# 1. 调用被装饰的 before_tool_callback;
181+
# 2. 如果调用的 before_tool_callback 有返回值,以这个为准
182+
if (before_tool_result := await func(tool, args, tool_context)) is not None:
183+
return before_tool_result
184+
185+
# 如果 tool 不是 CalculationMCPTool,不应该调用这个 callback
186+
if not isinstance(tool, CalculationMCPTool):
187+
raise TypeError(
188+
"Not CalculationMCPTool type, current tool can't create job!"
189+
)
190+
191+
user_id = _get_user_id(tool_context)
192+
cost = cost_func()
193+
balance = await get_user_photon_balance(user_id)
194+
195+
logger.info(
196+
f"[check_user_phonon_balance] user_id={user_id}, cost={cost}, balance={balance}"
197+
)
198+
if balance < cost:
199+
raise RuntimeError('Phonon is not enough, Please recharge.')
200+
201+
return wrapper
202+
203+
204+
def check_job_create(func: BeforeToolCallback) -> BeforeToolCallback:
205+
@wraps(func)
206+
async def wrapper(
207+
tool: BaseTool, args: dict, tool_context: ToolContext
208+
) -> Optional[dict]:
209+
# 两步操作:
210+
# 1. 调用被装饰的 before_tool_callback;
211+
# 2. 如果调用的 before_tool_callback 有返回值,以这个为准
212+
if (before_tool_result := await func(tool, args, tool_context)) is not None:
213+
return before_tool_result
214+
215+
# 如果 tool 不是 CalculationMCPTool,不应该调用这个 callback
216+
if not isinstance(tool, CalculationMCPTool):
217+
raise TypeError(
218+
"Not CalculationMCPTool type, current tool can't create job!"
219+
)
220+
221+
if tool.executor is not None:
222+
job_create_url = f"{OPENAPI_HOST}/openapi/v1/job/create"
223+
user_project_list_url = f"{OPENAPI_HOST}/openapi/v1/open/user/project/list"
224+
payload = {
225+
'projectId': MATERIALS_PROJECT_ID,
226+
'name': 'check_job_create',
227+
}
228+
params = {'accessKey': MATERIALS_ACCESS_KEY}
229+
230+
logger.info(
231+
f"[check_job_create] project_id = {MATERIALS_PROJECT_ID}, "
232+
f"ak = {MATERIALS_ACCESS_KEY}"
233+
)
234+
235+
async with aiohttp.ClientSession() as session:
236+
async with session.get(
237+
user_project_list_url, params=params
238+
) as response:
239+
res = json.loads(await response.text())
240+
logger.info(f"[check_job_create] res = {res}")
241+
project_name = [
242+
item['project_name']
243+
for item in res['data']['items']
244+
if item['project_id'] == MATERIALS_PROJECT_ID
245+
][0]
246+
247+
async with aiohttp.ClientSession() as session:
248+
async with session.post(
249+
job_create_url, json=payload, params=params
250+
) as response:
251+
res = json.loads(await response.text())
252+
if res['code'] != 0:
253+
if res['code'] == 2000:
254+
res['error'][
255+
'msg'
256+
] = f"您所用项目为 `{project_name}`,该项目余额不足,请充值或更换项目后重试。"
257+
return res
258+
259+
return wrapper
260+
261+
167262
@check_None_wrapper
168263
def _get_ak(ctx: Union[InvocationContext, ToolContext, CallbackContext]):
169264
session_state = get_session_state(ctx)
@@ -180,6 +275,14 @@ def _get_projectId(ctx: Union[InvocationContext, ToolContext]):
180275
)
181276

182277

278+
@check_None_wrapper
279+
def _get_user_id(ctx: Union[InvocationContext, ToolContext]):
280+
session_state = get_session_state(ctx)
281+
return session_state[FRONTEND_STATE_KEY].get('adk_user_id') or os.getenv(
282+
'MATERIALS_USER_ID'
283+
)
284+
285+
183286
def _inject_ak(ctx: Union[InvocationContext, ToolContext], executor, storage):
184287
access_key = _get_ak(ctx)
185288
if executor is not None:
@@ -295,64 +398,6 @@ async def wrapper(
295398
return wrapper
296399

297400

298-
def check_job_create(func: BeforeToolCallback) -> BeforeToolCallback:
299-
@wraps(func)
300-
async def wrapper(
301-
tool: BaseTool, args: dict, tool_context: ToolContext
302-
) -> Optional[dict]:
303-
# 两步操作:
304-
# 1. 调用被装饰的 before_tool_callback;
305-
# 2. 如果调用的 before_tool_callback 有返回值,以这个为准
306-
if (before_tool_result := await func(tool, args, tool_context)) is not None:
307-
return before_tool_result
308-
309-
# 如果 tool 不是 CalculationMCPTool,不应该调用这个 callback
310-
if not isinstance(tool, CalculationMCPTool):
311-
raise TypeError(
312-
"Not CalculationMCPTool type, current tool can't create job!"
313-
)
314-
315-
if tool.executor is not None:
316-
job_create_url = f"{OPENAPI_HOST}/openapi/v1/job/create"
317-
user_project_list_url = f"{OPENAPI_HOST}/openapi/v1/open/user/project/list"
318-
payload = {
319-
'projectId': MATERIALS_PROJECT_ID,
320-
'name': 'check_job_create',
321-
}
322-
params = {'accessKey': MATERIALS_ACCESS_KEY}
323-
324-
logger.info(
325-
f"[check_job_create] project_id = {MATERIALS_PROJECT_ID}, "
326-
f"ak = {MATERIALS_ACCESS_KEY}"
327-
)
328-
329-
async with aiohttp.ClientSession() as session:
330-
async with session.get(
331-
user_project_list_url, params=params
332-
) as response:
333-
res = json.loads(await response.text())
334-
logger.info(f"[check_job_create] res = {res}")
335-
project_name = [
336-
item['project_name']
337-
for item in res['data']['items']
338-
if item['project_id'] == MATERIALS_PROJECT_ID
339-
][0]
340-
341-
async with aiohttp.ClientSession() as session:
342-
async with session.post(
343-
job_create_url, json=payload, params=params
344-
) as response:
345-
res = json.loads(await response.text())
346-
if res['code'] != 0:
347-
if res['code'] == 2000:
348-
res['error'][
349-
'msg'
350-
] = f"您所用项目为 `{project_name}`,该项目余额不足,请充值或更换项目后重试。"
351-
return res
352-
353-
return wrapper
354-
355-
356401
# 总应该在最后
357402
def catch_before_tool_callback_error(func: BeforeToolCallback) -> BeforeToolCallback:
358403
@wraps(func)

agents/matmaster_agent/base_agents/job_agent.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
catch_before_tool_callback_error,
1818
check_before_tool_callback_effect,
1919
check_job_create,
20+
check_user_phonon_balance,
2021
default_after_model_callback,
2122
default_after_tool_callback,
2223
default_before_tool_callback,
@@ -161,7 +162,9 @@ def __init__(
161162
# Todo: support List[before_tool_callback]
162163
before_tool_callback = catch_before_tool_callback_error(
163164
inject_current_env(
164-
inject_username_ticket(check_job_create(before_tool_callback))
165+
inject_username_ticket(
166+
check_job_create(check_user_phonon_balance(before_tool_callback))
167+
)
165168
)
166169
)
167170
after_tool_callback = check_before_tool_callback_effect(
@@ -359,6 +362,8 @@ async def _run_async_impl(
359362
):
360363
try:
361364
dict_result = load_tool_response(event)
365+
if dict_result.get('status', None) == 'error':
366+
raise eval(dict_result['error_type'])(dict_result['error'])
362367
except BaseException:
363368
yield event
364369
raise

agents/matmaster_agent/constant.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,8 @@
4747
FINANCE_INFO_API = f"{FINANCE_API_BASE}/api/integral/info"
4848
FINANCE_CONSUME_API = f"{FINANCE_API_BASE}/api/integral/consume"
4949

50-
MATERIALS_USER_ID = int(os.getenv('MATERIALS_USER_ID'), -1)
51-
MATERIALS_ORG_ID = int(os.getenv('MATERIALS_ORG_ID'), -1)
50+
MATERIALS_USER_ID = int(os.getenv('MATERIALS_USER_ID', -1))
51+
MATERIALS_ORG_ID = int(os.getenv('MATERIALS_ORG_ID', -1))
5252
MATERIALS_ACCESS_KEY = str(os.getenv('MATERIALS_ACCESS_KEY'))
5353
MATERIALS_PROJECT_ID = int(os.getenv('MATERIALS_PROJECT_ID'))
5454

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import logging
2+
3+
import aiohttp
4+
5+
from agents.matmaster_agent.constant import (
6+
FINANCE_INFO_API,
7+
)
8+
9+
logger = logging.getLogger(__name__)
10+
11+
12+
async def get_user_photon_balance(user_id):
13+
payload = {'userId': int(user_id)}
14+
async with aiohttp.ClientSession() as session:
15+
async with session.post(
16+
FINANCE_INFO_API, json=payload, headers={'Content-Type': 'application/json'}
17+
) as response:
18+
response.raise_for_status()
19+
res = await response.json()
20+
21+
logger.info(f'[get_user_photon_balance] balance = {res['data']['balance']}')
22+
return res['data']['balance']

0 commit comments

Comments
 (0)