|
| 1 | +"""FsCli system prompt""" |
| 2 | + |
| 3 | +from datetime import datetime |
| 4 | +from pathlib import Path |
| 5 | + |
| 6 | +from ...core.enumeration import Role, ChunkEnum |
| 7 | +from ...core.op import BaseReactStream |
| 8 | +from ...core.schema import Message, StreamChunk |
| 9 | + |
| 10 | + |
| 11 | +class FsCli(BaseReactStream): |
| 12 | + """FsCli agent with system prompt.""" |
| 13 | + |
| 14 | + def __init__( |
| 15 | + self, |
| 16 | + working_dir: str, |
| 17 | + context_window_tokens: int = 128000, |
| 18 | + reserve_tokens: int = 36000, |
| 19 | + keep_recent_tokens: int = 20000, |
| 20 | + hybrid_enabled: bool = True, |
| 21 | + hybrid_vector_weight: float = 0.7, |
| 22 | + hybrid_text_weight: float = 0.3, |
| 23 | + hybrid_candidate_multiplier: float = 3.0, |
| 24 | + **kwargs, |
| 25 | + ): |
| 26 | + super().__init__(**kwargs) |
| 27 | + self.working_dir: str = working_dir |
| 28 | + Path(self.working_dir).mkdir(parents=True, exist_ok=True) |
| 29 | + self.context_window_tokens: int = context_window_tokens |
| 30 | + self.reserve_tokens: int = reserve_tokens |
| 31 | + self.keep_recent_tokens: int = keep_recent_tokens |
| 32 | + self.hybrid_enabled: bool = hybrid_enabled |
| 33 | + self.hybrid_vector_weight: float = hybrid_vector_weight |
| 34 | + self.hybrid_text_weight: float = hybrid_text_weight |
| 35 | + self.hybrid_candidate_multiplier: float = hybrid_candidate_multiplier |
| 36 | + |
| 37 | + self.messages: list[Message] = [] |
| 38 | + self.previous_summary: str = "" |
| 39 | + |
| 40 | + async def reset(self) -> str: |
| 41 | + """Reset conversation history using summary. |
| 42 | +
|
| 43 | + Summarizes current messages to memory files and clears history. |
| 44 | + """ |
| 45 | + if not self.messages: |
| 46 | + self.messages.clear() |
| 47 | + self.previous_summary = "" |
| 48 | + return "No history to reset." |
| 49 | + |
| 50 | + # Import required modules |
| 51 | + from ..fs import FsSummarizer |
| 52 | + |
| 53 | + # Summarize current conversation and save to memory files |
| 54 | + current_date = datetime.now().strftime("%Y-%m-%d") |
| 55 | + summarizer = FsSummarizer(tools=self.tools, working_dir=self.working_dir) |
| 56 | + |
| 57 | + result = await summarizer.call(messages=self.messages, date=current_date, service_context=self.service_context) |
| 58 | + self.messages.clear() |
| 59 | + self.previous_summary = "" |
| 60 | + return f"History saved to memory files and reset. Result: {result.get('answer', 'Done')}" |
| 61 | + |
| 62 | + async def context_check(self) -> dict: |
| 63 | + """Check if messages exceed token limits.""" |
| 64 | + # Import required modules |
| 65 | + from ..fs import FsContextChecker |
| 66 | + |
| 67 | + # Step 1: Check and find cut point |
| 68 | + checker = FsContextChecker( |
| 69 | + context_window_tokens=self.context_window_tokens, |
| 70 | + reserve_tokens=self.reserve_tokens, |
| 71 | + keep_recent_tokens=self.keep_recent_tokens, |
| 72 | + ) |
| 73 | + return await checker.call(messages=self.messages, service_context=self.service_context) |
| 74 | + |
| 75 | + async def compact(self, force_compact: bool = False) -> str: |
| 76 | + """Compact history then reset. |
| 77 | +
|
| 78 | + First compacts messages if they exceed token limits (generating a summary), |
| 79 | + then calls reset_history to save to files and clear. |
| 80 | +
|
| 81 | + Args: |
| 82 | + force_compact: If True, force compaction of all messages into summary |
| 83 | +
|
| 84 | + Returns: |
| 85 | + str: Summary of compaction result |
| 86 | + """ |
| 87 | + if not self.messages: |
| 88 | + return "No history to compact." |
| 89 | + |
| 90 | + # Import required modules |
| 91 | + from ..fs import FsCompactor |
| 92 | + |
| 93 | + # Step 1: Check and find cut point |
| 94 | + cut_result = await self.context_check() |
| 95 | + tokens_before = cut_result.get("token_count", 0) |
| 96 | + |
| 97 | + if force_compact: |
| 98 | + # Force compact: summarize all messages, leave only summary |
| 99 | + messages_to_summarize = self.messages |
| 100 | + turn_prefix_messages = [] |
| 101 | + left_messages = [] |
| 102 | + elif not cut_result.get("needs_compaction", False): |
| 103 | + # No compaction needed |
| 104 | + return "History is within token limits, no compaction needed." |
| 105 | + else: |
| 106 | + # Normal compaction: use cut point result |
| 107 | + messages_to_summarize = cut_result.get("messages_to_summarize", []) |
| 108 | + turn_prefix_messages = cut_result.get("turn_prefix_messages", []) |
| 109 | + left_messages = cut_result.get("left_messages", []) |
| 110 | + |
| 111 | + # Step 2: Generate summary via Compactor |
| 112 | + compactor = FsCompactor() |
| 113 | + summary_content = await compactor.call( |
| 114 | + messages_to_summarize=messages_to_summarize, |
| 115 | + turn_prefix_messages=turn_prefix_messages, |
| 116 | + previous_summary=self.previous_summary, |
| 117 | + service_context=self.service_context, |
| 118 | + ) |
| 119 | + |
| 120 | + # Step 3: Assemble final messages |
| 121 | + summary_message = Message(role=Role.USER, content=summary_content) |
| 122 | + self.messages = [summary_message] + left_messages |
| 123 | + self.previous_summary = summary_content |
| 124 | + |
| 125 | + # Step 4: Call reset_history to save and clear |
| 126 | + reset_result = await self.reset() |
| 127 | + return f"History compacted from {tokens_before} tokens. {reset_result}" |
| 128 | + |
| 129 | + async def build_messages(self) -> list[Message]: |
| 130 | + """Build system prompt message.""" |
| 131 | + current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S %A") |
| 132 | + |
| 133 | + system_prompt = self.prompt_format( |
| 134 | + "system_prompt", |
| 135 | + workspace_dir=self.working_dir, |
| 136 | + current_time=current_time, |
| 137 | + has_previous_summary=bool(self.previous_summary), |
| 138 | + previous_summary=self.previous_summary or "", |
| 139 | + ) |
| 140 | + |
| 141 | + return [ |
| 142 | + Message(role=Role.SYSTEM, content=system_prompt), |
| 143 | + *self.messages, |
| 144 | + Message(role=Role.USER, content=self.context.query), |
| 145 | + ] |
| 146 | + |
| 147 | + async def execute(self): |
| 148 | + """Execute the agent.""" |
| 149 | + messages = await self.build_messages() |
| 150 | + |
| 151 | + t_tools, messages, success = await self.react(messages, self.tools) |
| 152 | + |
| 153 | + # Update self.messages: react() returns [SYSTEM, ...history...], |
| 154 | + # so we remove the first SYSTEM message |
| 155 | + self.messages = messages[1:] |
| 156 | + |
| 157 | + # Emit final done signal |
| 158 | + await self.context.add_stream_chunk( |
| 159 | + StreamChunk( |
| 160 | + chunk_type=ChunkEnum.DONE, |
| 161 | + chunk="", |
| 162 | + metadata={ |
| 163 | + "success": success, |
| 164 | + "total_steps": len(t_tools), |
| 165 | + }, |
| 166 | + ), |
| 167 | + ) |
| 168 | + |
| 169 | + return { |
| 170 | + "answer": messages[-1].content if success else "", |
| 171 | + "success": success, |
| 172 | + "messages": messages, |
| 173 | + "tools": t_tools, |
| 174 | + } |
0 commit comments