iT邦幫忙

2026 iThome 鐵人賽

DAY 16
0

當程序首次被啟動時,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 裡做循環:

  1. "選擇此目錄":選定目前的資料夾 -> 返回目前路徑。
  2. "上一頁":不在頂層資料夾時才可用,回到上層資料夾,繼續下一輪迴圈。
  3. 選路徑:更新 now_dir,繼續下一輪迴圈。
  4. "取消":只有在指令調用時才可用(main.py 調用時還沒有路徑,一定要選一個路徑出來)。
    https://ithelp.ithome.com.tw/upload/images/20260928/20183608Zo9TgS4o4y.png

先把 "選擇此目錄" 放在清單(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 # 做拼接

接入 main.py

緊接著,我們要把 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 後,要分成兩種情況:

  1. 輸入的指令已經包含了路徑:檢查確認是「存在的路徑」可以直接改為此路徑,否則調用 select_directory() 選擇。

    由於 args 為「串列」需要將其拼接為字串,而字串中間要有空格是為了防止如 my place 這樣有空格的資料夾名稱被切開(handle_cmd() 切的)無法正確還原,拼接完後還要轉 Path 物件並轉為「絕對路徑」(防止輸入為相對路徑)。

  2. 只輸入 /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 程式也可以做設定!


上一篇
Day 15 - 輸入指令來改東西
下一篇
Day 17 - 設定檔 - 上
系列文
手刻 AI Agent!大一新生的 Python 實戰筆記 共 18 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言