Merge branch 'master' into product/public

This commit is contained in:
Dita Aji Pratama 2026-10-05 11:53:43 +07:00
commit ba84ca1a3a
21 changed files with 364 additions and 137 deletions

21
LICENSE Normal file
View File

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Dita Aji Pratama
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1
agent/characters/musashi/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
service.yaml

View File

@ -0,0 +1,23 @@
# This file serves as a template for character-specific service overrides.
# Copy this to agent/characters/<character>/service.yaml and fill in the credentials.
xmpp:
enabled: false
username: ""
password: ""
muc_rooms: # YAML list, e.g.:
# - "room1@conference.server"
# - "room2@conference.server"
nickname: "" # custom MUC nickname (empty = use username)
selective_response: true # true = only response if mentioned/relevant
debug: false # true = tampilkan log debug menyeluruh (semua stanza XML, lifecycle, dll)
mute_session_end: false # true = tidak kirim pesan saat sesi berakhir (timeout)
telegram:
enabled: false
token: ""
allowed_group_ids: # YAML list, empty = all group. e.g.:
# - "123456789"
# - "987654321"
selective_response: true # true = only response if mentioned/relevant
mute_session_end: false # true = tidak kirim pesan saat sesi berakhir (timeout)

View File

@ -2,9 +2,9 @@ import os, sys, threading, time, signal
import config import config
from lib import gadget, personality, moop, ragroleplay as ragroleplay_lib from lib import gadget, personality, moop, workspace, ragroleplay as ragroleplay_lib
from modules import argsparser from modules import argsparser
from tools import coder, carrack, ragroleplay as ragroleplay_tools, vision, imagegen, smart, smart_rp from tools import coder, carrack, ragroleplay as ragroleplay_tools, vision, imagegen, smart, smart_rp, syshendrik
from services.llm_client import LLMClient from services.llm_client import LLMClient
from services.xmpp_client import XMPPClient from services.xmpp_client import XMPPClient
@ -20,7 +20,6 @@ tools_definition = [
gadget.tools_mapping( schema = coder.schema_run_bash, handler = coder.run_bash ), gadget.tools_mapping( schema = coder.schema_run_bash, handler = coder.run_bash ),
gadget.tools_mapping( schema = coder.schema_search_code, handler = coder.search_code ), gadget.tools_mapping( schema = coder.schema_search_code, handler = coder.search_code ),
gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ), gadget.tools_mapping( schema = coder.schema_git_operation, handler = coder.git_operation ),
gadget.tools_mapping( schema = coder.schema_workspace_change, handler = coder.workspace_change ),
gadget.tools_mapping( schema = coder.schema_search_grep, handler = coder.search_grep ), gadget.tools_mapping( schema = coder.schema_search_grep, handler = coder.search_grep ),
gadget.tools_mapping( schema = coder.schema_search_glob, handler = coder.search_glob ), gadget.tools_mapping( schema = coder.schema_search_glob, handler = coder.search_glob ),
# Carrack Tools # Carrack Tools
@ -47,6 +46,8 @@ tools_definition = [
gadget.tools_mapping( schema = ragroleplay_tools.schema_memories_delete, handler = ragroleplay_tools.memories_delete ), gadget.tools_mapping( schema = ragroleplay_tools.schema_memories_delete, handler = ragroleplay_tools.memories_delete ),
gadget.tools_mapping( schema = ragroleplay_tools.schema_end_session, handler = ragroleplay_tools.end_session ), gadget.tools_mapping( schema = ragroleplay_tools.schema_end_session, handler = ragroleplay_tools.end_session ),
# SysHendrik Tools
gadget.tools_mapping( schema = syshendrik.schema_workspace_change, handler = syshendrik.workspace_change ),
] ]
@ -67,7 +68,7 @@ def main():
if not os.path.isdir(resolved): if not os.path.isdir(resolved):
print(f"Error: '{resolved}' is not a valid directory", flush=True) print(f"Error: '{resolved}' is not a valid directory", flush=True)
sys.exit(1) sys.exit(1)
coder.set_current_workspace(resolved) workspace.set_current_workspace(resolved)
# Inference # Inference
llm_client = LLMClient("", "", "", config.llm_timeout) llm_client = LLMClient("", "", "", config.llm_timeout)

View File

@ -9,7 +9,7 @@ import curses
import os import os
import config import config
from .agent import log from .agent import log
from tools.coder import set_current_workspace from lib import workspace
from . import keycodes from . import keycodes
from .moopui import ( from .moopui import (
model_selector_popup, model_selector_popup,
@ -157,7 +157,7 @@ def workspace_popup(app, stdscr):
if ws: if ws:
resolved = os.path.abspath(ws) resolved = os.path.abspath(ws)
if os.path.isdir(resolved): if os.path.isdir(resolved):
set_current_workspace(resolved) workspace.set_current_workspace(resolved)
log(app, "system", f"Workspace \u2192 {resolved}") log(app, "system", f"Workspace \u2192 {resolved}")
else: else:
log(app, "error", f"Invalid directory: {resolved}") log(app, "error", f"Invalid directory: {resolved}")

View File

@ -6,7 +6,7 @@
import curses import curses
import json import json
from tools.coder import get_current_workspace from lib import workspace
from . import theme from . import theme
from .actions import ACTIONS from .actions import ACTIONS
@ -361,7 +361,7 @@ def draw_status(app, stdscr):
mode = " PROCESSING " if app.processing else " READY " mode = " PROCESSING " if app.processing else " READY "
set_name = (getattr(app, "model_set_name", "") or "-").strip() or "-" set_name = (getattr(app, "model_set_name", "") or "-").strip() or "-"
model = (app.llm.model or "-").strip() or "-" model = (app.llm.model or "-").strip() or "-"
ws = get_current_workspace() or "-" ws = workspace.get_current_workspace() or "-"
send_key = ACTIONS["send_prompt"].get("shortcut") or "Ctrl+Return" send_key = ACTIONS["send_prompt"].get("shortcut") or "Ctrl+Return"
right = f" {app.character_name} {send_key}:Send " right = f" {app.character_name} {send_key}:Send "

18
lib/workspace.py Normal file
View File

@ -0,0 +1,18 @@
import os
# Pemilik tunggal state workspace sesi agent.
# Modul lain (tools.coder, tools.syshendrik, tools.vision, interfaces.tui)
# membaca/mengganti workspace lewat getter/setter di sini, bukan menyimpan
# salinan state sendiri, agar tidak ada dua sumber kebenaran.
CURRENT_WORKSPACE = os.getcwd()
def get_current_workspace():
global CURRENT_WORKSPACE
return CURRENT_WORKSPACE
def set_current_workspace(path):
global CURRENT_WORKSPACE
# chdir dulu agar state hanya ter-update ketika perpindahan benar-benar sukses
os.chdir(path)
CURRENT_WORKSPACE = path

View File

@ -0,0 +1,78 @@
# Feature: Mute Session End Notification
## Goal
Tambah field konfigurasi baru per-service (xmpp & telegram) untuk menonaktifkan
pesan pemberitahuan saat sesi berakhir. Jika `true`, agent tidak mengirim pesan
"pemberitahuan sesi berakhir" (`*session end*` / `Sesi ditutup. Sampai jumpa`)
ketika sesi ditutup karena timeout/kegiatan berhenti.
Field mengikuti pola yang sudah ada di `service.yaml` (sudah mendukung override
per karakter untuk xmpp & telegram).
## Field
- Nama: `mute_session_end`
- Tipe: bool
- Default: `false` (perilaku lama: tetap kirim pesan penutup)
- Letak: di bawah section `xmpp:` dan `telegram:` (service.yaml & config.yaml)
- Konstanta global baru: `XMPP_MUTE_SESSION_END`, `TELEGRAM_MUTE_SESSION_END`
## Scope pesan yang di-mute (jalur target)
Hanya pesan penutup yang dihasilkan oleh sistem saat sesi berakhir otomatis:
- `xmpp_client._timeout_session`
- `telegram_client._timeout_session`
Sesi tetap di-reset seperti semula; hanya pengiriman teks penutupnya yang dilewati.
**Dikonfirmasi:** natural-close via tool `end_session` TIDAK ikut di-mute (farewell
adalah bagian percakapan model, bukan pesan sistem).
## Perubahan tambahan: timeout group/MUC
Diperpanjang dari 5 menit (300s) menjadi 30 menit (1800s) untuk group/MUC.
Titik yang diubah:
- `telegram_client.py` (`_process_message`): `... else 300` → `1800` (chat_type group).
- `xmpp_client.py`: dua `session.start_timer(300, ..., 'groupchat')` → `1800`.
Private/DM tetap 86400 (di luar scope).
## Files to change
### 1. config.py
- Tambah pembacaan default dari `config.yaml` pada blok modul-level:
- `XMPP_MUTE_SESSION_END` (xmpp section, default "false")
- `TELEGRAM_MUTE_SESSION_END` (telegram section, default "false")
- Tambah keduanya ke `global` + reset di `_reset_service_from_config()`.
- Tambah keduanya ke `global` + aplikasi override di `load_service_config()`
memakai pola yang sama seperti field lain:
- `if "mute_session_end" in xmpp_data: XMPP_MUTE_SESSION_END = _to_bool(...)`
- `if "mute_session_end" in telegram_data: TELEGRAM_MUTE_SESSION_END = _to_bool(...)`
- Parsing bool pakai pola string existing: `str(v).strip().lower() in ("true","1","yes")`.
### 2. services/xmpp_client.py (`_timeout_session`)
- Bungkus `self._schedule_send(session_id, body, mtype)` dengan guard:
`if not config.XMPP_MUTE_SESSION_END:` — tetap panggil `self._session_mgr.reset(session_id)`.
- Opsional: log bahwa notifikasi di-mute.
### 3. services/telegram_client.py (`_timeout_session`)
- Bungkus `self._schedule_send(chat_id, body)` dengan guard:
`if not config.TELEGRAM_MUTE_SESSION_END:` — tetap `self._session_mgr.reset(...)`.
### 4. Templates (dokumentasi, agar field mudah ditemukan)
- `config.default.yaml`: tambah `mute_session_end: false` di section xmpp & telegram.
- `agent/characters/*/service.default.yaml` (semua karakter): tambah field yang sama.
- Catatan: `service.yaml` runtime bersifat gitignored; karena loader cek keberadaan
key, karakter tanpa key baru akan fallback ke default `false` (backward compatible).
## Backward compatibility
- Tidak ada service.yaml existing yang wajib diubah.
- Key hilang => default `false` => behavior identik dengan sekarang.
## Validation plan
- `python -c "import config; print(config.XMPP_MUTE_SESSION_END, config.TELEGRAM_MUTE_SESSION_END)"`
untuk default.
- Load karakter dengan `mute_session_end: true` (via service.yaml test / char override)
dan verifikasi konstanta menjadi True.
- Kode review manual kedua `_timeout_session`: saat True, tidak ada `send`/`schedule_send`,
sesi tetap reset.
## Resolved
Scope final: hanya `_timeout_session`. Natural-close `end_session` tidak disentuh
(sudah dikonfirmasi user). Timeout group/MUC dinaikkan ke 30 menit.

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

@ -0,0 +1,38 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* TUI/GUI/API Settings
** TUI
*** Parameters for tokenizer
*** Service list
****_ Kontak dengan daemon
*** Model set & Provider list <<done>>
*** Roleplayer user ID
@endmindmap

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -0,0 +1,36 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* Configuration per character <<done>>
** service.yaml <<done>>
***_ Untuk kredensial service yang di gunakan
** model.yaml <<done>>
***_ Untuk menetapkan model set default yang digunakan
@endmindmap

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,42 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* Tools for configuration
** Workspace Switch <<done>>
** Service toggler
** Listing model set and provider
** Model set switch <<skip>>
***_ Sepertinya mustahil dan potensi ngerusak sistem
* Tools for generation
** plantuml generator
** Slide deck
** Motion graphic
@endmindmap

Binary file not shown.

Before

Width:  |  Height:  |  Size: 50 KiB

View File

@ -1,76 +0,0 @@
@startmindmap
<style>
mindmapDiagram {
//FontColor white
.done {
BackgroundColor green
BackgroundColor lightgreen
//FontColor black
}
.unknown {
BackgroundColor #FFBBCC
BackgroundColor purple
FontColor white
}
.skip {
BackgroundColor #FFBBCC
BackgroundColor red
FontColor white
}
.info {
BackgroundColor blue
BackgroundColor lightblue
}
}
</style>
;top to bottom direction
* Tools for configuration
** Workspace Switch <<done>>
** Service toggler
** Listing model set and provider
** Model set switch <<skip>>
***_ Sepertinya mustahil dan potensi ngerusak sistem
* Tools for generation
** plantuml generator
** Slide deck
** Motion graphic
* TUI/GUI/API Settings
** TUI
*** Parameters for tokenizer
*** Service list
****_ Kontak dengan daemon
*** Model set & Provider list <<done>>
*** Roleplayer user ID
* Configuration per character <<done>>
** service.yaml <<done>>
***_ Untuk kredensial service yang di gunakan
** model.yaml <<done>>
***_ Untuk menetapkan model set default yang digunakan
* opencode main skill
** Explore (Passive)
** Plan
** Build
* Verbosity
** Concise
***_ Keep your answers short and to the point.
** Balanced
***_ Provide balanced answers. Not too brief, not too long.
** Detailed
***_ Give thorough, detailed answers with explanations.
* Full-Duplex Model
**_ Voice Activity Detection
**_ Noise interruption detector included
** Mic & Voice Frontend
** Models
*** GPT-Live-1
@endmindmap

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -27,6 +27,43 @@ mindmapDiagram {
;top to bottom direction ;top to bottom direction
* Decision/Classification Features
** API Inference
*** Library
****_ Dependency
*****_ .venv/bin/pip install huggingface_hub transformers
****_ Download
*****_ .venv/bin/hf download convaiinnovations/laya --local-dir models/convaiinnovations/laya
*****_ .venv/bin/hf download convaiinnovations/laya-multilingual --local-dir models/convaiinnovations/laya-multilingual
*** Model
**** Classification
***** convaiinnovations/laya
***** convaiinnovations/laya-multilingual
*** Inference
**** llama.cpp
**** laya.cpp (lkarlslund)
*****_ HTTP Server bawaan
* opencode main skill
** Explore (Passive)
** Plan
** Build
* Verbosity
** Concise
***_ Keep your answers short and to the point.
** Balanced
***_ Provide balanced answers. Not too brief, not too long.
** Detailed
***_ Give thorough, detailed answers with explanations.
* Full-Duplex Model
**_ Voice Activity Detection
**_ Noise interruption detector included
** Mic & Voice Frontend
** Models
*** GPT-Live-1
* Speech * Speech
** Push to talk ** Push to talk
*** STT (ASR) *** STT (ASR)

View File

@ -4,18 +4,7 @@ import re
import fnmatch import fnmatch
import glob as glob_module import glob as glob_module
# Global state to track the agent's current working directory from lib import workspace
# This ensures that subsequent bash commands run in the correct directory
CURRENT_WORKSPACE = os.getcwd()
def get_current_workspace():
global CURRENT_WORKSPACE
return CURRENT_WORKSPACE
def set_current_workspace(path):
global CURRENT_WORKSPACE
CURRENT_WORKSPACE = path
os.chdir(path)
schema_read_file = { schema_read_file = {
"type": "function", "type": "function",
@ -34,8 +23,8 @@ schema_read_file = {
def read_file(path): def read_file(path):
try: try:
# Use absolute path if it's relative to CURRENT_WORKSPACE # Use absolute path if it's relative to current workspace
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
with open(full_path, 'r', encoding='utf-8', errors='replace') as f: with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
lines = f.readlines() lines = f.readlines()
return ''.join(f"{i+1}: {line}" for i, line in enumerate(lines)) return ''.join(f"{i+1}: {line}" for i, line in enumerate(lines))
@ -60,7 +49,7 @@ schema_write_file = {
def write_file(path, content): def write_file(path, content):
try: try:
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
with open(full_path, 'w', encoding='utf-8') as f: with open(full_path, 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
return f"Successfully wrote to {full_path}" return f"Successfully wrote to {full_path}"
@ -86,7 +75,7 @@ schema_edit_file = {
def edit_file(path, old_string, new_string): def edit_file(path, old_string, new_string):
try: try:
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
with open(full_path, 'r', encoding='utf-8', errors='replace') as f: with open(full_path, 'r', encoding='utf-8', errors='replace') as f:
content = f.read() content = f.read()
if old_string not in content: if old_string not in content:
@ -116,7 +105,7 @@ schema_run_bash = {
def run_bash(command): def run_bash(command):
try: try:
# Prepend 'cd' to the command to ensure it runs in the correct workspace # Prepend 'cd' to the command to ensure it runs in the correct workspace
full_command = f"cd '{get_current_workspace()}' && {command}" full_command = f"cd '{workspace.get_current_workspace()}' && {command}"
result = subprocess.run(full_command, shell=True, capture_output=True, text=True, timeout=30) result = subprocess.run(full_command, shell=True, capture_output=True, text=True, timeout=30)
return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}" return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}"
except Exception as e: except Exception as e:
@ -142,7 +131,7 @@ schema_search_code = {
def search_code(pattern, search_type, path="."): def search_code(pattern, search_type, path="."):
try: try:
# Resolve path relative to workspace # Resolve path relative to workspace
search_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path search_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
if search_type == "glob": if search_type == "glob":
files = glob_module.glob(f"{search_path}/**/{pattern}", recursive=True) files = glob_module.glob(f"{search_path}/**/{pattern}", recursive=True)
@ -187,41 +176,11 @@ def git_operation(args):
try: try:
# Execute git command from the current workspace # Execute git command from the current workspace
full_command = ["git", *args] full_command = ["git", *args]
result = subprocess.run(full_command, cwd=get_current_workspace(), capture_output=True, text=True, timeout=10) result = subprocess.run(full_command, cwd=workspace.get_current_workspace(), capture_output=True, text=True, timeout=10)
return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}" return f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}\nreturn code: {result.returncode}"
except Exception as e: except Exception as e:
return f"Error running git command: {str(e)}" return f"Error running git command: {str(e)}"
schema_workspace_change = {
"type": "function",
"function": {
"name": "workspace_change",
"description": "Change the current working directory of the agent session. Supports tilde (~) expansion for the user home directory (/home/aji/).",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "The target directory path to change to."}
},
"required": ["path"]
}
}
}
def workspace_change(path):
try:
if path.startswith("~"):
target_path = os.path.expanduser(path)
else:
target_path = os.path.abspath(path)
if os.path.exists(target_path) and os.path.isdir(target_path):
set_current_workspace(target_path)
return f"Successfully changed workspace to: {target_path}"
else:
return f"Error: The path {target_path} does not exist or is not a directory."
except Exception as e:
return f"Error changing workspace: {str(e)}"
schema_search_grep = { schema_search_grep = {
"type": "function", "type": "function",
"function": { "function": {
@ -241,7 +200,7 @@ schema_search_grep = {
def search_grep(pattern, path=".", include=None): def search_grep(pattern, path=".", include=None):
try: try:
search_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path search_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
results = [] results = []
for root, dirs, files in os.walk(search_path): for root, dirs, files in os.walk(search_path):
for file in files: for file in files:
@ -277,7 +236,7 @@ schema_search_glob = {
def search_glob(pattern, path="."): def search_glob(pattern, path="."):
try: try:
search_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path search_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
files = glob_module.glob(f"{search_path}/{pattern}", recursive=True) files = glob_module.glob(f"{search_path}/{pattern}", recursive=True)
def _isfile(f): def _isfile(f):
try: try:

49
tools/syshendrik.py Normal file
View File

@ -0,0 +1,49 @@
import os
from lib import workspace
# Tools untuk membantu sistem Hendrik.
# State workspace sesi dimiliki lib.workspace dan dipakai bersama oleh
# tool coding, vision, dan TUI -- di sini hanya dibungkus sebagai tool aksi.
schema_workspace_change = {
"type": "function",
"function": {
"name": "workspace_change",
"description": "Change the current working directory of the agent session. Supports tilde (~) expansion for the user home directory (/home/aji/).",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string", "description": "The target directory path to change to."}
},
"required": ["path"]
}
}
}
def workspace_change(path):
previous_path = workspace.get_current_workspace()
try:
if path.startswith("~"):
target_path = os.path.expanduser(path)
else:
target_path = os.path.abspath(path)
if os.path.exists(target_path) and os.path.isdir(target_path):
workspace.set_current_workspace(target_path)
return (
f"Successfully changed workspace.\n"
f"previous workspace: {previous_path}\n"
f"current workspace : {target_path}"
)
else:
return (
f"Error: The path {target_path} does not exist or is not a directory.\n"
f"previous workspace: {previous_path}\n"
f"current workspace : {previous_path} (unchanged)"
)
except Exception as e:
return (
f"Error changing workspace: {str(e)}\n"
f"previous workspace: {previous_path}\n"
f"current workspace : {workspace.get_current_workspace()}"
)

View File

@ -6,7 +6,7 @@ import tempfile
import urllib.request import urllib.request
import ftplib import ftplib
from urllib.parse import urlparse from urllib.parse import urlparse
from tools.coder import get_current_workspace from lib import workspace
MIME_MAP = { MIME_MAP = {
".jpg": "image/jpeg", ".jpg": "image/jpeg",
@ -152,7 +152,7 @@ def describe_image(path):
return f"Error fetching SFTP: {e}" return f"Error fetching SFTP: {e}"
# ── Local file ───────────────────────────────────────────────────── # ── Local file ─────────────────────────────────────────────────────
full_path = os.path.join(get_current_workspace(), path) if not os.path.isabs(path) else path full_path = os.path.join(workspace.get_current_workspace(), path) if not os.path.isabs(path) else path
if not os.path.isfile(full_path): if not os.path.isfile(full_path):
return f"Error: file not found: {full_path}" return f"Error: file not found: {full_path}"