|
5 | 5 | from typing import TYPE_CHECKING, Any, Dict, List, Optional |
6 | 6 |
|
7 | 7 | from openspace.agents.base import BaseAgent |
| 8 | +from openspace.agents.grounding.context import ( |
| 9 | + clear_skill_context as _clear_skill_context, |
| 10 | + has_skill_context as _has_skill_context, |
| 11 | + set_skill_context as _set_skill_context, |
| 12 | + set_skill_registry as _set_skill_registry, |
| 13 | +) |
| 14 | +from openspace.agents.grounding.messages import ( |
| 15 | + _MAX_SINGLE_CONTENT_CHARS, |
| 16 | + cap_message_content as _cap_message_content, |
| 17 | + truncate_messages as _truncate_messages_impl, |
| 18 | +) |
8 | 19 | from openspace.grounding.core.types import BackendType, ToolResult |
9 | 20 | from openspace.platforms.screenshot import ScreenshotClient |
10 | 21 | from openspace.prompts import GroundingAgentPrompts |
@@ -86,116 +97,35 @@ def set_skill_context( |
86 | 97 | context: str, |
87 | 98 | skill_ids: Optional[List[str]] = None, |
88 | 99 | ) -> None: |
89 | | - """Inject skill guidance into the agent's system prompt. |
90 | | -
|
91 | | - Called by ``OpenSpace.execute()`` before ``process()`` when skills |
92 | | - are matched. The context is a formatted string built by |
93 | | - ``SkillRegistry.build_context_injection()``. |
94 | | -
|
95 | | - Args: |
96 | | - context: Formatted skill content for system prompt injection. |
97 | | - skill_ids: skill_id values of injected skills. |
98 | | - """ |
99 | | - self._skill_context = context if context else None |
100 | | - self._active_skill_ids = skill_ids or [] |
101 | | - if self._skill_context: |
102 | | - logger.info(f"Skill context set: {', '.join(self._active_skill_ids) or '(unnamed)'}") |
| 100 | + """Inject skill guidance into the agent's system prompt.""" |
| 101 | + return _set_skill_context(self, context, skill_ids) |
103 | 102 |
|
104 | 103 | def clear_skill_context(self) -> None: |
105 | 104 | """Remove skill guidance (used before fallback execution).""" |
106 | | - if self._skill_context: |
107 | | - logger.info(f"Skill context cleared (was: {', '.join(self._active_skill_ids)})") |
108 | | - self._skill_context = None |
109 | | - self._active_skill_ids = [] |
| 105 | + return _clear_skill_context(self) |
110 | 106 |
|
111 | 107 | @property |
112 | 108 | def has_skill_context(self) -> bool: |
113 | | - return self._skill_context is not None |
| 109 | + return _has_skill_context(self) |
114 | 110 |
|
115 | 111 | def set_skill_registry(self, registry: Optional["SkillRegistry"]) -> None: |
116 | 112 | """Attach a SkillRegistry so the agent can offer ``retrieve_skill`` as a tool.""" |
117 | | - self._skill_registry = registry |
118 | | - if registry: |
119 | | - count = len(registry.list_skills()) |
120 | | - logger.info(f"Skill registry attached ({count} skill(s) available for mid-iteration retrieval)") |
| 113 | + return _set_skill_registry(self, registry) |
121 | 114 |
|
122 | | - _MAX_SINGLE_CONTENT_CHARS = 30_000 |
| 115 | + _MAX_SINGLE_CONTENT_CHARS = _MAX_SINGLE_CONTENT_CHARS |
123 | 116 |
|
124 | 117 | @classmethod |
125 | 118 | def _cap_message_content(cls, messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]: |
126 | | - """Truncate oversized individual message contents in-place. |
127 | | -
|
128 | | - Targets tool-result messages and assistant messages that can |
129 | | - carry enormous file contents (read_file on large CSVs/scripts). |
130 | | - System messages and the first user instruction are never touched. |
131 | | - """ |
132 | | - cap = cls._MAX_SINGLE_CONTENT_CHARS |
133 | | - trimmed = 0 |
134 | | - for msg in messages: |
135 | | - content = msg.get("content") |
136 | | - if not isinstance(content, str) or len(content) <= cap: |
137 | | - continue |
138 | | - if msg.get("role") == "system": |
139 | | - continue |
140 | | - original_len = len(content) |
141 | | - msg["content"] = ( |
142 | | - content[: cap // 2] |
143 | | - + f"\n\n... [truncated {original_len - cap:,} chars] ...\n\n" |
144 | | - + content[-(cap // 2) :] |
145 | | - ) |
146 | | - trimmed += 1 |
147 | | - if trimmed: |
148 | | - logger.info(f"Capped {trimmed} oversized message(s) to {cap:,} chars each") |
149 | | - return messages |
| 119 | + """Truncate oversized individual message contents in-place.""" |
| 120 | + return _cap_message_content(messages, cls._MAX_SINGLE_CONTENT_CHARS) |
150 | 121 |
|
151 | 122 | def _truncate_messages( |
152 | 123 | self, messages: List[Dict[str, Any]], keep_recent: int = 8, max_tokens_estimate: int = 120000 |
153 | 124 | ) -> List[Dict[str, Any]]: |
154 | | - # First: cap any single oversized message to prevent one huge |
155 | | - # tool-result from dominating the context window. |
156 | | - messages = self._cap_message_content(messages) |
157 | | - |
158 | | - if len(messages) <= keep_recent + 2: # +2 for system and initial user |
159 | | - return messages |
160 | | - |
161 | | - total_text = json.dumps(messages, ensure_ascii=False) |
162 | | - estimated_tokens = len(total_text) // 4 |
163 | | - |
164 | | - if estimated_tokens < max_tokens_estimate: |
165 | | - return messages |
166 | | - |
167 | | - logger.info( |
168 | | - f"Truncating message history: {len(messages)} messages, " |
169 | | - f"~{estimated_tokens:,} tokens -> keeping recent {keep_recent} rounds" |
170 | | - ) |
171 | | - |
172 | | - system_messages = [] |
173 | | - user_instruction = None |
174 | | - conversation_messages = [] |
175 | | - |
176 | | - for msg in messages: |
177 | | - role = msg.get("role") |
178 | | - if role == "system": |
179 | | - system_messages.append(msg) |
180 | | - elif role == "user" and user_instruction is None: |
181 | | - user_instruction = msg |
182 | | - else: |
183 | | - conversation_messages.append(msg) |
184 | | - |
185 | | - recent_messages = conversation_messages[-(keep_recent * 2) :] if conversation_messages else [] |
186 | | - |
187 | | - truncated = system_messages.copy() |
188 | | - if user_instruction: |
189 | | - truncated.append(user_instruction) |
190 | | - truncated.extend(recent_messages) |
191 | | - |
192 | | - logger.info( |
193 | | - f"After truncation: {len(truncated)} messages, " |
194 | | - f"~{len(json.dumps(truncated, ensure_ascii=False)) // 4:,} tokens (estimated)" |
| 125 | + return _truncate_messages_impl( |
| 126 | + messages, keep_recent, max_tokens_estimate, cap=self._MAX_SINGLE_CONTENT_CHARS |
195 | 127 | ) |
196 | 128 |
|
197 | | - return truncated |
198 | | - |
199 | 129 | async def process(self, context: Dict[str, Any]) -> Dict[str, Any]: |
200 | 130 | """ |
201 | 131 | Process a task execution request with multi-round iteration control. |
|
0 commit comments