mirror of
https://github.com/HKUDS/OpenSpace.git
synced 2026-10-09 03:17:51 +00:00
commit
b0021b46bb
11 changed files with 87 additions and 20 deletions
|
|
@ -322,7 +322,13 @@ async def refresh_mcp_cache(config_path: Optional[str] = None):
|
|||
def _load_config(args) -> OpenSpaceConfig:
|
||||
"""Load configuration"""
|
||||
import os
|
||||
from openspace.host_detection import build_llm_kwargs, build_grounding_config_path
|
||||
from openspace.host_detection import (
|
||||
build_grounding_config_path,
|
||||
build_llm_kwargs,
|
||||
load_runtime_env,
|
||||
)
|
||||
|
||||
load_runtime_env()
|
||||
|
||||
cli_overrides = {}
|
||||
if args.max_iterations is not None:
|
||||
|
|
@ -494,4 +500,4 @@ def run_main():
|
|||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_main()
|
||||
run_main()
|
||||
|
|
|
|||
|
|
@ -185,8 +185,17 @@ class GroundingAgent(BaseAgent):
|
|||
conversation_messages.append(msg)
|
||||
|
||||
recent_messages = conversation_messages[-(keep_recent * 2):] if conversation_messages else []
|
||||
|
||||
|
||||
truncated = system_messages.copy()
|
||||
dropped = len(conversation_messages) - len(recent_messages)
|
||||
if dropped > 0:
|
||||
truncated.append({
|
||||
"role": "system",
|
||||
"content": (
|
||||
f"{self._ITERATION_GUIDANCE_PREFIX} {dropped} earlier messages were "
|
||||
"truncated to save context. The original task instruction is preserved below."
|
||||
),
|
||||
})
|
||||
if user_instruction:
|
||||
truncated.append(user_instruction)
|
||||
truncated.extend(recent_messages)
|
||||
|
|
@ -350,7 +359,7 @@ class GroundingAgent(BaseAgent):
|
|||
f"Tool results: {len(tool_results_this_iteration)}, "
|
||||
f"Content length: {len(assistant_content)} chars")
|
||||
|
||||
if len(assistant_content) > 0:
|
||||
if len(assistant_content.strip()) > 0:
|
||||
logger.info(f"Iteration {current_iteration} - Assistant content preview: {repr(assistant_content[:300])}")
|
||||
consecutive_empty_responses = 0 # Reset counter on valid response
|
||||
else:
|
||||
|
|
@ -747,9 +756,19 @@ class GroundingAgent(BaseAgent):
|
|||
}
|
||||
})
|
||||
|
||||
# Use dedicated visual analysis model if configured, otherwise use main LLM model
|
||||
# Resolve visual-model credentials independently when the visual
|
||||
# model differs from the main reasoning model.
|
||||
visual_model = self._visual_analysis_model or (self._llm_client.model if self._llm_client else "openrouter/anthropic/claude-sonnet-4.5")
|
||||
_llm_extra = getattr(self._llm_client, 'litellm_kwargs', {}) if self._llm_client else {}
|
||||
_llm_extra = {}
|
||||
if self._llm_client and visual_model == self._llm_client.model:
|
||||
_llm_extra = getattr(self._llm_client, 'litellm_kwargs', {}) or {}
|
||||
elif self._visual_analysis_model:
|
||||
try:
|
||||
from openspace.host_detection import build_llm_kwargs
|
||||
visual_model, _llm_extra = build_llm_kwargs(visual_model)
|
||||
except Exception as e:
|
||||
logger.debug(f"Failed to resolve dedicated visual model credentials: {e}")
|
||||
_llm_extra = {}
|
||||
response = await asyncio.wait_for(
|
||||
litellm.acompletion(
|
||||
model=visual_model,
|
||||
|
|
@ -1218,4 +1237,4 @@ class GroundingAgent(BaseAgent):
|
|||
"step": self.step,
|
||||
"instruction": instruction,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,11 @@ Supported host agents:
|
|||
import logging
|
||||
from typing import Dict, Optional
|
||||
|
||||
from openspace.host_detection.resolver import build_llm_kwargs, build_grounding_config_path
|
||||
from openspace.host_detection.resolver import (
|
||||
build_grounding_config_path,
|
||||
build_llm_kwargs,
|
||||
load_runtime_env,
|
||||
)
|
||||
from openspace.host_detection.nanobot import (
|
||||
get_openai_api_key as _nanobot_get_openai_api_key,
|
||||
read_nanobot_mcp_env,
|
||||
|
|
@ -29,6 +33,7 @@ from openspace.host_detection.nanobot import (
|
|||
)
|
||||
from openspace.host_detection.openclaw import (
|
||||
get_openclaw_openai_api_key as _openclaw_get_openai_api_key,
|
||||
is_openclaw_host,
|
||||
read_openclaw_skill_env,
|
||||
try_read_openclaw_config,
|
||||
)
|
||||
|
|
@ -80,10 +85,12 @@ def get_openai_api_key() -> Optional[str]:
|
|||
__all__ = [
|
||||
"build_llm_kwargs",
|
||||
"build_grounding_config_path",
|
||||
"load_runtime_env",
|
||||
"get_openai_api_key",
|
||||
"read_host_mcp_env",
|
||||
"read_nanobot_mcp_env",
|
||||
"try_read_nanobot_config",
|
||||
"is_openclaw_host",
|
||||
"read_openclaw_skill_env",
|
||||
"try_read_openclaw_config",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -279,6 +279,13 @@ def get_openclaw_openai_api_key() -> Optional[str]:
|
|||
return None
|
||||
|
||||
|
||||
def is_openclaw_host() -> bool:
|
||||
"""Detect if the current environment is running under OpenClaw."""
|
||||
if os.environ.get("OPENCLAW_STATE_DIR") or os.environ.get("OPENCLAW_CONFIG_PATH"):
|
||||
return True
|
||||
return _resolve_openclaw_config_path() is not None
|
||||
|
||||
|
||||
def try_read_openclaw_config(model: str) -> Optional[Dict[str, Any]]:
|
||||
"""Read LLM credentials from OpenClaw's env-style config blocks."""
|
||||
env_block = _get_openclaw_env("openspace")
|
||||
|
|
@ -306,4 +313,3 @@ def try_read_openclaw_config(model: str) -> Optional[Dict[str, Any]]:
|
|||
return result
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -62,6 +62,11 @@ def _load_env_once() -> None:
|
|||
load_dotenv()
|
||||
|
||||
|
||||
def load_runtime_env() -> None:
|
||||
"""Public wrapper for one-time runtime .env loading."""
|
||||
_load_env_once()
|
||||
|
||||
|
||||
def _pick_first_env(names: tuple[str, ...]) -> str:
|
||||
for name in names:
|
||||
value = os.environ.get(name, "").strip()
|
||||
|
|
|
|||
|
|
@ -9,8 +9,9 @@ from openspace.grounding.core.types import ToolSchema, ToolResult, ToolStatus
|
|||
from openspace.grounding.core.tool import BaseTool
|
||||
from openspace.utils.logging import Logger
|
||||
|
||||
# .env loading is centralized in host_detection.resolver._load_env_once()
|
||||
# which is called by build_llm_kwargs / build_grounding_config_path.
|
||||
# .env loading is centralized in host_detection.resolver.load_runtime_env().
|
||||
# CLI/MCP entrypoints call it before reading startup env vars, and the
|
||||
# resolver helpers also call it defensively.
|
||||
|
||||
# Disable LiteLLM verbose logging to prevent stdout blocking with large tool schemas
|
||||
litellm.set_verbose = False
|
||||
|
|
@ -169,9 +170,10 @@ def _infer_backend_from_tool_name(tool_name: str) -> Optional[str]:
|
|||
if not tool_name or not isinstance(tool_name, str):
|
||||
return None
|
||||
name = tool_name.strip()
|
||||
# Dedup format: "server__toolname" -> use suffix
|
||||
# Dedup format: "server__toolname" -> use suffix.
|
||||
# Use rsplit to handle server names that themselves contain "__".
|
||||
if "__" in name:
|
||||
name = name.split("__", 1)[-1]
|
||||
name = name.rsplit("__", 1)[-1]
|
||||
shell_tools = {"shell_agent", "read_file", "write_file", "list_dir", "run_shell"}
|
||||
if name in shell_tools:
|
||||
return "shell"
|
||||
|
|
|
|||
|
|
@ -137,7 +137,13 @@ async def _get_openspace():
|
|||
|
||||
logger.info("Initializing OpenSpace engine ...")
|
||||
from openspace.tool_layer import OpenSpace, OpenSpaceConfig
|
||||
from openspace.host_detection import build_llm_kwargs, build_grounding_config_path
|
||||
from openspace.host_detection import (
|
||||
build_grounding_config_path,
|
||||
build_llm_kwargs,
|
||||
load_runtime_env,
|
||||
)
|
||||
|
||||
load_runtime_env()
|
||||
|
||||
env_model = os.environ.get("OPENSPACE_MODEL", "")
|
||||
workspace = os.environ.get("OPENSPACE_WORKSPACE")
|
||||
|
|
|
|||
|
|
@ -668,8 +668,9 @@ class RecordingManager:
|
|||
if not tool_name or not isinstance(tool_name, str):
|
||||
return None
|
||||
name = tool_name.strip()
|
||||
# Use rsplit to handle server names that themselves contain "__".
|
||||
if "__" in name:
|
||||
name = name.split("__", 1)[-1]
|
||||
name = name.rsplit("__", 1)[-1]
|
||||
shell_tools = {"shell_agent", "read_file", "write_file", "list_dir", "run_shell"}
|
||||
if name in shell_tools:
|
||||
return "shell"
|
||||
|
|
|
|||
|
|
@ -84,13 +84,17 @@ def _correct_skill_ids(
|
|||
if prefix and k.split("__")[0] == prefix
|
||||
]
|
||||
|
||||
best, best_dist = None, 4 # threshold: edit distance ≤ 3
|
||||
# Adaptive threshold: tighten when many candidates share the prefix
|
||||
max_dist = 2 if len(candidates) > 20 else 4 # ≤1 or ≤3
|
||||
best, best_dist, ambiguous = None, max_dist, False
|
||||
for cand in candidates:
|
||||
d = _edit_distance(raw_id, cand)
|
||||
if d < best_dist:
|
||||
best, best_dist = cand, d
|
||||
best, best_dist, ambiguous = cand, d, False
|
||||
elif d == best_dist and cand != best:
|
||||
ambiguous = True # multiple candidates at same distance
|
||||
|
||||
if best is not None:
|
||||
if best is not None and not ambiguous:
|
||||
logger.info(
|
||||
f"Corrected LLM skill ID: {raw_id!r} → {best!r} "
|
||||
f"(edit_distance={best_dist})"
|
||||
|
|
|
|||
|
|
@ -201,6 +201,7 @@ class SkillEvolver:
|
|||
# evolved for each degraded tool. Keyed by tool_key.
|
||||
# Pruned when a tool leaves the problematic list (= recovered).
|
||||
self._addressed_degradations: Dict[str, Set[str]] = {}
|
||||
self._degradation_lock = asyncio.Lock()
|
||||
|
||||
# Track background tasks so they can be awaited on shutdown.
|
||||
self._background_tasks: Set[asyncio.Task] = set()
|
||||
|
|
@ -219,7 +220,10 @@ class SkillEvolver:
|
|||
f"Waiting for {len(self._background_tasks)} background "
|
||||
f"evolution task(s) to finish..."
|
||||
)
|
||||
await asyncio.gather(*self._background_tasks, return_exceptions=True)
|
||||
results = await asyncio.gather(*self._background_tasks, return_exceptions=True)
|
||||
for r in results:
|
||||
if isinstance(r, BaseException):
|
||||
logger.warning(f"Background evolution task failed during shutdown: {r}")
|
||||
self._background_tasks.clear()
|
||||
|
||||
async def evolve(self, ctx: EvolutionContext) -> Optional[SkillRecord]:
|
||||
|
|
@ -306,6 +310,13 @@ class SkillEvolver:
|
|||
if not problematic_tools:
|
||||
return []
|
||||
|
||||
async with self._degradation_lock:
|
||||
return await self._process_tool_degradation_locked(problematic_tools)
|
||||
|
||||
async def _process_tool_degradation_locked(
|
||||
self, problematic_tools: List,
|
||||
) -> List[SkillRecord]:
|
||||
"""Inner body of process_tool_degradation, called under _degradation_lock."""
|
||||
# Prune recovered tools: if a tool_key used to be tracked but is
|
||||
# no longer in the current problematic list, it recovered — clear
|
||||
# its addressed set so future re-degradation gets a fresh pass.
|
||||
|
|
|
|||
|
|
@ -934,4 +934,4 @@ class OpenSpace:
|
|||
if self._running:
|
||||
status = "running"
|
||||
backends = ", ".join(self.config.backend_scope) if self.config.backend_scope else "all"
|
||||
return f"<OpenSpace(status={status}, backends={backends}, model={self.config.llm_model})>"
|
||||
return f"<OpenSpace(status={status}, backends={backends}, model={self.config.llm_model})>"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue