From 6dee8e3ea620051090d1af4efca6796b7533e9d3 Mon Sep 17 00:00:00 2001 From: chaeyn Date: Sat, 3 Oct 2026 10:22:19 +0900 Subject: [PATCH 1/5] =?UTF-8?q?fix:=20=EC=9D=BC=EB=B0=98=20Wiki=20?= =?UTF-8?q?=EC=A7=88=EB=AC=B8=EC=9D=98=20=EC=A1=B0=ED=9A=8C=20=EC=98=88?= =?UTF-8?q?=EC=82=B0=EA=B3=BC=20=EC=9A=94=EC=B2=AD=20=EB=8C=80=ED=99=94?= =?UTF-8?q?=EB=9F=89=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/hermes/configure_wiki_budget.py | 35 +++++ .../hermes/framework-wiki-budget/__init__.py | 126 ++++++++++++++++++ .../hermes/framework-wiki-budget/plugin.yaml | 6 + 3 files changed, 167 insertions(+) create mode 100644 deploy/hermes/configure_wiki_budget.py create mode 100644 deploy/hermes/framework-wiki-budget/__init__.py create mode 100644 deploy/hermes/framework-wiki-budget/plugin.yaml diff --git a/deploy/hermes/configure_wiki_budget.py b/deploy/hermes/configure_wiki_budget.py new file mode 100644 index 0000000..81fe095 --- /dev/null +++ b/deploy/hermes/configure_wiki_budget.py @@ -0,0 +1,35 @@ +"""Install the opt-in Wiki budget plugin without modifying Hermes core or secrets.""" +import argparse +from datetime import datetime, timezone +from pathlib import Path +import shutil +import yaml + +NAME = 'framework-wiki-budget' + +def configure(home): + path = home / 'config.yaml' + config = yaml.safe_load(path.read_text()) + plugins = config.setdefault('plugins', {}) + if not isinstance(plugins, dict): raise ValueError('Review existing plugins settings') + enabled = plugins.get('enabled', []) + disabled = plugins.get('disabled', []) + if not isinstance(enabled,list) or not isinstance(disabled,list): raise ValueError('Review plugin allow-list') + plugins['enabled'] = list(dict.fromkeys([*enabled, NAME])) + if NAME in disabled: plugins['disabled'] = [name for name in disabled if name != NAME] + backup = home/'backups'/('wiki-budget-'+datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%S%fZ')) + backup.mkdir(parents=True,mode=0o700) + shutil.copyfile(path,backup/'config.yaml');(backup/'config.yaml').chmod(0o600) + destination = home/'plugins'/NAME + if destination.exists(): shutil.copytree(destination,backup/NAME) + source=Path(__file__).with_name(NAME) + destination.mkdir(parents=True,exist_ok=True) + for name in ['plugin.yaml','__init__.py']: + temporary=destination/(name+'.tmp');temporary.write_bytes((source/name).read_bytes());temporary.replace(destination/name) + temporary=path.with_name('config.yaml.wiki-budget-tmp') + temporary.write_text(yaml.safe_dump(config,allow_unicode=True,sort_keys=False));temporary.chmod(0o600);temporary.replace(path) + return backup + +if __name__=='__main__': + parser=argparse.ArgumentParser();parser.add_argument('--hermes-home',type=Path,default=Path('/home/chaeyn/.hermes')) + print('Wiki budget plugin installed; private backup:',configure(parser.parse_args().hermes_home)) diff --git a/deploy/hermes/framework-wiki-budget/__init__.py b/deploy/hermes/framework-wiki-budget/__init__.py new file mode 100644 index 0000000..2b59a79 --- /dev/null +++ b/deploy/hermes/framework-wiki-budget/__init__.py @@ -0,0 +1,126 @@ +"""Request-only context selection and read budgets using Hermes plugin contracts.""" +from copy import deepcopy +from dataclasses import dataclass, field +import logging +import re +import threading +import time + +log = logging.getLogger(__name__) +TOPIC = re.compile(r'framework|innolive|inno live|이노라이브|위키|노션|notion', re.I) +LOOKUP = re.compile(r'알려|설명|요약|정리|찾아|변천|변화|현황|목록|링크|무엇|뭐|어떻|읽|볼 수|show|explain|summari|history|list|find', re.I) +DEEP_OR_WRITE = re.compile(r'심층|자세히|상세히|꼼꼼|전부|모든 문서|끝까지|deep research|exhaustive|고쳐|수정해|수정해줘|작성해|구현해|코드.*(?:분석|확인|검토)|배포|커밋|실행해|갱신|업데이트해|삭제|등록해|보내|전송', re.I) +FOLLOWUP = re.compile(r'이 문서|그 문서|이 모델|그 모델|방금|앞서|이 중|그중|그러면|그럼|후속') +SOURCE = 'mcp__framework_wiki__' +READS = {'get_sources_context', 'search_notion', 'read_notion_page', 'search', 'get_context', 'get_note_outline', 'read_sections', 'get_wiki_status', 'list_notes'} +INSTRUCTION = '일반 문서 질문: get_sources_context(sources="all", limit=8, max_chars=12000)를 한 번 조회하고, 필요하면 원문을 최대 2회 보충한다. 제목 검색을 반복하지 않는다. 근거가 확보되면 원문 링크와 함께 답한다. 3회의 문서 조회 또는 3회의 도구 단계 뒤에는 조회를 끝내고 확인 범위와 남은 불확실성을 밝힌다. 요청하지 않은 심층 조사와 skill 수정을 시작하지 않는다.' + + +def text_of(message): + content = message.get('content', '') + if isinstance(content, str): return content + if isinstance(content, list): + return '\n'.join(x.get('text', '') for x in content if isinstance(x, dict) and isinstance(x.get('text'), str)) + return '' + + +def ordinary_question(text, history=()): + if DEEP_OR_WRITE.search(text) or not LOOKUP.search(text): return False + if TOPIC.search(text): return True + return bool(FOLLOWUP.search(text) and any(TOPIC.search(text_of(m)) for m in history[-6:] if m.get('role') == 'user')) + + +def select_messages(messages, old_chars=8000): + """Keep the current user and all current tool pairs; select plain previous context.""" + start = next((i for i in range(len(messages)-1, -1, -1) if messages[i].get('role') == 'user'), None) + if start is None: return deepcopy(messages) + prefix = [deepcopy(m) for m in messages[:start] if m.get('role') in ('system', 'developer')] + previous = [] + for m in reversed(messages[:start]): + if m.get('role') not in ('user', 'assistant') or m.get('tool_calls') or m.get('type') == 'function_call': continue + content = text_of(m) + if not content: continue + budget = max(0, old_chars - sum(len(x['content']) for x in previous)) + if not budget: break + cap = min(budget, 4000) + clipped = content if len(content) <= cap else content[:max(0, cap-1000)] + content[-min(1000, cap):] + previous.append({'role': m['role'], 'content': clipped}) + if len(previous) >= 4: break + # Retain alternating plain conversation, without dangling old tool calls. + previous.reverse() + while previous and previous[-1]['role'] != 'assistant': previous.pop() + return prefix + previous + deepcopy(messages[start:]) + + +@dataclass +class Turn: + started: float + reads: int = 0 + catalog: int = 0 + seen: set = field(default_factory=set) + + +class Budget: + def __init__(self, clock=time.monotonic): + self.clock = clock; self.turns = {}; self.lock = threading.RLock() + + def key(self, session_id='', turn_id='', task_id='', **kwargs): + return (session_id, turn_id or task_id) + + def begin(self, user_message='', conversation_history=(), platform='', **ids): + if platform != 'discord' or not ordinary_question(str(user_message), conversation_history): return None + with self.lock: + now = self.clock() + self.turns = {k: v for k, v in self.turns.items() if now-v.started < 600} + self.turns[self.key(**ids)] = Turn(now) + return {'context': INSTRUCTION} + + def request(self, request, api_call_count=0, **ids): + with self.lock: + turn = self.turns.get(self.key(**ids)) + if turn is None: return None + payload = deepcopy(request) + key = 'messages' if isinstance(payload.get('messages'), list) else 'input' + if isinstance(payload.get(key), list): + before = len(str(payload[key])) + payload[key] = select_messages(payload[key]) + log.info('Wiki request context selected: chars=%s->%s reads=%s', before, len(str(payload[key])), turn.reads) + if turn.reads >= 3 or turn.catalog >= 8 or api_call_count >= 4 or self.clock()-turn.started >= 90: + payload['tools'] = [] + payload.pop('tool_choice', None) + payload.pop('parallel_tool_calls', None) + return {'request': payload, 'source': 'framework-wiki-budget', 'reason': 'ordinary document question'} + + def tool(self, tool_name, args, tool_call_id='', **ids): + with self.lock: + turn = self.turns.get(self.key(**ids)) + if turn is None: return None + name = tool_name + if name == 'tool_call': + calls = args.get('calls', []) + if len(calls) != 1 or not isinstance(calls[0], dict): return self.block() + name = calls[0].get('name', '') + # The bridge may fire the hook again for the unwrapped name with the same id. + dedup = (tool_call_id, name) + if tool_call_id and dedup in turn.seen: return None + if self.clock()-turn.started >= 90: return self.block() + if name.startswith(SOURCE) and name[len(SOURCE):] in READS: + if turn.reads >= 3: return self.block() + turn.reads += 1 + elif name in {'tool_search', 'tool_describe', 'skill_view', 'skills_list'}: + if turn.catalog >= 8: return self.block() + turn.catalog += 1 + else: return self.block() + turn.seen.add(dedup) + return None + + @staticmethod + def block(): + return {'action': 'block', 'message': '일반 문서 질문의 조회 범위를 마쳤습니다. 확보한 근거로 답하고 확인하지 못한 부분을 밝혀 주세요.'} + + +def register(ctx): + budget = Budget() + ctx.register_hook('pre_llm_call', budget.begin) + ctx.register_hook('pre_tool_call', budget.tool) + ctx.register_middleware('llm_request', budget.request) diff --git a/deploy/hermes/framework-wiki-budget/plugin.yaml b/deploy/hermes/framework-wiki-budget/plugin.yaml new file mode 100644 index 0000000..87c6f38 --- /dev/null +++ b/deploy/hermes/framework-wiki-budget/plugin.yaml @@ -0,0 +1,6 @@ +name: framework-wiki-budget +version: 0.1.0 +description: Bound ordinary Discord Wiki questions while preserving saved conversation history. +provides_hooks: + - pre_llm_call + - pre_tool_call From 6065d90a97d68f2d1e15a2d27c4581afc84fa16c Mon Sep 17 00:00:00 2001 From: chaeyn Date: Sat, 3 Oct 2026 10:22:19 +0900 Subject: [PATCH 2/5] =?UTF-8?q?test:=20Wiki=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=98=88=EC=82=B0=EA=B3=BC=20=EC=8B=A4=EC=A0=9C=20Hermes=20?= =?UTF-8?q?=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EA=B2=A9=EB=A6=AC=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/hermes/test_wiki_budget.py | 90 +++++++++++++++++++++++++++++ deploy/hermes/verify_wiki_budget.py | 42 ++++++++++++++ 2 files changed, 132 insertions(+) create mode 100644 deploy/hermes/test_wiki_budget.py create mode 100644 deploy/hermes/verify_wiki_budget.py diff --git a/deploy/hermes/test_wiki_budget.py b/deploy/hermes/test_wiki_budget.py new file mode 100644 index 0000000..e6bf9bd --- /dev/null +++ b/deploy/hermes/test_wiki_budget.py @@ -0,0 +1,90 @@ +import importlib.util +import sys +from pathlib import Path +import unittest + +spec = importlib.util.spec_from_file_location('wiki_budget', Path(__file__).with_name('framework-wiki-budget')/'__init__.py') +m = importlib.util.module_from_spec(spec); sys.modules[spec.name] = m; spec.loader.exec_module(m) + +class BudgetTests(unittest.TestCase): + def setUp(self): + self.now = 0 + self.b = m.Budget(clock=lambda: self.now) + self.ids = {'session_id':'s', 'turn_id':'t', 'task_id':'task'} + self.b.begin(user_message='innolive AI 모델의 변천사를 알려줘',platform='discord',**self.ids) + + def test_intent_preserves_actions_and_deep_research(self): + for q in ['innolive AI 모델의 변천사를 알려줘','Framework 노션 보고서 목록 찾아줘','위키 서버 구조 설명해줘']: + self.assertTrue(m.ordinary_question(q)) + for q in ['innolive 코드를 수정해줘','Framework 모든 문서를 조사하고 정리해줘','위키에 이 결과 보내줘','innolive 코드 분석해줘','Notion 상세히 알려줘','날씨 알려줘']: + self.assertFalse(m.ordinary_question(q),q) + history=[{'role':'user','content':'innolive AI 모델 알려줘'}] + self.assertTrue(m.ordinary_question('그 모델의 실험 보고서를 찾아줘',history)) + + def test_current_tool_pairs_and_saved_history_preserved(self): + history=[{'role':'system','content':'keep'}, {'role':'user','content':'old'}, {'role':'assistant','tool_calls':[{'id':'oldcall'}]}, {'role':'tool','tool_call_id':'oldcall','content':'x'*200000}, {'role':'assistant','content':'old answer'}, {'role':'user','content':'current'}, {'type':'function_call','call_id':'new','name':'read'}, {'type':'function_call_output','call_id':'new','output':'new evidence'}] + before=repr(history) + selected=m.select_messages(history) + self.assertEqual(repr(history),before) + self.assertEqual(selected[-3:],history[-3:]) + self.assertEqual(selected[0],history[0]) + self.assertNotIn('x'*100,repr(selected)) + self.assertNotIn('oldcall',repr(selected)) + + def test_recent_plain_history_budget_and_codex_input(self): + old=[{'role':'user','content':'u'*5000},{'role':'assistant','content':'a'*20000},{'role':'user','content':'current'}] + out=self.b.request({'input':old,'instructions':'unchanged','tools':[{'name':'tool_call'}]},api_call_count=1,**self.ids)['request'] + self.assertLess(len(str(out['input'])),8200) + self.assertEqual(out['instructions'],'unchanged') + self.assertEqual(len(old[1]['content']),20000) + linked=[{'role':'user','content':'Framework AI 문서 알려줘'},{'role':'assistant','content':'a'*20000+'https://app.notion.com/p/123'},{'role':'user','content':'이 문서를 요약해줘'}] + chosen=m.select_messages(linked) + self.assertIn('https://app.notion.com/p/123',repr(chosen)) + self.assertIn('Framework AI 문서 알려줘',repr(chosen)) + + def test_parallel_bridge_budget_and_duplicate_hooks(self): + for i in range(3): + name=m.SOURCE+'get_sources_context' + self.assertIsNone(self.b.tool('tool_call',{'calls':[{'name':name,'arguments':{}}]},tool_call_id=str(i),**self.ids)) + self.assertIsNone(self.b.tool(name,{},tool_call_id=str(i),**self.ids)) + self.assertEqual(self.b.tool(name,{},tool_call_id='4',**self.ids)['action'],'block') + out=self.b.request({'messages':[],'tools':[1],'tool_choice':'auto','parallel_tool_calls':True},**self.ids)['request'] + self.assertEqual(out['tools'],[]);self.assertNotIn('tool_choice',out) + + def test_deadline_forces_answer_and_blocks_more_tools(self): + self.now=91 + self.assertEqual(self.b.request({'input':[],'tools':[1]},**self.ids)['request']['tools'],[]) + self.assertEqual(self.b.tool(m.SOURCE+'search_notion',{},**self.ids)['action'],'block') + + def test_three_tool_rounds_then_final_and_nonmatching_turn_unchanged(self): + self.assertEqual(self.b.request({'input':[],'tools':[1]},api_call_count=4,**self.ids)['request']['tools'],[]) + self.assertIsNone(self.b.request({'input':[]},session_id='other',turn_id='t')) + self.assertIsNone(self.b.begin(user_message='Framework 위키 업데이트해',platform='discord',session_id='other',turn_id='t')) + self.assertIsNone(self.b.begin(user_message='innolive 알려줘',platform='cli',session_id='other',turn_id='t')) + + def test_mutation_blocked_and_new_turn_resets_budget(self): + self.assertEqual(self.b.tool('terminal',{},**self.ids)['action'],'block') + self.b.tool(m.SOURCE+'search_notion',{},**self.ids) + self.b.begin(user_message='위키 구조 알려줘',platform='discord',**self.ids) + self.assertEqual(self.b.turns[self.b.key(**self.ids)].reads,0) + + +class InstallTests(unittest.TestCase): + def test_preserves_config_and_backups_and_repeat_is_idempotent(self): + from tempfile import TemporaryDirectory + import yaml + from configure_wiki_budget import configure + with TemporaryDirectory() as d: + home=Path(d);path=home/'config.yaml' + config={'model':{'default':'gpt-6-luna'},'agent':{'reasoning_effort':'max','max_turns':150},'plugins':{'enabled':['existing'],'entries':{'existing':{'settings':{'keep':True}}}},'mcp_servers':{'other':{'headers':{'Authorization':'private'}}}} + path.write_text(yaml.safe_dump(config));before=path.read_text() + backup=configure(home);configure(home) + result=yaml.safe_load(path.read_text()) + self.assertEqual(result['agent'],config['agent']);self.assertEqual(result['mcp_servers'],config['mcp_servers']) + self.assertEqual(result['plugins']['enabled'],['existing','framework-wiki-budget']) + self.assertEqual(result['plugins']['entries'],config['plugins']['entries']) + self.assertEqual((backup/'config.yaml').read_text(),before) + self.assertEqual((backup/'config.yaml').stat().st_mode & 0o777,0o600) + self.assertTrue((home/'plugins/framework-wiki-budget/plugin.yaml').exists()) + +if __name__=='__main__':unittest.main() diff --git a/deploy/hermes/verify_wiki_budget.py b/deploy/hermes/verify_wiki_budget.py new file mode 100644 index 0000000..cecfcb7 --- /dev/null +++ b/deploy/hermes/verify_wiki_budget.py @@ -0,0 +1,42 @@ +"""Exercise the real Hermes plugin loader and middleware across isolated homes.""" +import argparse +import os +from pathlib import Path +import shutil +import sys +from tempfile import TemporaryDirectory + + +def verify(runtime): + sys.path.insert(0,str(runtime)) + import hermes_bootstrap # noqa: F401 + from hermes_cli.plugins import get_plugin_manager, invoke_hook + from hermes_cli.middleware import apply_llm_request_middleware + original=os.environ.get('HERMES_HOME') + try: + with TemporaryDirectory(prefix='wiki-budget-contract-') as directory: + root=Path(directory);a=root/'a';b=root/'b';a.mkdir();b.mkdir() + shutil.copytree(Path(__file__).with_name('framework-wiki-budget'),a/'plugins/framework-wiki-budget') + (a/'config.yaml').write_text('plugins:\n enabled: [framework-wiki-budget]\n') + (b/'config.yaml').write_text('plugins:\n enabled: []\n') + ids={'session_id':'contract','turn_id':'turn','task_id':'task'} + payload={'input':[{'role':'user','content':'old'},{'role':'assistant','content':'x'*20000},{'role':'user','content':'innolive AI 모델의 변천사를 알려줘'}],'tools':[{'name':'tool_call'}]} + for home,enabled in [(a,True),(b,False),(a,True)]: + os.environ['HERMES_HOME']=str(home) + manager=get_plugin_manager();manager.discover_and_load() + invoke_hook('pre_llm_call',user_message='innolive AI 모델의 변천사를 알려줘',platform='discord',conversation_history=[],**ids) + actual=apply_llm_request_middleware(payload,api_call_count=4,**ids).payload + if enabled: + assert len(str(actual['input']))<5000 and actual['tools']==[] + blocked=invoke_hook('pre_tool_call',tool_name='terminal',args={},tool_call_id='blocked',**ids) + assert any(row and row.get('action')=='block' for row in blocked) + else: assert actual==payload + assert len(payload['input'][1]['content'])==20000 + print('Hermes plugin discovery, middleware, tool policy and A -> B -> A home isolation passed') + finally: + if original is None:os.environ.pop('HERMES_HOME',None) + else:os.environ['HERMES_HOME']=original + +if __name__=='__main__': + parser=argparse.ArgumentParser();parser.add_argument('--runtime',type=Path,default=Path('/home/chaeyn/.hermes/hermes-agent')) + verify(parser.parse_args().runtime) From 13858cc1434b4c058c428f4170cde47547a7b0ae Mon Sep 17 00:00:00 2001 From: chaeyn Date: Sat, 3 Oct 2026 10:22:19 +0900 Subject: [PATCH 3/5] =?UTF-8?q?docs:=20=EC=9D=BC=EB=B0=98=20=EB=AC=B8?= =?UTF-8?q?=EC=84=9C=20=EC=A7=88=EB=AC=B8=20=EC=A0=9C=ED=95=9C=EA=B3=BC=20?= =?UTF-8?q?=ED=94=8C=EB=9F=AC=EA=B7=B8=EC=9D=B8=20=EB=B0=B0=EC=B9=98=20?= =?UTF-8?q?=EC=A0=88=EC=B0=A8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/README.md b/README.md index 2e396bb..34c6e27 100644 --- a/README.md +++ b/README.md @@ -62,3 +62,14 @@ docker compose --env-file .env -f deploy/compose.yaml -f deploy/compose.hermes.y ``` `/healthz`는 브리지 프로세스 상태만 검사합니다. 인증과 모델 사용 가능 여부는 실제 요약 요청으로 검증해야 합니다. 요청이 실패해도 유료 OpenAI API로 자동 전환하지 않습니다. + +## 일반 문서 질문의 조회 제한 + +`deploy/hermes/configure_wiki_budget.py`는 사용자 Hermes 홈에 `framework-wiki-budget` 플러그인을 설치하고 기존 설정을 비공개 백업합니다. 배치 후 Gateway를 재시작합니다. 플러그인은 Discord의 일반 Framework·InnoLive 문서 질문에만 적용됩니다. 심층 조사와 수정·배포 요청은 기존 경로를 사용합니다. + +일반 질문은 문서 조회를 3회로 제한하고, 네 번째 모델 요청부터 도구 없이 답변합니다. 90초가 지나면 추가 조회를 중단합니다. 이미 진행 중인 모델·문서 요청은 이 제한으로 취소하지 않으므로 전체 응답 시간의 상한은 아닙니다. 과거 도구 출력은 요청에서 제외하고 최근 일반 대화는 8,000자 이내로 선택합니다. 이번 질문의 도구 호출과 결과 쌍, 시스템 지침, 저장된 대화 기록은 보존합니다. 기존 `gpt-6-luna`·`max` 추론 설정을 유지합니다. + +```bash +python deploy/hermes/configure_wiki_budget.py --hermes-home /home/chaeyn/.hermes +python -m unittest discover -s deploy/hermes -p 'test_*.py' +``` From 2d8ba7f29564513121b537cc14961253f25d3d6b Mon Sep 17 00:00:00 2001 From: chaeyn Date: Sat, 3 Oct 2026 10:27:36 +0900 Subject: [PATCH 4/5] =?UTF-8?q?fix:=20=EB=8F=84=EA=B5=AC=20=ED=98=B8?= =?UTF-8?q?=EC=B6=9C=EC=9D=B4=20=EB=A7=8E=EC=9D=80=20=EB=8C=80=ED=99=94?= =?UTF-8?q?=EC=9D=98=20=ED=9B=84=EC=86=8D=20=EB=AC=B8=EC=84=9C=20=EC=A7=88?= =?UTF-8?q?=EB=AC=B8=20=EB=B2=94=EC=9C=84=20=EC=9C=A0=EC=A7=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/hermes/framework-wiki-budget/__init__.py | 2 +- deploy/hermes/test_wiki_budget.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/deploy/hermes/framework-wiki-budget/__init__.py b/deploy/hermes/framework-wiki-budget/__init__.py index 2b59a79..a58cbdb 100644 --- a/deploy/hermes/framework-wiki-budget/__init__.py +++ b/deploy/hermes/framework-wiki-budget/__init__.py @@ -27,7 +27,7 @@ def text_of(message): def ordinary_question(text, history=()): if DEEP_OR_WRITE.search(text) or not LOOKUP.search(text): return False if TOPIC.search(text): return True - return bool(FOLLOWUP.search(text) and any(TOPIC.search(text_of(m)) for m in history[-6:] if m.get('role') == 'user')) + return bool(FOLLOWUP.search(text) and any(TOPIC.search(text_of(m)) for m in [row for row in history if row.get('role') == 'user'][-3:])) def select_messages(messages, old_chars=8000): diff --git a/deploy/hermes/test_wiki_budget.py b/deploy/hermes/test_wiki_budget.py index e6bf9bd..854347d 100644 --- a/deploy/hermes/test_wiki_budget.py +++ b/deploy/hermes/test_wiki_budget.py @@ -20,6 +20,8 @@ def test_intent_preserves_actions_and_deep_research(self): self.assertFalse(m.ordinary_question(q),q) history=[{'role':'user','content':'innolive AI 모델 알려줘'}] self.assertTrue(m.ordinary_question('그 모델의 실험 보고서를 찾아줘',history)) + history += [{'role':'tool','content':'evidence'} for _ in range(12)] + self.assertTrue(m.ordinary_question('그 모델의 실험 보고서를 찾아줘',history)) def test_current_tool_pairs_and_saved_history_preserved(self): history=[{'role':'system','content':'keep'}, {'role':'user','content':'old'}, {'role':'assistant','tool_calls':[{'id':'oldcall'}]}, {'role':'tool','tool_call_id':'oldcall','content':'x'*200000}, {'role':'assistant','content':'old answer'}, {'role':'user','content':'current'}, {'type':'function_call','call_id':'new','name':'read'}, {'type':'function_call_output','call_id':'new','output':'new evidence'}] From 9d26ce75f8d85f769e6040e0557edee099057569 Mon Sep 17 00:00:00 2001 From: chaeyn Date: Sat, 3 Oct 2026 10:29:15 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix:=20=EC=8B=A4=EC=A0=9C=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=EC=99=80=20=EC=84=9C=EB=B2=84=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=EC=9D=98=20=EA=B8=B0=EC=A1=B4=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=B4=EC=A1=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- deploy/hermes/framework-wiki-budget/__init__.py | 2 +- deploy/hermes/test_wiki_budget.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deploy/hermes/framework-wiki-budget/__init__.py b/deploy/hermes/framework-wiki-budget/__init__.py index a58cbdb..ea0b56a 100644 --- a/deploy/hermes/framework-wiki-budget/__init__.py +++ b/deploy/hermes/framework-wiki-budget/__init__.py @@ -9,7 +9,7 @@ log = logging.getLogger(__name__) TOPIC = re.compile(r'framework|innolive|inno live|이노라이브|위키|노션|notion', re.I) LOOKUP = re.compile(r'알려|설명|요약|정리|찾아|변천|변화|현황|목록|링크|무엇|뭐|어떻|읽|볼 수|show|explain|summari|history|list|find', re.I) -DEEP_OR_WRITE = re.compile(r'심층|자세히|상세히|꼼꼼|전부|모든 문서|끝까지|deep research|exhaustive|고쳐|수정해|수정해줘|작성해|구현해|코드.*(?:분석|확인|검토)|배포|커밋|실행해|갱신|업데이트해|삭제|등록해|보내|전송', re.I) +DEEP_OR_WRITE = re.compile(r'심층|자세히|상세히|꼼꼼|전부|모든 문서|끝까지|deep research|exhaustive|코드|로그|실서버|운영 서버|GPU 서버|실제 실행|고쳐|수정해|수정해줘|작성해|구현해|코드.*(?:분석|확인|검토)|배포|커밋|실행해|갱신|업데이트해|삭제|등록해|보내|전송', re.I) FOLLOWUP = re.compile(r'이 문서|그 문서|이 모델|그 모델|방금|앞서|이 중|그중|그러면|그럼|후속') SOURCE = 'mcp__framework_wiki__' READS = {'get_sources_context', 'search_notion', 'read_notion_page', 'search', 'get_context', 'get_note_outline', 'read_sections', 'get_wiki_status', 'list_notes'} diff --git a/deploy/hermes/test_wiki_budget.py b/deploy/hermes/test_wiki_budget.py index 854347d..4633a74 100644 --- a/deploy/hermes/test_wiki_budget.py +++ b/deploy/hermes/test_wiki_budget.py @@ -16,7 +16,7 @@ def setUp(self): def test_intent_preserves_actions_and_deep_research(self): for q in ['innolive AI 모델의 변천사를 알려줘','Framework 노션 보고서 목록 찾아줘','위키 서버 구조 설명해줘']: self.assertTrue(m.ordinary_question(q)) - for q in ['innolive 코드를 수정해줘','Framework 모든 문서를 조사하고 정리해줘','위키에 이 결과 보내줘','innolive 코드 분석해줘','Notion 상세히 알려줘','날씨 알려줘']: + for q in ['innolive 코드를 수정해줘','Framework 모든 문서를 조사하고 정리해줘','위키에 이 결과 보내줘','innolive 코드 분석해줘','innolive 코드 동작 알려줘','innolive GPU 서버 상태 알려줘','Notion 상세히 알려줘','날씨 알려줘']: self.assertFalse(m.ordinary_question(q),q) history=[{'role':'user','content':'innolive AI 모델 알려줘'}] self.assertTrue(m.ordinary_question('그 모델의 실험 보고서를 찾아줘',history))