CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
235 lines
10 KiB
Python
235 lines
10 KiB
Python
"""Co4E custom-agent editor — create/edit a persisted persona.
|
|
|
|
Mirrors nova's agent-config-panel field set: name, role, icon, instructions,
|
|
model, permission preset, and an attached-skills checklist.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from typing import List
|
|
|
|
from PySide6.QtCore import Qt
|
|
from PySide6.QtWidgets import (
|
|
QComboBox, QDialog, QDialogButtonBox, QFormLayout, QHBoxLayout, QInputDialog,
|
|
QLineEdit, QListWidget, QListWidgetItem, QPlainTextEdit, QPushButton,
|
|
QVBoxLayout, QWidget,
|
|
)
|
|
|
|
from ..core.co4e import PERMISSION_PRESETS, CustomAgent
|
|
from ..core.worker import AgentWorker
|
|
from ..i18n import tr
|
|
from .icons import icon, icon_picker_combo
|
|
|
|
|
|
class Co4EAgentDialog(QDialog):
|
|
"""Hộp thoại tạo/sửa một agent tự tạo của Co4E: tên, vai trò, model, chỉ dẫn,
|
|
skill và tệp đính kèm.
|
|
"""
|
|
def __init__(self, ctx, agent: CustomAgent, skill_names: List[str], parent=None):
|
|
"""Form thêm/sửa một agent Co4E: tên, prompt và các skill nó được dùng."""
|
|
super().__init__(parent)
|
|
self.ctx = ctx
|
|
self._agent = agent
|
|
self.setWindowTitle(tr("co4e.agent_edit_title") if agent.name else tr("co4e.agent_new_title"))
|
|
self.resize(460, 480)
|
|
form = QFormLayout(self)
|
|
|
|
self.name_edit = QLineEdit(agent.name)
|
|
form.addRow(tr("co4e.f_name"), self.name_edit)
|
|
self.role_edit = QLineEdit(agent.role or "AGENT")
|
|
form.addRow(tr("co4e.f_role"), self.role_edit)
|
|
# Dropdown of every icon in the registry (Monitoring's Icon Management
|
|
# set + built-ins), each row previewing its actual glyph — still
|
|
# editable so a not-yet-added custom name can be typed directly.
|
|
self.icon_edit = icon_picker_combo(agent.icon)
|
|
self.icon_edit.lineEdit().setPlaceholderText(tr("co4e.f_icon_placeholder"))
|
|
form.addRow(tr("co4e.f_icon"), self.icon_edit)
|
|
self.instructions_edit = QPlainTextEdit(agent.instructions)
|
|
self.instructions_edit.setMaximumHeight(150)
|
|
# ✨ AI-assist — draft the agent's instructions from its name + role
|
|
# (handy when you're not attaching a skill).
|
|
self.gen_btn = QPushButton(tr("co4e.ai_draft"))
|
|
self.gen_btn.setIcon(icon("sparkle"))
|
|
self.gen_btn.setToolTip(tr("co4e.ai_draft_tooltip"))
|
|
self.gen_btn.setEnabled(ctx is not None)
|
|
self.gen_btn.clicked.connect(self._ai_draft)
|
|
instr_box = QWidget()
|
|
ib = QVBoxLayout(instr_box)
|
|
ib.setContentsMargins(0, 0, 0, 0)
|
|
ib.addWidget(self.instructions_edit)
|
|
ib.addWidget(self.gen_btn, alignment=Qt.AlignRight)
|
|
form.addRow(tr("co4e.f_instructions"), instr_box)
|
|
|
|
# Extra context — free-text background/info fed to the agent at run time.
|
|
self.context_edit = QPlainTextEdit(getattr(agent, "context", ""))
|
|
self.context_edit.setMaximumHeight(90)
|
|
self.context_edit.setPlaceholderText(tr("co4e.f_context_placeholder"))
|
|
form.addRow(tr("co4e.f_context"), self.context_edit)
|
|
|
|
model_row = QHBoxLayout()
|
|
self.model_combo = QComboBox()
|
|
self.model_combo.setEditable(True)
|
|
self.model_combo.setEditText(agent.model)
|
|
self.load_btn = QPushButton()
|
|
self.load_btn.setIcon(icon("download"))
|
|
self.load_btn.setToolTip(tr("co4e.load_models_tooltip"))
|
|
self.load_btn.clicked.connect(self._load_models)
|
|
self.load_btn.setEnabled(ctx is not None)
|
|
model_row.addWidget(self.model_combo, 1)
|
|
model_row.addWidget(self.load_btn)
|
|
mrow = QWidget(); mrow.setLayout(model_row)
|
|
form.addRow(tr("co4e.f_model"), mrow)
|
|
|
|
self.perm_combo = QComboBox()
|
|
for preset in PERMISSION_PRESETS:
|
|
self.perm_combo.addItem(tr(f"co4e.perm.{preset}"), preset)
|
|
idx = self.perm_combo.findData(agent.permission_preset)
|
|
self.perm_combo.setCurrentIndex(idx if idx >= 0 else 0)
|
|
form.addRow(tr("co4e.f_permission"), self.perm_combo)
|
|
|
|
self.skills_list = QListWidget()
|
|
self.skills_list.setMaximumHeight(120)
|
|
for name in skill_names:
|
|
it = QListWidgetItem(name)
|
|
it.setFlags(it.flags() | Qt.ItemIsUserCheckable)
|
|
it.setCheckState(Qt.Checked if name in agent.skills else Qt.Unchecked)
|
|
self.skills_list.addItem(it)
|
|
form.addRow(tr("co4e.f_skills"), self.skills_list)
|
|
|
|
# Attachments — files whose extracted text is fed to the agent at run time.
|
|
self.attach_list = QListWidget()
|
|
self.attach_list.setMaximumHeight(70)
|
|
self._attachments: List[str] = list(getattr(agent, "attachments", []) or [])
|
|
self._refresh_attach_list()
|
|
attach_add = QPushButton(tr("co4e.attach_add"))
|
|
attach_add.setIcon(icon("plus"))
|
|
attach_add.clicked.connect(self._add_attachment)
|
|
attach_del = QPushButton(tr("co4e.attach_remove"))
|
|
attach_del.setIcon(icon("trash"))
|
|
attach_del.clicked.connect(self._del_attachment)
|
|
ab = QHBoxLayout()
|
|
ab.addWidget(attach_add)
|
|
ab.addWidget(attach_del)
|
|
ab.addStretch(1)
|
|
abtn = QWidget(); abtn.setLayout(ab)
|
|
form.addRow(tr("co4e.f_attachments"), self.attach_list)
|
|
form.addRow("", abtn)
|
|
|
|
buttons = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Cancel)
|
|
buttons.accepted.connect(self.accept)
|
|
buttons.rejected.connect(self.reject)
|
|
form.addRow(buttons)
|
|
|
|
def _ai_draft(self) -> None:
|
|
"""Draft the instructions from the agent's name + role — first asking
|
|
for an optional description so the generated instructions can be more
|
|
specific/detailed than name+role alone would produce."""
|
|
if self.ctx is None:
|
|
return
|
|
name = self.name_edit.text().strip()
|
|
role = self.role_edit.text().strip()
|
|
if not name and not role:
|
|
return
|
|
hint, ok = QInputDialog.getMultiLineText(
|
|
self, tr("co4e.ai_draft_hint_title"), tr("co4e.ai_draft_hint_label"))
|
|
if not ok:
|
|
return
|
|
hint = hint.strip()
|
|
self.gen_btn.setEnabled(False)
|
|
ctx = self.ctx
|
|
|
|
def job(worker: AgentWorker):
|
|
"""Chạy nền: nhờ model soạn thử phần chỉ dẫn cho agent."""
|
|
from ..core.ai_task_planner import generate_agent_prompt
|
|
return {"text": generate_agent_prompt(ctx.build_active_provider(), name, role, hint,
|
|
cancel=worker.is_cancelled)}
|
|
|
|
def done(result: dict):
|
|
"""Đổ chỉ dẫn vừa soạn vào ô soạn thảo."""
|
|
self.gen_btn.setEnabled(True)
|
|
if result.get("text"):
|
|
self.instructions_edit.setPlainText(result["text"])
|
|
|
|
w = AgentWorker(job)
|
|
w.finished_ok.connect(done)
|
|
w.failed.connect(lambda _e: self.gen_btn.setEnabled(True))
|
|
self._gen_worker = w
|
|
w.start()
|
|
|
|
def _refresh_attach_list(self) -> None:
|
|
"""Vẽ lại danh sách tệp đính kèm (chỉ hiện tên tệp, đường dẫn để trong tooltip)."""
|
|
from pathlib import Path as _P
|
|
self.attach_list.clear()
|
|
for p in self._attachments:
|
|
item = QListWidgetItem(_P(p).name)
|
|
item.setToolTip(p)
|
|
self.attach_list.addItem(item)
|
|
|
|
def _add_attachment(self) -> None:
|
|
"""Thêm tệp đính kèm cho agent."""
|
|
from PySide6.QtWidgets import QFileDialog
|
|
files, _ = QFileDialog.getOpenFileNames(self, tr("co4e.attach_add"))
|
|
for f in files:
|
|
if f and f not in self._attachments:
|
|
self._attachments.append(f)
|
|
self._refresh_attach_list()
|
|
|
|
def _del_attachment(self) -> None:
|
|
"""Gỡ tệp đính kèm đang chọn."""
|
|
row = self.attach_list.currentRow()
|
|
if 0 <= row < len(self._attachments):
|
|
self._attachments.pop(row)
|
|
self._refresh_attach_list()
|
|
|
|
def result_agent(self) -> CustomAgent:
|
|
"""Bản ghi agent dựng từ nội dung đang có trên form.
|
|
|
|
Vai trò luôn viết HOA và không bao giờ rỗng — prompt của bước dùng thẳng
|
|
chuỗi này nên để trống sẽ sinh ra câu cụt.
|
|
"""
|
|
a = self._agent
|
|
a.name = self.name_edit.text().strip() or "Agent"
|
|
a.role = (self.role_edit.text().strip() or "AGENT").upper()
|
|
a.icon = self.icon_edit.currentText().strip()
|
|
a.instructions = self.instructions_edit.toPlainText().strip()
|
|
a.context = self.context_edit.toPlainText().strip()
|
|
a.model = self.model_combo.currentText().strip()
|
|
a.permission_preset = self.perm_combo.currentData() or "inherit"
|
|
a.skills = [self.skills_list.item(i).text()
|
|
for i in range(self.skills_list.count())
|
|
if self.skills_list.item(i).checkState() == Qt.Checked]
|
|
a.attachments = list(self._attachments)
|
|
return a
|
|
|
|
def _load_models(self) -> None:
|
|
"""Nạp danh sách model đang sống vào ô chọn, ở luồng nền."""
|
|
if self.ctx is None:
|
|
return
|
|
from ..core import preview_ai
|
|
from ..core.worker import AgentWorker
|
|
|
|
self.load_btn.setEnabled(False)
|
|
ctx = self.ctx
|
|
|
|
def job(_w):
|
|
"""Chạy nền: hỏi mọi provider danh sách model đang dùng được."""
|
|
return preview_ai.fetch_live_models(ctx)
|
|
|
|
def done(result: dict):
|
|
"""Gộp model của mọi provider vào ô chọn, giữ nguyên thứ đang chọn."""
|
|
self.load_btn.setEnabled(True)
|
|
models = []
|
|
for lst in (result or {}).values():
|
|
models.extend(lst)
|
|
cur = self.model_combo.currentText()
|
|
self.model_combo.blockSignals(True)
|
|
self.model_combo.clear()
|
|
self.model_combo.addItems(sorted(set(models)))
|
|
self.model_combo.setEditText(cur)
|
|
self.model_combo.blockSignals(False)
|
|
|
|
w = AgentWorker(job)
|
|
w.finished_ok.connect(done)
|
|
w.failed.connect(lambda _e: self.load_btn.setEnabled(True))
|
|
self._worker = w
|
|
w.start()
|