上一篇,我們把設定檔的格式以及存取部分完成了,
而今天,我們要正式地來使用這些設定項目,也就是把它給接入我們的 Agent 系統裡,最後,我們還要做一個有趣的應用,可以期待一下~
先簡單來看一下 MeowgentConfig 要如何使用,
首先,要先獲取經過讀取的 MeowgentConfig 資料模型(也就是 config 而不是單純的 MeowgentConfig 資料模型,它只是預設的「設計圖」),用它來做處理:
..., config = ConfigManager.load_config()
如果是要「讀取」(取)這個項目則是:
當前模型 = config.models.default_model
而如果要「修改」(存)一個項目,例如 default_model:
config.models.default_model = "新模型名稱"
可以看出,讀取完的 MeowgentConfig 這個模型物件是我們要「存」「取」設定的媒介,而不是存在資料夾裡的 config.json 檔案。
所以,先來在 main.py 裡調用 load_config() 吧!
接著,就可以用 config 來取用設定項目了:
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
...
config_success, config = ConfigManager.load_config()
...
這裡,我們先把預設的模型給綁定上去:
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
config_success, config = ...
model_name = config.models.default_model
model = ...
...
然後除了這裡,還記得 Day 15 做的 _change_model() 嗎?
用指令改完模型後,也別忘了要傳入設定檔以及改設定檔,但在這之前,別忘了 handle_cmd() 還沒有接收 config 這個參數,在 main.py 也還沒有傳入它喔!
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
...
try:
while ...:
...
if user_input ...:
handle_cmd(..., config=config)
...
# src/meowgent/cli/commands.py
...
def handle_cmd(..., config: MeowgentConfig):
...
# src/meowgent/cli/commands.py
from ...
from config import MeowgentConfig, ConfigManager
...
@cmd_registry(...)
def _change_model(..., config: MeowgentConfig, **kwargs):
...
model_object.renew_system_prompt(...)
config.models.default_model = new_model
ConfigManager.save_config(config)
cli.console.print(...)
我們在建立 OllamaProvider 時加上一個參數 temperature,而別忘了 ollama.chat() 的參數部分也要跟進:
# src/meowgent/providers/ollama_provider.py
...
class OllamaProvider(LLMProvider):
def __init__(..., temperature: float = 0.1):
super()...
self.temperature = temperature
def stream_generate(...) -> ...:
...
response = ollama.chat(
...,
options={
...,
"temperature": self.temperature
}
)
...
然後接入 main.py:
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
...
model = Agent(OllamaProvider(..., temperature=config.models.temperature), ...)
這裡非常簡單,只需要傳入 max_turns 參數就行了:
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
...
model = Agent(OllamaProvider(...), max_turns=config.agent.max_turns)
...
這裡我們採取的方案是直接在 Agent 的 chat() 中依據審核模式判斷是否需要審核(need_approval),藉此決定是否需要呼叫 ask_tool_approval() 向使用者詢問:
# src/meowgent/agent.py
...
class Agent():
def __init__(..., tool_approval_mode: str = "default"):
...
self.tool_approval_mode = tool_approval_mode
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
...
model = Agent(..., tool_approval_mode=config.agent.tool_approval_mode)
接著,分別做各個模式的邏輯處理:
# src/meowgent/agent.py
...
class Agent():
...
def chat(...) -> ...:
while ...:
...
if tools_to_execute:
for ...:
...
t = ToolCall(...)
if self.tool_approval_mode == "approval_all":
need_approval = False
elif self.tool_approval_mode == "always ask":
need_approval = True
else: # "default" 時
need_approval = getattr(
TOOL_REGISTRY.get(t.tool_name, None),
"need_approval",
True
)
if not need_approval ...:
...
雖然可能不太會發生設定檔損毀的問題,但我們想像一下,某天使用者發現設定檔只是非常簡單的 JSON 格式,心血來潮想要自己來更改看看,但一不小心改壞了,該怎麼辦呢?
這裡就做一個非常有趣的應用了,這軟體本身不就是一個 Agent 嗎?我們讓程式「自動呼叫模型」來對設定檔進行修復。
建立 repair(),傳入的參數為設定檔(預設設定檔)以及負責輸出的 CLIRenderer 物件(要用裡面的 Console 物件),回傳為 MeowgentConfig,也就是資料模型物件。
# src/meowgent/config/config_repair.py
from .config_schema import MeowgentConfig
from pathlib import Path
import shutil
from .config_manager import CONFIG_FILE, ConfigManager
from cli import CLIRenderer
import textwrap
import questionary
from prompt_toolkit.styles import Style
from rich.padding import Padding
def repair(config: MeowgentConfig, cli: CLIRenderer) -> MeowgentConfig:
from agent import Agent
from providers import OllamaProvider
from cli import get_tool_approval
有些模組在函式內部才進行匯入,是為了避免「循環匯入」。
這時,先來處理一下在什麼情況下會去呼叫這個函數:
當 config_success 為 False 時需要呼叫此函數,而在 False 的狀況下,回傳的 config 只會是預設情況下的設定檔。
# src/meowgent/cli/main.py
...
if __name__ == "__main__":
...
cli = ...
...
config_success, config = ConfigManager.load_config()
if not config_success: # 未成功
config = repair(config=config, cli=cli)
要特別注意的是:CLIRenderer 物件要在
repair()前被建立(因為repair()會用到)。
repair() 要做到的事為什麼需要備份呢?那是因為,我們無法確定模型做的修復是否意外竄改了設定項目,最好還是保留原先錯誤的檔案以防萬一:
用的是 shutil.copy2(),參數的部分前者是原始檔路徑,後者是新路徑。
shutil 裡有很多類型的檔案複製函數,其中
shutil.copy2()是「最完整」的複製函數,它完整保留原始檔案的建立時間、修改時間(時間戳記)與讀寫執行權限,也就是會保留完整的「元數據」。copyfileobj() --> 僅複製 with open() 物件位元組串流 copyfile() -----> 僅複製硬碟檔案內容 (不帶權限、不帶時間) copy() ---------> 複製檔案內容 + 檔案權限 (chmod) copy2() --------> 複製檔案內容 + 完整元數據 (包含時間戳記 mtime/atime) 【最佳】
新路徑的部分,使用 Pathlib 的 with_suffix() 來替換副檔名。
.bak是專門用於做「備份」的,加在任何格式的副檔名後面。
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
backup_path = shutil.copy2(
CONFIG_FILE,
CONFIG_FILE.with_suffix(".json.bak")
) # 備份
接著要先建立模型物件(Agent 類別物件):
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
model = Agent(
OllamaProvider(model_name=config.models.default_model, temperature=config.models.temperature),
max_turns=config.agent.max_turns,
tool_approval_mode=config.agent.tool_approval_mode
)
有發現嗎?跟
main.py裡完全一模一樣呢!
然後,先把要給模型做修復的 prompt 組裝好:
while是為了要在沒有成功修復的情況下可以繼續做修復,等等會詳細說到。
model_json_schema()可以取得資料模型的「JSON Schema 規範定義」(定義了每個欄位的名稱、型別與範圍約束),讓模型知道正確的格式結構應該長什麼樣子。CONFIG_FILE是上一篇在config_manager.py裡定義的:# src/meowgent/config/config_manager.py ... CONFIG_DIR = Path.home() / ".meowgent" CONFIG_FILE = CONFIG_DIR / "config.json" ...
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
while True:
prompt = textwrap.dedent(f"""
你是一個專門修復損壞 JSON 設定檔的助理,且回答時力求精簡。
以下是這份設定檔應當符合的正確 JSON Schema 規範:
{MeowgentConfig.model_json_schema()}
對無法正常解析的 JSON 內容做修復:
```json
{CONFIG_FILE.read_text(encoding="utf-8")}
```
在不更動原始數據的前提下,用 write_file 工具,
直接將 {CONFIG_FILE} 修改成正確樣式,
盡量在只使用一次工具的情況下準確完成。
調用工具前不說客套話、開場白,例如「好的,我調用工具來做修復:」,直接調用工具。
""")
跟之前在 Day 9 做 system prompt 時一樣,用
textwrap.dedent()來優化縮排。
有注意到上面的提示詞裡的最後一句嗎?為什麼要要求模型不要廢話直接調用工具呢?
這邊先解釋一下底下的邏輯,我們的目的是讓模型「盡可能的減少不必要的 token 輸出」,也就是回答部分盡量避免,以節省時間。
所以理想情況上,狀態為 response 時(視為工具調用結束)就不讓模型繼續輸出了,
而提示詞的最後一行就是為了避免出現「好,這就來調用 write_file <tool_call>...」這樣導致前面文字輸出就誤判,直接結束迴圈(工具根本還沒執行)。
但還有一個問題,如果模型不聽話,還是說了廢話怎麼辦?
這邊判斷的部分,會先檢查是否執行過工具(用 executed_tool 記錄),執行過才放行到 response 狀態下的 break。
但如果模型真的很皮,第二次調用工具還是先廢話,那就沒救了哈哈。
所以在提示詞的部分我還特別提醒了使用 write_file 以及盡量在使用一次的情況下完成。但也是有其他的方法可以試試,像是不使用工具,而是叫模型回答只說 JSON 格式的內容,然後再寫把格式放入檔案的邏輯,
但這樣做同樣怕模型不聽話,輸出了除了格式以外的其他廢話。
模型輸出的部分跟在 main.py 一樣是用 for 來獲取出 chat 裡 yield 回傳的內容,然後根據上面所說的邏輯做出停止的邏輯:
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
while True:
...
executed_tool = False
for chunk in model.chat(
user_input=prompt,
tool_approval=get_tool_approval
):
if chunk.status == "tool_executed":
executed_tool = True
elif chunk.status == "response" and executed_tool:
break
模型執行完後,不能盲目的認定已經修復好了,還需要對其做驗證:
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
while True:
...
success, config = ConfigManager.load_config()
這邊就是前面說過的,如果失敗的話,讓使用者選擇是否繼續修復,
所以在成功以及選擇不繼續時「跳出迴圈」:
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
while True:
...
if success:
break
else:
if questionary.confirm(
"格式驗證失敗,是否交由模型繼續修復",
default=False,
style=Style([
("question", "dim"),
("instruction", "dim")
])
).ask() is False:
break
直到迴圈結束,就可以開始存檔了,分成幾種狀況做處理:
先準備好 cover_confirm(是否覆蓋)、use_default_confirm(使用預設覆蓋) 兩個布林值,接著先處理情況 1 分割成 1-1 及 1-2:
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
while True:
...
# 狀態初始化
cover_confirm = False
use_default_confirm = False
if success: # 如果驗證格式成功
cli.console.print(Padding("[green]模型成功修復設定檔[/green]", (0, 0, 0, 2)))
cover_confirm = questionary.confirm(
"已成功修復設定檔,是否覆蓋套用?",
default=True,
style=Style([
("question", "dim"),
("instruction", "dim")
])
).ask()
情況 1-1,模型的修復被批准了,調用 save_config() 做存檔,回傳資料模型給 main.py:
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
if cover_confirm: # 使用者同意套用模型的修復
ConfigManager.save_config(config)
cli.console.print(Padding("[green]修復檔成功覆蓋[/green]", (0, 0, 0, 2)))
return config
仔細看會發現,1-2 和 2 底下的情況是一樣的,這裡直接共用判斷:
細節處我們用
remind做出區別。
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
if not success or not cover_confirm:
# 如果模型修復的還是沒辦法讀 or 使用者拒絕用模型修復的
remind = "已取消使用模型修復的檔案" if success else "模型修復失敗"
use_default_confirm = questionary.confirm(
f"{remind},是否將預設設定覆蓋到設定檔(已將損壞檔案備份於 {backup_path})",
default=True,
style=Style([
("question", "dim"),
("instruction", "dim")
])
).ask()
接下來是用預設來做覆蓋,也就是 1-2-1 跟 2-1,也是要回傳資料模型(在這裡回傳的是預設的設定):
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
if use_default_confirm: # 使用者同意套用預設值 -> 重置為全新乾淨的預設物件!
config = MeowgentConfig()
ConfigManager.save_config(config)
cli.console.print(Padding("[green]已套用預設設定[/green]", (0, 0, 0, 2)))
return config
最後剩下的就是 1-2-2 和 2-2 的不做任何覆蓋了,這邊需要把原始的檔案做恢復(把備份檔複製回原始的檔案做覆蓋),同樣是用到 shutil.copy2(),最後回傳資料模型:
# src/meowgent/config/config_repair.py
...
def repair(...) -> ...:
...
if use_default_confirm:
...
else: # 使用者不同意套用預設 -> 還原原始備份,當次以預設物件開機
shutil.copy2(backup_path, CONFIG_FILE)
cli.console.print(Padding("[green]備份檔已恢復[/green]", (0, 0, 0, 2)))
return MeowgentConfig()
哇~今天這篇好長啊,應該是至今為止最長的一篇了!
我們把昨天做好的設定項目都給套用了上去,還做了讓模型修復 JSON 設定檔的應用。
但你有沒有發現,這些設定都還沒有可以做更改的管道呢,下一篇,我們來處理這件事!