當程序首次被啟動時,main.py 調用 get_system_prompt() 時需要有 path 參數,也就是模型的「工作目錄」,來限制在區域內做事。
而先前,我們只能像這樣用手動的方式來自訂,且無法修改:
# src/meowgent/cli/main.py
from ...
import os
...
if __name__ == "__main__":
...
model.renew_system_prompt(
...,
path="~/Download"
)
現在我們來實作可以自選路徑的功能吧!
它可以從 main.py 和 commands.py 調用,分別對應到首次確立和指令修改。
匯入 pathlib 以及 inspect,並建立 select_directory():
# src/meowgent/cli/input_prompt.py
from ...
...
from pathlib import Path
import inspect
...
def select_directory(start_dir: Optional[str] = None) -> Optional[str]:
if start_dir is None:
now_dir = Path.home() # 若無傳入起點,則在主目錄
else:
now_dir = Path(start_dir)
選擇的邏輯是這樣的,且在 while True 裡做循環:
"選擇此目錄":選定目前的資料夾 -> 返回目前路徑。"上一頁":不在頂層資料夾時才可用,回到上層資料夾,繼續下一輪迴圈。now_dir,繼續下一輪迴圈。"取消":只有在指令調用時才可用(main.py 調用時還沒有路徑,一定要選一個路徑出來)。
先把 "選擇此目錄" 放在清單(file_list)第一個選項,.iterdir() 用迭代器的形式拿出 Path 物件放入 item,接著用 .is_dir() 和 .name.startswith(".") 做出「是資料夾且非隱藏資料夾」的判斷,符合條件的才加入清單:
# src/meowgent/cli/input_prompt.py
...
def select_directory(...):
...
while True:
file_list = ["選擇此目錄"]
for item in now_dir.iterdir():
if item.is_dir() and not item.name.startswith("."):
# 是資料夾且非隱藏資料夾
file_list.append(item.name)
再來是 "上一頁" 和 "取消" 的選項:
前者用 now_dir != now_dir.parent 判斷;後者用 inspect.stack()[1].filename 抓出呼叫的檔案路徑,再 Path().name 把檔名取出,判斷是否為 main.py。
如果沒有父目錄
.parent會回傳自己原先路徑
# src/meowgent/cli/input_prompt.py
...
def select_directory(...):
...
while True:
...
if now_dir != now_dir.parent: # 不在目錄最頂層 -> 加入上一頁
file_list.append("上一頁")
if Path(inspect.stack()[1].filename).name != "main.py":
# 不在主程序呼叫(首次設定路徑)-> 可取消更改路徑
file_list.append("取消")
然後,就可以用 questionary 來做選擇了,選完接著對 select_dir 做判斷:
# src/meowgent/cli/input_prompt.py
...
def select_directory(...):
...
while True:
...
select_dir = questionary.select(
"選擇工作目錄",
file_list,
style=Style([
('question', 'dim'),
('instruction', 'dim')
])
).ask()
if select_dir == "取消":
break # 退出迴圈 -> 相當於退出函數
elif select_dir == "選擇此目錄":
return str(now_dir) # Path -> str
elif select_dir == "上一頁":
now_dir = now_dir.parent
else:
now_dir = now_dir / select_dir # 做拼接
緊接著,我們要把 select_directory() 接入主程序裡頭:
這麼一來,我們一打開程序,就會直接跳出選擇目錄的選項,且「沒有取消選項」。
# src/meowgent/cli/main.py
from ...
from cli import ..., select_directory
...
if __name__ == "__main__":
...
model = ...
path = select_directory()
model.renew_system_prompt(
...,
path=path
)
os.chdir(path) # 進入指定工作目錄
cli = ...
先進行指令的註冊:
# src/meowgent/cli/commands.py
from ...
from pathlib import Path
import os
...
@cmd_registry("/cd")
def _change_path(args: list, cli: CLIRenderer, model_object: Agent, **kwargs):
""" 選擇工作目錄 """
from cli import select_directory
有沒有發現,在這裡我們才匯入
select_directory(),那為什麼不在最開頭匯入呢?
主要就是因為要避免「循環引用」的問題(有些地方可能也有發生,但我改完忘了,哈哈!如果有報錯再自己問一下 Ai ...)。
我們從主程序main.py的匯入開始看:#src/meowgent/cli/main.py from agent import Agent from providers import OllamaProvider from dotenv import load_dotenv from cli import get_input, get_tool_approval, CLIRenderer, start_ollama, clean_ollama, handle_cmd, select_directory # -> 執行到這裡了 ...第四行匯入了 cli,我們看到
src/meowgent/cli/__init__.py看看:# src/meowgent/cli/__init__.py from .renderers import CLIRenderer from .commands import COMMAND_REGISTRY, handle_cmd # -> 執行到這裡了 from .input_prompt import get_input, get_tool_approval, select_directory ...這邊第二行的地方,進到了
commands.py,而如果select_directory()在開頭這就被匯入:# src/meowgent/cli/commands.py(假設) from agent import Agent from cli import CLIRenderer, select_directory # -> 執行到這裡了 ...第二行又回到了
src/meowgent/cli/__init__.py裡,第一個拿的CLIRenderer沒有問題,在第一行就出現了,但是select_directory()第三行才出現(現在才走到第二行),就進入到了「循環」狀態,cli/__init__.py載入到一半去找commands.py,commands.py剛開始又回頭找cli/__init__.py,cli/__init__.py又想找select_directory()陷入無限循環,所以拋出ImportError報錯。
那你可能會想,把
cli/__init__.py裡input_prompt.py跟commands.py的匯入順序對調不就好了?
哈!當然沒那麼簡單。# src/meowgent/cli/input_prompt.py ... import inspect from cli import COMMAND_REGISTRY # -> 執行到這裡了因為進入到
input_prompt.py後,最後一行還是要進到commands.py,到commands.py後同樣會面臨相同的「循環」問題。
所以,這裡的解法是「延遲匯入」,我們等到真正需要使用到
select_directory()的_change_path()裡再去做匯入。
註冊搞定以後,來進行邏輯的處理,輸入 /cd 後,要分成兩種情況:
select_directory() 選擇。
由於
args為「串列」需要將其拼接為字串,而字串中間要有空格是為了防止如my place這樣有空格的資料夾名稱被切開(handle_cmd()切的)無法正確還原,拼接完後還要轉 Path 物件並轉為「絕對路徑」(防止輸入為相對路徑)。
/cd,沒有包含路徑(args 為空):直接調用 select_directory() 選擇。# src/meowgent/cli/commands.py
...
@...
def _change_path(...):
if args:
...
path = Path(" ".join(args)).expanduser()
if path.is_dir():
new_path = path
else:
cli.console.print(Padding("[red]查無此路徑[/red]", (0, 0, 0, 2)))
new_path = select_directory(start_dir=str(Path.cwd()))
else:
new_path = select_directory(start_dir=str(Path.cwd()))
if new_path:
os.chdir(new_path) # 切換到路徑
最後,別忘了要更新 system prompt 給模型,順便輸出一下切換訊息:
# src/meowgent/cli/commands.py
...
@...
def _change_path(...):
...
if new_path:
...
model_object.renew_system_prompt(path=str(Path.cwd()))
cli.console.print(Padding(
f"[dim]路徑已切換到 {str(Path.cwd())}[/dim]",
(0, 0, 0, 2)
))
這樣這部分就搞定囉~
每個軟體中都會有「設定」功能,而在下一篇,我們來做設定檔的部分!讓我們的 Agent 程式也可以做設定!