轉眼間已經來到 Day 20 了,也來到了設定檔的最後一篇,馬上設定的功能就可以完善了,我們繼續來處理更改設定的指令 _change_config()!
回顧一下,昨天有說到這個函數要處理的四件事:
昨天上傳文章忘記把照片傳上去(而且昨天那篇圖片很多張啊 T_T),可能有些地方讀起來怪怪的,已經改好了,可以再稍微看一下圖片部分
我們要區分出以下四種:
前兩者只要分別導向兩個函數處理後結束 _change_config(),分別是等等要新增的 _cancel_select() 和 Day 15 做的 _change_model()。
結束的部分,因為如果選了三或四(二的取消在
_change_model()裡)後取消,也需要結束的邏輯,所以做成函數。
剛剛有說到退出的部分要導 _cancel_select(),先從這個函數開始吧!
其實很簡單,就只是印出要退出,然後加個分隔線:
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
""" ... """
def _cancel_select():
cli.console.print(Padding("[red]退出設定更改[/red]", (0, 0, 0, 2)))
cli.console.print(cli.get_rule())
choices ...
...
調用處則是呼叫完函數用 return 退出 _change_config():
判斷部分的
None是為了捕捉上方選擇時 ctrl c 退出。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
select = questionary.select(...).ask()
if select == "離開設定" or select == None:
_cancel_select()
return
模型的部分也非常簡單,呼叫 _change_model() 後 return 就好了:
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
if select == ("models", "default_model"):
_change_model(model_object=model_object, cli=cli, config=config)
return
對應到的是第三點以及第四點。
首先要把資訊提取出來,分別要有:
now_value(拿來做選擇的預設值)。desc(項目的標題)。value_type(型別驗證用)。# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
cat_name, field_name = select # 提取出選擇
sub_mdoel: BaseModel = getattr(config, cat_name)
sub_class = sub_mdoel.__class__ # 子類別
field_info = sub_class.model_fields[field_name] # 取出欄位內的內容
now_value = getattr(sub_mdoel, field_name) # 當前值
desc = field_info.description # 描述
value_type = field_info.annotation # 型別
new_value = None
new_value = None要先做宣告是因為等等它會在閉包內部被賦值,以及它是作為未變更情況下阻擋存檔的防護。
先做這個比較簡單的,只要讀取出 Literal 的選項就行。
對 value_type 使用 get_origin() 取得外層型別,判斷是不是 Literal。
這邊來講一下
get_origin()和get_args():
舉例來說,Dict[str, int]外層型別就是指字典,而字串及整數則是內層。
外層用get_origin()得到typing.Dict;內層用get_args()得到(str, int)。那這裡來延伸個觀念,如果是
Dict[Dict[str, int], str]怎麼辦呢?誰是外誰是內?
先做get_origin(),拿到的是「最外層」,也就是typing.Dict,然後get_args()會拿到「內層全部」-(typing.Dict[str, int], str),而得到這個元組,就能繼續往下扒開了,這邊就不繼續講了。而研究這部分時,我也發現了件事情,在 Python 3.9 以上 官方不建議從 typing 匯入容器型別了,直接可以用原生替代,也就是不用像以往這樣:
from typing import Dict test_dict_1 : Dict[str, int] = {"test": 0} # 以往做法直接不用匯入
test_dict_2 : dict[str, int] = {"test": 0}這樣就好了。
但為了專案的一致性,這邊我就不改了,先照著原先從 typing 匯入的方式就好。
如果判斷為是,就用 get_args() 拿出放在 Literal 裡的選項,最後在選項裡面加入一個取消:
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
if get_origin(value_type) is Literal:
options = list(get_args(value_type))
options.append("取消")
接著馬上放入 questionary.select() 渲染選項,選擇後先判斷是否取消或沒選,是則直接退出:
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
if get_origin ...:
...
new_value = questionary.select(
message=f"請選擇 {desc}:",
choices=options
).ask()
if new_value == "取消" or new_value == None:
_cancel_select()
return
這部分就比較麻煩了,我們需要「額外做一個函數」用來檢查用戶手動「輸入的值是否合規」,例如如果項目要求是數字而用戶輸入文字的話就不行,範圍設在 0~1.5 但輸入 20 也不行。
條件放在前面 Literal 檢查後面,用 questionary.text() 取得用戶輸入,validate 傳入的就是用來驗證的函數,我們等下馬上來實作。
判斷如果 text_result 得到 None 的話(也就是在輸入的地方用戶退出了)就呼叫 _cancel_select() 來做退出。
你可能會好奇,如果輸入「沒有通過驗證」,那怎麼辦?
看到上面的圖,左下角有提示「驗證錯誤」,沒錯!只有能通過驗證的輸入才能結束questionary.text()(除非退出)。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
if get_origin ...:
...
else:
text_result = questionary.text(
message=f"輸入 {desc} 的值:",
default=str(now_value),
# 要求為字串,要避免為數字
validate=_type_validator
# 會驗證直到通過(True),或 ctrl-c or esc 取消(None)
).ask()
if text_result is None:
# 如果輸入完值並完成驗證但最後退出選擇(因為驗證是邊輸入就邊在跑的)
_cancel_select()
return
而有發現一件事嗎,上面完全沒有賦值到 new_value 的動作,也就是說會在函數裡面用 nonlocal 對外部變數 new_value 做修改:
利用了「閉包」概念,簡單來說就是讓內部函數可以「讀取甚至修改外部函數的變數」。
像下面這樣雖然看起來只是簡單的讀取而已,但也是閉包的應用:def make_multiplier(factor): # 外部函式的區域變數:factor def multiplier(number): # 內部函式引用了外部函式的變數 factor return number * factor # 回傳內部函式物件本身 return multiplier而另一種應用就是
nonlocal new_value這樣。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
def _cancel_select():
...
def _type_validator(input: str) -> Optional[str]:
""" 驗證輸入是否合規 """
nonlocal new_value # 宣告修改外層變數(閉包)
而驗證的方法就是直接把輸入放入資料模型,看會不會報錯(用 try 捕捉),驗證成功時回傳 True,否則回傳字串。
一開始傳入的是字串,但當我們把字串放入資料模型內
model_validate()就會自動轉換為正確型別,例如int,所以賦值時不需要額外做型別轉換。
而回傳的字串就是剛剛圖片中左下角顯示的紅字。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
def _type_validator(...) -> ...:
...
format_dict = sub_mdoel.model_dump() # 取出內容轉字典
format_dict[field_name] = input # 將輸入值替換進去
try:
validated_obj = sub_class.model_validate(format_dict)
new_value = getattr(validated_obj, field_name)
# 已經不是字串,而是正確型別了
return True
except ValidationError as e:
return f"驗證錯誤 {e.errors()[0]['msg']}"
except Exception as e:
return f"錯誤 {e}"
為什麼
f"驗證錯誤 {e.errors()[0]['msg']}"這邊裡面用了「單引號」,如果是 Python 3.12 之前的版本,f""時字串檢查只會依照雙引號的位置來判定,所以如果取值的地方鍵用雙引號就會造成「字串提早結束」,但是後面還有東西,就報錯了。
(f''時也一樣,裡面要改用雙引號)
修改的值存入到 new_value 後,還需要做存檔(調用 save_config()):
判斷還外加一個
new_value != now_value避免值一樣,白忙一場。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
if new_value is not None and new_value != now_value: # 成功更改值
setattr(sub_mdoel, field_name, new_value) # 更改屬性
ConfigManager.save_config(config)
還沒結束喔!我們還需要把這些項目給應用上去,最後加上提示:
模型部分已經有更新邏輯,provider 部分於
_change_model()做更新,system prompt 每次對話自動讀取 config 做更新。
# src/meowgent/cli/commands.py
...
@cmd_registry(...)
def _change_config(...):
...
if ...: # 延續前面的 if new_value is not None...
if cat_name == "models" and field_name == "temperature":
if hasattr(model_object.provider, "temperature"):
model_object.provider.temperature = new_value
elif cat_name == "agent":
if field_name == "max_turns":
model_object.max_turns = new_value
elif field_name == "tool_approval_mode":
model_object.tool_approval_mode = new_value
cli.console.print(Padding(
f"[green]{desc} 由 {now_value} 更新至 {new_value}[/green]",
(0, 0, 0, 2)
))
else:
cli.console.print(Padding(f"[dim]設定未變更[/dim]", (0, 0, 0, 2)))
哇!搞定了,_change_config() 這函數的內容也真是長啊~寫了差不多 150 行,佔了 commands.py 的一半了。
而經歷了 Day 17 到今天的 Day 20,設定檔相關的功能終於全部完善了,趕緊試試 /config 吧!
而明天要做的事是非常令我期待的「圖片多模態」讀取,馬上我們的 Agent 就可以看照片啦,真是太棒了!