feat: multi-profile support -- create, switch, delete profiles from web UI (Issue #28)
Add full profile management to the web UI, matching the hermes-agent CLI profile system. Profiles are isolated HERMES_HOME instances with their own config, skills, memory, cron, and API keys. Backend: new api/profiles.py wrapping hermes_cli.profiles, dynamic config reloading, 5 new API endpoints, profile-aware path resolution, HERMES_HOME env save/restore in streaming, module-level cache patching for skills_tool and cron/jobs. Frontend: profile chip in topbar with dropdown, Profiles sidebar panel with CRUD UI, boot-time profile fetch, cascade refresh on switch. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -134,17 +134,44 @@ if _AGENT_DIR is not None:
|
||||
else:
|
||||
_HERMES_FOUND = False
|
||||
|
||||
# ── Config file (optional YAML) ──────────────────────────────────────────────
|
||||
CONFIG_PATH = Path(os.getenv(
|
||||
'HERMES_CONFIG_PATH',
|
||||
str(HOME / '.hermes' / 'config.yaml')
|
||||
)).expanduser()
|
||||
# ── Config file (reloadable -- supports profile switching) ──────────────────
|
||||
_cfg_cache = {}
|
||||
_cfg_lock = threading.Lock()
|
||||
|
||||
try:
|
||||
import yaml as _yaml
|
||||
cfg = _yaml.safe_load(CONFIG_PATH.read_text()) if CONFIG_PATH.exists() else {}
|
||||
except Exception:
|
||||
cfg = {}
|
||||
def _get_config_path() -> Path:
|
||||
"""Return config.yaml path for the active profile."""
|
||||
env_override = os.getenv('HERMES_CONFIG_PATH')
|
||||
if env_override:
|
||||
return Path(env_override).expanduser()
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home
|
||||
return get_active_hermes_home() / 'config.yaml'
|
||||
except ImportError:
|
||||
return HOME / '.hermes' / 'config.yaml'
|
||||
|
||||
def get_config() -> dict:
|
||||
"""Return the cached config dict, loading from disk if needed."""
|
||||
if not _cfg_cache:
|
||||
reload_config()
|
||||
return _cfg_cache
|
||||
|
||||
def reload_config():
|
||||
"""Reload config.yaml from the active profile's directory."""
|
||||
with _cfg_lock:
|
||||
_cfg_cache.clear()
|
||||
config_path = _get_config_path()
|
||||
try:
|
||||
import yaml as _yaml
|
||||
if config_path.exists():
|
||||
loaded = _yaml.safe_load(config_path.read_text())
|
||||
if isinstance(loaded, dict):
|
||||
_cfg_cache.update(loaded)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Initial load
|
||||
reload_config()
|
||||
cfg = _cfg_cache # alias for backward compat with existing references
|
||||
|
||||
# ── Default workspace discovery ───────────────────────────────────────────────
|
||||
def _discover_default_workspace() -> Path:
|
||||
@@ -183,7 +210,7 @@ def print_startup_config():
|
||||
f' state dir : {STATE_DIR}',
|
||||
f' workspace : {DEFAULT_WORKSPACE}',
|
||||
f' host:port : {HOST}:{PORT}',
|
||||
f' config file : {CONFIG_PATH} {"(found)" if CONFIG_PATH.exists() else "(not found, using defaults)"}',
|
||||
f' config file : {_get_config_path()} {"(found)" if _get_config_path().exists() else "(not found, using defaults)"}',
|
||||
'',
|
||||
]
|
||||
print('\n'.join(lines), flush=True)
|
||||
@@ -234,11 +261,12 @@ MIME_MAP = {
|
||||
}
|
||||
|
||||
# ── Toolsets (from config.yaml or hardcoded default) ─────────────────────────
|
||||
CLI_TOOLSETS = cfg.get('platform_toolsets', {}).get('cli', [
|
||||
_DEFAULT_TOOLSETS = [
|
||||
'browser', 'clarify', 'code_execution', 'cronjob', 'delegation', 'file',
|
||||
'image_gen', 'memory', 'session_search', 'skills', 'terminal', 'todo',
|
||||
'web', 'webhook',
|
||||
])
|
||||
]
|
||||
CLI_TOOLSETS = get_config().get('platform_toolsets', {}).get('cli', _DEFAULT_TOOLSETS)
|
||||
|
||||
# ── Model / provider discovery ───────────────────────────────────────────────
|
||||
|
||||
@@ -396,7 +424,11 @@ def get_available_models() -> dict:
|
||||
|
||||
# 3. Try to read auth store for active provider (if hermes is installed)
|
||||
if not active_provider:
|
||||
auth_store_path = HOME / '.hermes' / 'auth.json'
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home as _gah
|
||||
auth_store_path = _gah() / 'auth.json'
|
||||
except ImportError:
|
||||
auth_store_path = HOME / '.hermes' / 'auth.json'
|
||||
if auth_store_path.exists():
|
||||
try:
|
||||
import json as _j
|
||||
@@ -406,7 +438,11 @@ def get_available_models() -> dict:
|
||||
pass
|
||||
|
||||
# 4. Check for API keys that imply available providers
|
||||
hermes_env_path = HOME / '.hermes' / '.env'
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home as _gah2
|
||||
hermes_env_path = _gah2() / '.env'
|
||||
except ImportError:
|
||||
hermes_env_path = HOME / '.hermes' / '.env'
|
||||
env_keys = {}
|
||||
if hermes_env_path.exists():
|
||||
try:
|
||||
@@ -655,3 +691,11 @@ if SETTINGS_FILE.exists():
|
||||
|
||||
# ── SESSIONS in-memory cache (LRU OrderedDict) ───────────────────────────────
|
||||
SESSIONS: collections.OrderedDict = collections.OrderedDict()
|
||||
|
||||
# ── Profile state initialisation ────────────────────────────────────────────
|
||||
# Must run after all imports are resolved to correctly patch module-level caches
|
||||
try:
|
||||
from api.profiles import init_profile_state
|
||||
init_profile_state()
|
||||
except ImportError:
|
||||
pass # hermes_cli not available -- default profile only
|
||||
|
||||
@@ -34,8 +34,8 @@ def _write_session_index():
|
||||
|
||||
|
||||
class Session:
|
||||
def __init__(self, session_id=None, title='Untitled', workspace=str(DEFAULT_WORKSPACE), model=DEFAULT_MODEL, messages=None, created_at=None, updated_at=None, tool_calls=None, pinned=False, archived=False, project_id=None, **kwargs):
|
||||
self.session_id = session_id or uuid.uuid4().hex[:12]; self.title = title; self.workspace = str(Path(workspace).expanduser().resolve()); self.model = model; self.messages = messages or []; self.tool_calls = tool_calls or []; self.created_at = created_at or time.time(); self.updated_at = updated_at or time.time(); self.pinned = bool(pinned); self.archived = bool(archived); self.project_id = project_id or None
|
||||
def __init__(self, session_id=None, title='Untitled', workspace=str(DEFAULT_WORKSPACE), model=DEFAULT_MODEL, messages=None, created_at=None, updated_at=None, tool_calls=None, pinned=False, archived=False, project_id=None, profile=None, **kwargs):
|
||||
self.session_id = session_id or uuid.uuid4().hex[:12]; self.title = title; self.workspace = str(Path(workspace).expanduser().resolve()); self.model = model; self.messages = messages or []; self.tool_calls = tool_calls or []; self.created_at = created_at or time.time(); self.updated_at = updated_at or time.time(); self.pinned = bool(pinned); self.archived = bool(archived); self.project_id = project_id or None; self.profile = profile
|
||||
@property
|
||||
def path(self): return SESSION_DIR / f'{self.session_id}.json'
|
||||
def save(self): self.updated_at = time.time(); self.path.write_text(json.dumps(self.__dict__, ensure_ascii=False, indent=2), encoding='utf-8'); _write_session_index()
|
||||
@@ -44,7 +44,7 @@ class Session:
|
||||
p = SESSION_DIR / f'{sid}.json'
|
||||
if not p.exists(): return None
|
||||
return cls(**json.loads(p.read_text(encoding='utf-8')))
|
||||
def compact(self): return {'session_id': self.session_id, 'title': self.title, 'workspace': self.workspace, 'model': self.model, 'message_count': len(self.messages), 'created_at': self.created_at, 'updated_at': self.updated_at, 'pinned': self.pinned, 'archived': self.archived, 'project_id': self.project_id}
|
||||
def compact(self): return {'session_id': self.session_id, 'title': self.title, 'workspace': self.workspace, 'model': self.model, 'message_count': len(self.messages), 'created_at': self.created_at, 'updated_at': self.updated_at, 'pinned': self.pinned, 'archived': self.archived, 'project_id': self.project_id, 'profile': self.profile}
|
||||
|
||||
def get_session(sid):
|
||||
with LOCK:
|
||||
@@ -63,7 +63,12 @@ def get_session(sid):
|
||||
|
||||
def new_session(workspace=None, model=None):
|
||||
# Use _cfg.DEFAULT_MODEL (not the import-time snapshot) so save_settings() changes take effect
|
||||
s = Session(workspace=workspace or get_last_workspace(), model=model or _cfg.DEFAULT_MODEL)
|
||||
try:
|
||||
from api.profiles import get_active_profile_name
|
||||
_profile = get_active_profile_name()
|
||||
except ImportError:
|
||||
_profile = None
|
||||
s = Session(workspace=workspace or get_last_workspace(), model=model or _cfg.DEFAULT_MODEL, profile=_profile)
|
||||
with LOCK:
|
||||
SESSIONS[s.session_id] = s
|
||||
SESSIONS.move_to_end(s.session_id)
|
||||
|
||||
246
api/profiles.py
Normal file
246
api/profiles.py
Normal file
@@ -0,0 +1,246 @@
|
||||
"""
|
||||
Hermes Web UI -- Profile state management.
|
||||
Wraps hermes_cli.profiles to provide profile switching for the web UI.
|
||||
|
||||
The web UI maintains a process-level "active profile" that determines which
|
||||
HERMES_HOME directory is used for config, skills, memory, cron, and API keys.
|
||||
Profile switches update os.environ['HERMES_HOME'] and monkey-patch module-level
|
||||
cached paths in hermes-agent modules (skills_tool, cron/jobs) that snapshot
|
||||
HERMES_HOME at import time.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
from pathlib import Path
|
||||
|
||||
# ── Module state ────────────────────────────────────────────────────────────
|
||||
_active_profile = 'default'
|
||||
_profile_lock = threading.Lock()
|
||||
_DEFAULT_HERMES_HOME = Path.home() / '.hermes'
|
||||
|
||||
|
||||
def _read_active_profile_file() -> str:
|
||||
"""Read the sticky active profile from ~/.hermes/active_profile."""
|
||||
ap_file = _DEFAULT_HERMES_HOME / 'active_profile'
|
||||
if ap_file.exists():
|
||||
try:
|
||||
name = ap_file.read_text().strip()
|
||||
if name:
|
||||
return name
|
||||
except Exception:
|
||||
pass
|
||||
return 'default'
|
||||
|
||||
|
||||
# ── Public API ──────────────────────────────────────────────────────────────
|
||||
|
||||
def get_active_profile_name() -> str:
|
||||
"""Return the currently active profile name."""
|
||||
return _active_profile
|
||||
|
||||
|
||||
def get_active_hermes_home() -> Path:
|
||||
"""Return the HERMES_HOME path for the currently active profile."""
|
||||
if _active_profile == 'default':
|
||||
return _DEFAULT_HERMES_HOME
|
||||
profile_dir = _DEFAULT_HERMES_HOME / 'profiles' / _active_profile
|
||||
if profile_dir.is_dir():
|
||||
return profile_dir
|
||||
return _DEFAULT_HERMES_HOME
|
||||
|
||||
|
||||
def _set_hermes_home(home: Path):
|
||||
"""Set HERMES_HOME env var and monkey-patch cached module-level paths."""
|
||||
os.environ['HERMES_HOME'] = str(home)
|
||||
|
||||
# Patch skills_tool module-level cache (snapshots HERMES_HOME at import)
|
||||
try:
|
||||
import tools.skills_tool as _sk
|
||||
_sk.HERMES_HOME = home
|
||||
_sk.SKILLS_DIR = home / 'skills'
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
# Patch cron/jobs module-level cache
|
||||
try:
|
||||
import cron.jobs as _cj
|
||||
_cj.HERMES_DIR = home
|
||||
_cj.CRON_DIR = home / 'cron'
|
||||
_cj.JOBS_FILE = _cj.CRON_DIR / 'jobs.json'
|
||||
_cj.OUTPUT_DIR = _cj.CRON_DIR / 'output'
|
||||
except (ImportError, AttributeError):
|
||||
pass
|
||||
|
||||
|
||||
def _reload_dotenv(home: Path):
|
||||
"""Load .env from the profile dir into os.environ (additive)."""
|
||||
env_path = home / '.env'
|
||||
if not env_path.exists():
|
||||
return
|
||||
try:
|
||||
for line in env_path.read_text().splitlines():
|
||||
line = line.strip()
|
||||
if line and not line.startswith('#') and '=' in line:
|
||||
k, v = line.split('=', 1)
|
||||
k = k.strip()
|
||||
v = v.strip().strip('"').strip("'")
|
||||
if k and v:
|
||||
os.environ[k] = v
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def init_profile_state():
|
||||
"""Initialize profile state at server startup.
|
||||
|
||||
Reads ~/.hermes/active_profile, sets HERMES_HOME env var, patches
|
||||
module-level cached paths. Called once from config.py after imports.
|
||||
"""
|
||||
global _active_profile
|
||||
_active_profile = _read_active_profile_file()
|
||||
home = get_active_hermes_home()
|
||||
_set_hermes_home(home)
|
||||
_reload_dotenv(home)
|
||||
|
||||
|
||||
def switch_profile(name: str) -> dict:
|
||||
"""Switch the active profile.
|
||||
|
||||
Validates the profile exists, updates process state, patches module caches,
|
||||
reloads .env, and reloads config.yaml.
|
||||
|
||||
Returns: {'profiles': [...], 'active': name}
|
||||
Raises ValueError if profile doesn't exist or agent is busy.
|
||||
"""
|
||||
global _active_profile
|
||||
|
||||
# Import here to avoid circular import at module load
|
||||
from api.config import STREAMS, STREAMS_LOCK, reload_config
|
||||
|
||||
# Block if agent is running
|
||||
with STREAMS_LOCK:
|
||||
if len(STREAMS) > 0:
|
||||
raise RuntimeError(
|
||||
'Cannot switch profiles while an agent is running. '
|
||||
'Cancel or wait for it to finish.'
|
||||
)
|
||||
|
||||
# Resolve profile directory
|
||||
if name == 'default':
|
||||
home = _DEFAULT_HERMES_HOME
|
||||
else:
|
||||
home = _DEFAULT_HERMES_HOME / 'profiles' / name
|
||||
if not home.is_dir():
|
||||
raise ValueError(f"Profile '{name}' does not exist.")
|
||||
|
||||
with _profile_lock:
|
||||
_active_profile = name
|
||||
_set_hermes_home(home)
|
||||
_reload_dotenv(home)
|
||||
|
||||
# Write sticky default for CLI consistency
|
||||
try:
|
||||
ap_file = _DEFAULT_HERMES_HOME / 'active_profile'
|
||||
ap_file.write_text(name if name != 'default' else '')
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Reload config.yaml from the new profile
|
||||
reload_config()
|
||||
|
||||
return {'profiles': list_profiles_api(), 'active': name}
|
||||
|
||||
|
||||
def list_profiles_api() -> list:
|
||||
"""List all profiles with metadata, serialized for JSON response."""
|
||||
try:
|
||||
from hermes_cli.profiles import list_profiles
|
||||
infos = list_profiles()
|
||||
except ImportError:
|
||||
# hermes_cli not available -- return just the default
|
||||
return [_default_profile_dict()]
|
||||
|
||||
active = _active_profile
|
||||
result = []
|
||||
for p in infos:
|
||||
result.append({
|
||||
'name': p.name,
|
||||
'path': str(p.path),
|
||||
'is_default': p.is_default,
|
||||
'is_active': p.name == active,
|
||||
'gateway_running': p.gateway_running,
|
||||
'model': p.model,
|
||||
'provider': p.provider,
|
||||
'has_env': p.has_env,
|
||||
'skill_count': p.skill_count,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def _default_profile_dict() -> dict:
|
||||
"""Fallback profile dict when hermes_cli is not importable."""
|
||||
return {
|
||||
'name': 'default',
|
||||
'path': str(_DEFAULT_HERMES_HOME),
|
||||
'is_default': True,
|
||||
'is_active': True,
|
||||
'gateway_running': False,
|
||||
'model': None,
|
||||
'provider': None,
|
||||
'has_env': (_DEFAULT_HERMES_HOME / '.env').exists(),
|
||||
'skill_count': 0,
|
||||
}
|
||||
|
||||
|
||||
def create_profile_api(name: str, clone_from: str = None,
|
||||
clone_config: bool = False) -> dict:
|
||||
"""Create a new profile. Returns the new profile info dict."""
|
||||
try:
|
||||
from hermes_cli.profiles import create_profile, validate_profile_name
|
||||
except ImportError:
|
||||
raise RuntimeError('Profile management requires hermes-agent to be installed.')
|
||||
|
||||
validate_profile_name(name)
|
||||
create_profile(
|
||||
name,
|
||||
clone_from=clone_from,
|
||||
clone_config=clone_config,
|
||||
clone_all=False,
|
||||
no_alias=True,
|
||||
)
|
||||
|
||||
# Find and return the newly created profile info
|
||||
for p in list_profiles_api():
|
||||
if p['name'] == name:
|
||||
return p
|
||||
return {'name': name, 'path': str(_DEFAULT_HERMES_HOME / 'profiles' / name)}
|
||||
|
||||
|
||||
def delete_profile_api(name: str) -> dict:
|
||||
"""Delete a profile. Switches to default first if it's the active one."""
|
||||
if name == 'default':
|
||||
raise ValueError("Cannot delete the default profile.")
|
||||
|
||||
# If deleting the active profile, switch to default first
|
||||
if _active_profile == name:
|
||||
try:
|
||||
switch_profile('default')
|
||||
except RuntimeError:
|
||||
raise RuntimeError(
|
||||
f"Cannot delete active profile '{name}' while an agent is running. "
|
||||
"Cancel or wait for it to finish."
|
||||
)
|
||||
|
||||
try:
|
||||
from hermes_cli.profiles import delete_profile
|
||||
delete_profile(name, yes=True)
|
||||
except ImportError:
|
||||
# Manual fallback: just remove the directory
|
||||
import shutil
|
||||
profile_dir = _DEFAULT_HERMES_HOME / 'profiles' / name
|
||||
if profile_dir.is_dir():
|
||||
shutil.rmtree(str(profile_dir))
|
||||
else:
|
||||
raise ValueError(f"Profile '{name}' does not exist.")
|
||||
|
||||
return {'ok': True, 'name': name}
|
||||
@@ -234,6 +234,15 @@ def handle_get(handler, parsed):
|
||||
if parsed.path == '/api/memory':
|
||||
return _handle_memory_read(handler)
|
||||
|
||||
# ── Profile API (GET) ──
|
||||
if parsed.path == '/api/profiles':
|
||||
from api.profiles import list_profiles_api, get_active_profile_name
|
||||
return j(handler, {'profiles': list_profiles_api(), 'active': get_active_profile_name()})
|
||||
|
||||
if parsed.path == '/api/profile/active':
|
||||
from api.profiles import get_active_profile_name, get_active_hermes_home
|
||||
return j(handler, {'name': get_active_profile_name(), 'path': str(get_active_hermes_home())})
|
||||
|
||||
return False # 404
|
||||
|
||||
|
||||
@@ -372,6 +381,43 @@ def handle_post(handler, parsed):
|
||||
if parsed.path == '/api/memory/write':
|
||||
return _handle_memory_write(handler, body)
|
||||
|
||||
# ── Profile API (POST) ──
|
||||
if parsed.path == '/api/profile/switch':
|
||||
name = body.get('name', '').strip()
|
||||
if not name: return bad(handler, 'name is required')
|
||||
try:
|
||||
from api.profiles import switch_profile
|
||||
result = switch_profile(name)
|
||||
return j(handler, result)
|
||||
except (ValueError, FileNotFoundError) as e:
|
||||
return bad(handler, str(e), 404)
|
||||
except RuntimeError as e:
|
||||
return bad(handler, str(e), 409)
|
||||
|
||||
if parsed.path == '/api/profile/create':
|
||||
name = body.get('name', '').strip()
|
||||
if not name: return bad(handler, 'name is required')
|
||||
try:
|
||||
from api.profiles import create_profile_api
|
||||
result = create_profile_api(
|
||||
name,
|
||||
clone_from=body.get('clone_from'),
|
||||
clone_config=bool(body.get('clone_config', False)),
|
||||
)
|
||||
return j(handler, {'ok': True, 'profile': result})
|
||||
except (ValueError, FileExistsError, RuntimeError) as e:
|
||||
return bad(handler, str(e))
|
||||
|
||||
if parsed.path == '/api/profile/delete':
|
||||
name = body.get('name', '').strip()
|
||||
if not name: return bad(handler, 'name is required')
|
||||
try:
|
||||
from api.profiles import delete_profile_api
|
||||
result = delete_profile_api(name)
|
||||
return j(handler, result)
|
||||
except (ValueError, FileNotFoundError) as e:
|
||||
return bad(handler, str(e))
|
||||
|
||||
# ── Settings (POST) ──
|
||||
if parsed.path == '/api/settings':
|
||||
saved = save_settings(body)
|
||||
@@ -731,7 +777,11 @@ def _handle_cron_recent(handler, parsed):
|
||||
|
||||
|
||||
def _handle_memory_read(handler):
|
||||
mem_dir = Path.home() / '.hermes' / 'memories'
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home
|
||||
mem_dir = get_active_hermes_home() / 'memories'
|
||||
except ImportError:
|
||||
mem_dir = Path.home() / '.hermes' / 'memories'
|
||||
mem_file = mem_dir / 'MEMORY.md'
|
||||
user_file = mem_dir / 'USER.md'
|
||||
memory = mem_file.read_text(encoding='utf-8', errors='replace') if mem_file.exists() else ''
|
||||
@@ -1078,7 +1128,11 @@ def _handle_skill_delete(handler, body):
|
||||
def _handle_memory_write(handler, body):
|
||||
try: require(body, 'section', 'content')
|
||||
except ValueError as e: return bad(handler, str(e))
|
||||
mem_dir = Path.home() / '.hermes' / 'memories'
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home
|
||||
mem_dir = get_active_hermes_home() / 'memories'
|
||||
except ImportError:
|
||||
mem_dir = Path.home() / '.hermes' / 'memories'
|
||||
mem_dir.mkdir(parents=True, exist_ok=True)
|
||||
section = body['section']
|
||||
if section == 'memory':
|
||||
|
||||
@@ -64,19 +64,30 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
||||
put('cancel', {'message': 'Cancelled before start'})
|
||||
return
|
||||
|
||||
# Resolve profile home for this agent run (snapshot at start)
|
||||
try:
|
||||
from api.profiles import get_active_hermes_home
|
||||
_profile_home = str(get_active_hermes_home())
|
||||
except ImportError:
|
||||
_profile_home = os.environ.get('HERMES_HOME', '')
|
||||
|
||||
_set_thread_env(
|
||||
TERMINAL_CWD=str(s.workspace),
|
||||
HERMES_EXEC_ASK='1',
|
||||
HERMES_SESSION_KEY=session_id,
|
||||
HERMES_HOME=_profile_home,
|
||||
)
|
||||
# Still set process-level env as fallback for tools that bypass thread-local
|
||||
with _agent_lock:
|
||||
old_cwd = os.environ.get('TERMINAL_CWD')
|
||||
old_exec_ask = os.environ.get('HERMES_EXEC_ASK')
|
||||
old_session_key = os.environ.get('HERMES_SESSION_KEY')
|
||||
old_hermes_home = os.environ.get('HERMES_HOME')
|
||||
os.environ['TERMINAL_CWD'] = str(s.workspace)
|
||||
os.environ['HERMES_EXEC_ASK'] = '1'
|
||||
os.environ['HERMES_SESSION_KEY'] = session_id
|
||||
if _profile_home:
|
||||
os.environ['HERMES_HOME'] = _profile_home
|
||||
|
||||
try:
|
||||
def on_token(text):
|
||||
@@ -187,6 +198,8 @@ def _run_agent_streaming(session_id, msg_text, model, workspace, stream_id, atta
|
||||
else: os.environ['HERMES_EXEC_ASK'] = old_exec_ask
|
||||
if old_session_key is None: os.environ.pop('HERMES_SESSION_KEY', None)
|
||||
else: os.environ['HERMES_SESSION_KEY'] = old_session_key
|
||||
if old_hermes_home is None: os.environ.pop('HERMES_HOME', None)
|
||||
else: os.environ['HERMES_HOME'] = old_hermes_home
|
||||
|
||||
except Exception as e:
|
||||
print('[webui] stream error:\n' + traceback.format_exc(), flush=True)
|
||||
|
||||
Reference in New Issue
Block a user