iT邦幫忙

2026 iThome 鐵人賽

DAY 10
0

從 Day 2~4 加入工具,Day 5~7 獨立 agent 和 provider,Day 8~9 system prompt 部分,不知道各位有沒有發現一件很尷尬的事,我們把各個功能都模塊化的放在各個地方,但主程序在哪裡呢?從 Day 1 之後,程序就無法正常的運行起來了。
而接下來五篇,我們要來逐步解決這個問題,前三篇先搞定好負責渲染的 CLI 介面,第四篇搞定 ollama 進程管理,第五篇正式把 if __name__ == "__main__": 接入!


關於 CLI

Command-Line Interface,俗稱命令行介面,是一種透過純文字輸入指令來和終端畫面互動的介面,相較於對使用者較友善的 GUI(圖形化介面)來說,更易於製作,所以本專案使用的是 CLI 的方案。

而在這裡,先來一次完整說明整個專案的 CLI 介面的編排邏輯,方便各位後續閱讀程式碼以及解釋。
目前分成了主程序、輸入捕捉、渲染以及進程管理四個部分:

src/meowgent/
 └── cli/
      ├── __init__.py
      ├── main.py                  # 進入點
      ├── input_prompt.py          # 專門負責 prompt_toolkit 的輸入框
      ├── renderers.py             # 專門負責 rich 的畫面渲染
      └── ollama_manager.py        # 管理 ollama 的進程開關

先講一下每個檔案要做的事情:
main.py
1. 初始化 Agent 物件。
2. 呼叫 renderers.py 做畫面初始化。
3. 啟動 ollama 進程。
4. 呼叫 input_prompt.py 獲取輸入。
5. 開啟迴圈(讓使用者不斷問問題)。
6. 呼叫 renderers.py 渲染對話。

input_prompt.py
1. 取得用戶輸入(包含用戶輸入的渲染)。
2. 紀錄輸入歷史(按方向鍵上可追溯紀錄)。
3. 工具調用的審核和渲染。

這裡雖然也包括了渲染選項的部分,但由於渲染同時可以獲得輸入,所以歸類在「輸入」。

renderers.py
1. 分隔線渲染。
2. 介面初始化渲染。
3. 單行流式渲染(推理和工具參數句)。
4. 推理總結渲染。
5. 工具調用結果渲染。
6. 模型回覆渲染。

ollama_manager.py:進程的啟動和關閉

__init__.py 我們在這裡先把未來用到的都先做匯入(未來可能還會加入新東西,然後我可能會忘了提到要補充,所以各位自己再檢查一下啦,哈哈):

# src/meowgent/cli/__init__.py
from .renderers import CLIRenderer
from .input_prompt import get_input, get_tool_approval, select_directory
from .ollama_manager import start_ollama, clean_ollama

輸入部分

事不宜遲,我們先來完成輸入的部分吧!也就是 input_prompt.py

獲取輸入

外部建立 _prompt_session 是因為整個程序必須全程使用同一個 PromptSession 物件,才能保存歷史輸入,
函數內部 PromptSession 物件只在外部無 _prompt_session 時才建立。
style 的部分做的是色彩樣式的管理,語法是這樣的:

style = Style.from_dict({
    "選擇器 Key": "樣式屬性 Value"
})

這裡,我們用的是空字串,表示為「全域」,也就是整個 PromptSession 物件相關的都會受影響。
接著還需要用 .prompt() 才能讓用戶輸入內容

# src/meowgent/cli/input_prompt.py
from prompt_toolkit import PromptSession
from prompt_toolkit.styles import Style
from typing import Optional

_prompt_session: Optional[PromptSession] = None

def get_input() -> str:
	global _prompt_session
	
	_prompt_session = PromptSession(
        style=Style.from_dict({
            "": "ansiblue" # 藍色
        })
    ) if _prompt_session is None else _prompt_session # 歷史輸入管理,如果已經建立過,不再建立
    
    return _prompt_session.prompt("> ")

未來,我們在 main.py 呼叫 get_input() 時就能獲得以下結果了:

> 幫我在下載資料夾內找一下所有的 PDF 檔案
▲               ▲
│               └──── 使用者打字:【ANSI 藍色字體】
└──────────────────── 提示箭頭:【ANSI 藍色字體】

且按下方向鍵上可以查看剛剛輸入的內容。


工具調用審核

我們現在來建立「工具審核的選項」,如以下兩種樣式,使用者只要按下鍵盤的 y 或 n(不分大小寫)就可進行審核:

? [工具調用]: list_file
  • path: /Users/matthew/Downloads
  • pattern: *.xlsx

  是否允許執行? (Y/n)
? [工具調用]: write_file
  • file_path: /Users/matthew/hello.py
  • content:
    import os
    import sys

    def main():
        print("Hello Meowgent!")

  是否允許執行? (Y/n)

如果只有一行參數就不進行換行直接顯示 • 參數名: 參數
而如果是像程式碼,或者是撰寫文章,就需要換到下一行,並且做縮排的處理

這裡,我們對齊輸入時的樣式(> )從 2 個字元後開始輸出文字。

主要是要對參數的部分做處理,看到上面的範例中,其實模型輸出的參數是長 import os \n import sys ... 這樣的,要對每個換行做縮排 4 格的處理,才能對齊格式。
for 取出參數字典的「參數名」跟「參數」,先檢查是否有換行(沒有就不需進行縮排的處理),用 splitlines() 把每行切成串列形式,再用 .join() 把換行放回去並加上縮排拼接回去,注意,第一行處要另外加入換行跟縮排append()lines 時。
做完了每一個參數的處理,把 lines 拼起來,中間加上換行,統合成 args

# src/meowgent/cli/input_prompt.py
from ...
import questionary

...

def get_tool_approval(tool_name: str, tool_args: dict) -> bool:
    lines = []
    for k, v in tool_args.items():
        v_str = str(v) # 防止模型如果傳入數字等非字串

        if "\n" in v_str:
            # 多行內容:每一行皆整齊縮排 4 格
            indented_v = "\n    ".join(v_str.splitlines())
            lines.append(f"  • {k}:\n    {indented_v}")
        else:
            lines.append(f"  • {k}: {v_str}")

    args = "\n".join(lines) if lines else "  (無參數)"

最後,用 questionary.confirm 渲染出選單:

要記得 questionary 的選擇後面都要加上 .ask()

# src/meowgent/cli/input_prompt.py
...

def get_tool_approval(...) -> ...:
	...
	
	return questionary.confirm(
        f"[工具調用]: {tool_name}\n{args}\n\n  是否允許執行?",
        default=True,
        style=Style([
            ("question", "dim"),         
            ("instruction", "dim")
        ])
    ).ask()

這邊又出現了 Style()色彩設定,沒錯,它跟剛才 PromptSession 用的是同一個 Style()
從匯入的地方 from prompt_toolkit.styles import Style 就可以明顯地看出 questionary 使用了 prompt_toolkit 的 Style 物件

"question"? [工具調用]: list_file 最前面的那個問號;"instruction" 則是 (Y/n) 提示。
"dim" 會把字體變暗(降低亮度),通常會呈現出「灰色」的效果。


如此一來,input_prompt.py 的部分就完成啦!下一篇,來處理主要的渲染部分 - renderers.py


上一篇
Day 9 - 訂製 system prompt
系列文
手刻 AI Agent!大一新生的 Python 實戰筆記10
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言