設定檔的格式在前面兩天的努力下已經完成了,但是,現在有辦法修改設定嗎?
當然沒辦法(除非手動找到檔案去改),所以今天要來做一個指令來去更改設定檔。
首先,把資料模型加入 handle_cmd() 的參數裡:
# src/meowgent/cli/commands.py
...
def handle_cmd(..., config: MeowgentConfig):
...
else:
func(..., config=config)
也別忘了要傳入參數:
# src/meowgent/cli/main.py
if __name__ == "__main__":
try:
while ...:
...
if user_input.startswith("/"):
handle_cmd(..., config=config)
接著,就可以建立一個處理設定指令的函數:
這邊傳入 CLIRenderer 物件、資料模型、Agent 物件三樣東西。
# src/meowgent/cli/commands.py
from ...
from rich.panel import Panel
from pydantic import BaseModel, ValidationError
from typing import get_origin, get_args, Literal, Optional
import inspect
...
@cmd_registry("/config")
def _change_config(
cli: CLIRenderer,
config: MeowgentConfig,
model_object: Agent,
**kwargs
):
""" 更改設定項目 """
這個函數裡面要處理以下幾件事:
動態取得資料模型的格式。
將取出的項目做選項拼接,顯示效果如下圖供使用者去選擇:
![[截圖 2026-09-27 下午5.31.07.png|355]]
解析選擇的項目,進到選擇的項目進行設定,如下面三張圖(用選的及用打字的):
![[截圖 2026-09-27 晚上11.23.18.png|347]]
上圖這是 Day 15 實作的
_change_model(),雖然可以用/model做更改,但既然設定檔上有這個項目,這邊也一併做了!
![[截圖 2026-09-27 晚上11.22.03.png|416]]
![[截圖 2026-09-27 晚上11.21.14.png|458]]
存檔以及對更新項目做應用。
今天先處理前兩項,剩下的兩項交給下一篇來做,我們一步一步來完成吧!
現在我們要做的事,就是先把上面那張圖片裡的每一行都**「動態」收集起來**。
沒錯!是動態的,也就是說未來加入新的項目也不需要對這部分代碼做更動。
但這邊比較麻煩的是,我想要做成像資料夾一樣的樹狀顯示效果,上方顯示類別,下面有子項目可以選擇,所以還需要特別做 ├──、└── 等等的判斷邏輯(當然也是動態的)。
大類別指的是
ModelsConfig、AgentConfig。
剛剛說過,我們要將所有選項收集起來放入 choices 串列中;首先用 questionary.Separator() 加入的第一項是下圖圈起來的部分:
![[Pasted image 20260927231634.png|318]]
questionary.Separator()是用在questionary.select()裡「不當作選項」的項目,也就是這行「不能選」。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
choices = [questionary.Separator("|")]
緊接著,用 model_fields 這個屬性取出資料模型的欄位字典,字典的鍵為欄位名(也就是這邊要拿的 models、agent),所以直接用 keys() 取鍵,最後把取出來的鍵轉成串列存入 cat_item。
由於只是要拿到上方類別的名字,這邊直接拿
MeowgentConfig而不拿config。
從很多地方的命名可能都會看到跟貓有關的,但
cat_item的 cat 可不是貓喔!
是「category - 類別」的簡寫。
下面還對串列做了計數,這是為了要做「樹狀圖結構判斷」。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
cat_item = list(MeowgentConfig.model_fields.keys())
cat_num = len(cat_item)
雖然我們已經得到了欄位名了,但還記得我們在 """ ... """ 有寫上中文註解嗎?其實這更符合拿來當名字,所以我們要把它給取出來。
用迴圈依序把放在串列裡的欄位名取出,並用 enumerate() 標上索引 cat_idx(同樣是為了樹狀圖結構)。
接著,要取得子資料模型物件 sub_mdoel,用它 .__class__ 取得類別本身,就可以拿到在 """ ... """ 寫的名字了(用 inspect.getdoc() 然後 strip() 清最後面的空格)!
特別將
sub_mdoel存為獨立變數是因為稍後還會用到。
為什麼用
getattr()而不直接用.獲取屬性呢?
有注意到嗎?我們的屬性名稱是動態存在變數中的,而使用.存取時只能接固定的屬性名稱,無法傳入變數,所以必須用getattr()來動態取得屬性。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
for cat_idx, cat_name in enumerate(cat_item):
sub_mdoel = getattr(config, cat_name)
# getattr(...) 相當於 config.models、config.agent
sub_class = sub_mdoel.__class__ # 取得類別
cat_doc = inspect.getdoc(sub_class).strip()
# 名字(模型相關、Agent 行為)
標題(類別下方的註解內容)取出來了,接著要來判斷樹狀圖該怎麼畫:
雖然我們這邊只有兩個項目,其實可以直接畫也很簡單,但為了未來的擴充性,還是選擇用動態的方式做。
這邊判斷的邏輯是如果該項目的「索引和總數 - 1 相同」,那就代表是最後一項,用 └──,
把項目數擴大一點比較好做想像:
![[截圖 2026-09-28 上午11.35.51.png|190]]
最後一項完美符合「索引和總數 - 1 相同」條件,所以用└──,其他用├──。
判斷好後連同標題一起加進 choices,要注意的是這個標題同樣不給選取,所以要記得用 questionary.Separator() 包裝。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
for ...:
...
tree_prefix = "└──" if cat_idx == cat_num - 1 else "├──"
choices.append(questionary.Separator(f"{tree_prefix} {cat_doc}"))
同樣的,我們要先取出子類別的數據字典,但這邊,我們要保留整個字典,所以用 items() 拿完整的鍵值對,放在 field_item,然後一樣為了要做樹狀結構所以要計數:
這個數據字典包含了屬性名(鍵)及 FieldInfo 物件,這邊還需要後者的物件是因為等等要取用
annotation(型別)、default(預設值)、description(欄位說明) 這三個屬性。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
for ...:
...
field_item = list(sub_class.model_fields.items())
field_num = len(field_item)
接著透過迴圈依序取出並以 enumerate() 做編號,分別取得目前的值以及說明:
假設
cat_name為'models',field_name為'default_model'時getattr(sub_mdoel, field_name)相當於config.models.default_model,
同樣是因為利用變數去呼叫屬性,所以不用.而用getattr()(後面一直用到,就不再提了)。
而之所以要取得當前值,是因為選項標題設計了顯示當前數值的功能,如下圖:
![[Pasted image 20260928164632.png|357]]而
field_info.description就是Field()中的description這個參數:# src/meowgent/config/config_schema.py class ModelsConfig(...): ... default_model: str = Field( ..., description="模型" )
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
for ...:
...
for idx, (field_name, field_info) in enumerate(field_item):
# 項目當前值
now_value = getattr(sub_mdoel, field_name)
# 相當於 config.models.default_model
# 項目解釋
desc = field_info.description
# Field() 裡的 description 參數
接著再來處理子項目的樹狀圖邏輯:
這邊有左右兩個地方要做處理(左邊 indent 右邊 branch):
![[Pasted image 20260928170336.png|304]]
前者的邏輯跟剛才的 tree_prefix 邏輯類似:如果不是最後一個大類別,就需要有 │ 連接線;如果是最後一個大類別,底下已經沒有其他類別了,所以直接留白(" ")。
後者的邏輯其實也是相同的,只是判斷的變數不同:如果是最後一個子項目就用 └──,否則用 ├──。
而這邊用了 questionary.Choice(),效果是選取後可以得到「和顯示值不同的東西」,顯示效果是 title,得到的值是 value,這裡後者設為用元組回傳 cat_name、field_name 兩個變數(給後面選取後取值用):
如果沒用
questionary.Choice(),而是choices.append(f"{indent + branch}{desc} (目前: {now_value})")選取後會得到字串本身。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
for ...:
...
for ...:
...
indent = " " if cat_idx == cat_num - 1 else "│ "
branch = "└── " if idx == field_num - 1 else "├── "
choices.append(
questionary.Choice(
title=f"{indent + branch}{desc} (目前: {now_value})",
value=(cat_name, field_name)
)
)
前面已經把該取出的東西都取出並放進 choices 裡了,接下來做一下收尾:
如果判斷這個類別不是最後一個,加上圖中這一個 Separator("│") 做出空一行的美化效果:
![[Pasted image 20260928171757.png|400]]
接著用「沒有參數」的 questionary.Separator() 達到分隔線的效果,最後加入離開的選項。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
for ...:
...
for ...:
...
if cat_idx < cat_num - 1:
choices.append(questionary.Separator("│"))
choices.append(questionary.Separator()) # 分隔線
choices.append("離開設定")
把 choices 傳入 questionary.select() 來完成選單渲染:
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
select = questionary.select(
message="選擇設定項目",
choices=choices
).ask()
前兩項搞定了!
明天,我們繼續處理選項的解析以及存檔部分。