feat(tickets): création d'un assistant IA de ticket (backend + frontend)
Ajoute le chat assistant IA attaché à un ticket (#8). Backend Rust : - use cases OpenTicketAssistant/CloseTicketAssistant + tests - politique d'outils par agent (domain/agent_tool_policy) et policy MCP - store de contexte assistant + gabarit default_ticket_assistant.md + tests - events TicketAssistantOpened/Closed - commandes Tauri open_ticket_chat/close_ticket_chat et câblage state/lib/events Frontend : - gateway (ports, adapters ticket + mock, domain) - hook useTicketAssistant + composant TicketAssistantPanel - intégration dans TicketDetail Tests verts : vitest tickets.test.tsx (23), cargo test application::ticket_assistant (1), infrastructure::assistant_context_store (2). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -50,6 +50,20 @@ pub enum DomainEventDto {
|
||||
/// Exit code.
|
||||
code: i32,
|
||||
},
|
||||
/// A ticket assistant chat was opened.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
TicketAssistantOpened {
|
||||
/// Bound ticket reference.
|
||||
issue_ref: String,
|
||||
/// Runtime profile id.
|
||||
profile_id: String,
|
||||
},
|
||||
/// A ticket assistant chat was closed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
TicketAssistantClosed {
|
||||
/// Bound ticket reference.
|
||||
issue_ref: String,
|
||||
},
|
||||
/// An agent's busy/idle state changed (cadrage C4 §4.2). The frontend dims
|
||||
/// "Envoyer" while `busy` is `true`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -551,6 +565,16 @@ impl From<&DomainEvent> for DomainEventDto {
|
||||
agent_id: agent_id.to_string(),
|
||||
code: *code,
|
||||
},
|
||||
DomainEvent::TicketAssistantOpened {
|
||||
issue_ref,
|
||||
profile_id,
|
||||
} => Self::TicketAssistantOpened {
|
||||
issue_ref: issue_ref.to_string(),
|
||||
profile_id: profile_id.to_string(),
|
||||
},
|
||||
DomainEvent::TicketAssistantClosed { issue_ref } => Self::TicketAssistantClosed {
|
||||
issue_ref: issue_ref.to_string(),
|
||||
},
|
||||
DomainEvent::AgentBusyChanged { agent_id, busy } => Self::AgentBusyChanged {
|
||||
agent_id: agent_id.to_string(),
|
||||
busy: *busy,
|
||||
@ -1081,4 +1105,24 @@ mod tests {
|
||||
assert_eq!(json["to"], to.to_string());
|
||||
assert_eq!(json["version"], 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ticket_assistant_events_relay_to_dto_and_wire() {
|
||||
let profile_id = domain::ProfileId::from_uuid(uuid::Uuid::from_u128(9));
|
||||
let opened = DomainEventDto::from(&DomainEvent::TicketAssistantOpened {
|
||||
issue_ref: domain::IssueRef::from(domain::IssueNumber::new(7).unwrap()),
|
||||
profile_id,
|
||||
});
|
||||
let opened = serde_json::to_value(&opened).expect("serialisable");
|
||||
assert_eq!(opened["type"], "ticketAssistantOpened");
|
||||
assert_eq!(opened["issueRef"], "#7");
|
||||
assert_eq!(opened["profileId"], profile_id.to_string());
|
||||
|
||||
let closed = DomainEventDto::from(&DomainEvent::TicketAssistantClosed {
|
||||
issue_ref: domain::IssueRef::from(domain::IssueNumber::new(7).unwrap()),
|
||||
});
|
||||
let closed = serde_json::to_value(&closed).expect("serialisable");
|
||||
assert_eq!(closed["type"], "ticketAssistantClosed");
|
||||
assert_eq!(closed["issueRef"], "#7");
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,6 +166,8 @@ pub fn run() {
|
||||
commands::list_agents,
|
||||
tickets::ticket_create,
|
||||
tickets::ticket_read,
|
||||
tickets::open_ticket_chat,
|
||||
tickets::close_ticket_chat,
|
||||
tickets::ticket_list,
|
||||
tickets::ticket_update,
|
||||
tickets::ticket_read_carnet,
|
||||
|
||||
@ -15,39 +15,40 @@ use application::{
|
||||
AgentResumer, AgentWakeService, AppError, AssignIssueAgent, AssignSkillToAgent,
|
||||
AssignTicketToSprint, AttachLiveAgent, BackgroundCommandArchive, CancelBackgroundTask,
|
||||
ChangeAgentProfile, CheckEmbedderSuggestion, CloseProject, CloseTab, CloseTerminal,
|
||||
ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch, CreateAgentFromTemplate,
|
||||
CreateIssue, CreateLayout, CreateMemory, CreateProject, CreateSkill, CreateSprint,
|
||||
CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteLayout, DeleteMemory, DeleteProfile,
|
||||
DeleteSkill, DeleteSprint, DeleteTemplate, DescribeEmbedderEngines, DetectAgentDrift,
|
||||
DetectProfiles, DismissEmbedderSuggestion, FirstRunState, GetLiveStateLean, GetMemory,
|
||||
GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit, GitGraph,
|
||||
GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn, HealthUseCase,
|
||||
InspectConversation, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents, ListAgentsInput,
|
||||
ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListProfiles, ListProjects,
|
||||
ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry, LiveSessions,
|
||||
LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout, McpRuntime,
|
||||
MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||
OrchestratorService, PermissionProjectorRegistry, ProposeContext, ReadAgentContext,
|
||||
ReadContext, ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMemory, ReadMemoryIndex,
|
||||
ReadProjectContext, ReadSkill, RecallMemory, ReconcileLayouts, ReconcileLiveState,
|
||||
ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles, RenameLayout,
|
||||
RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions, ResolveMemoryLinks,
|
||||
RetryBackgroundTask, RotateConversationLog, SaveEmbedderProfile, SaveProfile,
|
||||
SessionLimitService, SetActiveLayout, SnapshotRunningAgents, SpawnBackgroundCommand,
|
||||
StopLiveAgent, StructuredSessions, SuggestedThisSession, SyncAgentWithTemplate,
|
||||
TerminalSessions, UnassignSkillFromAgent, UnassignTicketFromSprint, UnlinkIssues,
|
||||
UpdateAgentContext, UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet, UpdateLiveState,
|
||||
UpdateMemory, UpdateProjectContext, UpdateProjectPermissions, UpdateSkill, UpdateTemplate,
|
||||
WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
CloseTicketAssistant, ConfigureProfiles, ContextGuardUseCases, CreateAgentFromScratch,
|
||||
CreateAgentFromTemplate, CreateIssue, CreateLayout, CreateMemory, CreateProject, CreateSkill,
|
||||
CreateSprint, CreateTemplate, DeleteAgent, DeleteEmbedderProfile, DeleteLayout, DeleteMemory,
|
||||
DeleteProfile, DeleteSkill, DeleteSprint, DeleteTemplate, DescribeEmbedderEngines,
|
||||
DetectAgentDrift, DetectProfiles, DismissEmbedderSuggestion, FirstRunState, GetLiveStateLean,
|
||||
GetMemory, GetProjectPermissions, GetProjectWorkState, GitBranches, GitCheckout, GitCommit,
|
||||
GitGraph, GitInit, GitLog, GitStage, GitStatus, GitUnstage, HarvestMemoryFromTurn,
|
||||
HealthUseCase, InspectConversation, LaunchAgent, LaunchAgentInput, LinkIssues, ListAgents,
|
||||
ListAgentsInput, ListEmbedderProfiles, ListIssues, ListLayouts, ListMemories, ListProfiles,
|
||||
ListProjects, ListResumableAgents, ListSkills, ListSprints, ListTemplates, LiveAgentRegistry,
|
||||
LiveSessions, LiveStateLeanProvider, LiveStateProvider, LiveStateReadProvider, LoadLayout,
|
||||
McpRuntime, MoveTabToNewWindow, MutateLayout, OnnxModelView, OpenProject, OpenTerminal,
|
||||
OpenTicketAssistant, OrchestratorService, PermissionProjectorRegistry, ProposeContext,
|
||||
ReadAgentContext, ReadContext, ReadConversationPage, ReadIssue, ReadIssueCarnet, ReadMemory,
|
||||
ReadMemoryIndex, ReadProjectContext, ReadSkill, RecallMemory, ReconcileLayouts,
|
||||
ReconcileLiveState, ReconcileLiveStateInput, RecordTurn, RecordTurnProvider, ReferenceProfiles,
|
||||
RenameLayout, RenameSprint, ReorderSprints, ResizeTerminal, ResolveAgentPermissions,
|
||||
ResolveMemoryLinks, RetryBackgroundTask, RotateConversationLog, SaveEmbedderProfile,
|
||||
SaveProfile, SessionLimitService, SetActiveLayout, SnapshotRunningAgents,
|
||||
SpawnBackgroundCommand, StopLiveAgent, StructuredSessions, SuggestedThisSession,
|
||||
SyncAgentWithTemplate, TerminalSessions, UnassignSkillFromAgent, UnassignTicketFromSprint,
|
||||
UnlinkIssues, UpdateAgentContext, UpdateAgentPermissions, UpdateIssue, UpdateIssueCarnet,
|
||||
UpdateLiveState, UpdateMemory, UpdateProjectContext, UpdateProjectPermissions, UpdateSkill,
|
||||
UpdateTemplate, WakeSessionProvider, WriteMemory, WriteToTerminal, AGENT_MEMORY_RECALL_BUDGET,
|
||||
};
|
||||
use async_trait::async_trait;
|
||||
use domain::ports::{
|
||||
AgentContextStore, AgentRuntime, AgentSession, AgentSessionFactory, AgentWakePort,
|
||||
BackgroundTaskPortError, BackgroundTaskRunner, BackgroundTaskStore, Clock, Embedder,
|
||||
EmbedderEnvInspector, EmbedderProfileStore, EmbedderPromptStore, EventBus, FileSystem, GitPort,
|
||||
IdGenerator, IssueNumberAllocator, IssueStore, MemoryRecall, MemoryStore, PermissionStore,
|
||||
ProcessSpawner, ProfileStore, ProjectStore, PtyPort, ScheduledTask, Scheduler, SkillStore,
|
||||
SprintStore, TemplateStore, WakeError, WakeReason,
|
||||
AgentContextStore, AgentRuntime, AgentSession, AgentSessionFactory, AgentToolPolicyStore,
|
||||
AgentWakePort, AssistantContextProvider, BackgroundTaskPortError, BackgroundTaskRunner,
|
||||
BackgroundTaskStore, Clock, Embedder, EmbedderEnvInspector, EmbedderProfileStore,
|
||||
EmbedderPromptStore, EventBus, FileSystem, GitPort, IdGenerator, IssueNumberAllocator,
|
||||
IssueStore, MemoryRecall, MemoryStore, PermissionStore, ProcessSpawner, ProfileStore,
|
||||
ProjectStore, PtyPort, ScheduledTask, Scheduler, SkillStore, SprintStore, TemplateStore,
|
||||
WakeError, WakeReason,
|
||||
};
|
||||
use domain::profile::{
|
||||
AgentProfile, ContextInjection, McpConfigStrategy, McpTransport, StructuredAdapter,
|
||||
@ -65,16 +66,16 @@ use infrastructure::{
|
||||
embedder_from_profile, AdaptiveMemoryRecall, BackgroundCompletionSink,
|
||||
BackgroundTaskReadyToDeliver, ClaudePermissionProjector, ClaudeTranscriptInspector,
|
||||
CliAgentRuntime, CodexPermissionProjector, CommandBackgroundRunner, EmbedderEnvProbe,
|
||||
FsBackgroundTaskStore, FsConversationLog, FsEmbedderProfileStore, FsEmbedderPromptStore,
|
||||
FsHandoffStore, FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore, FsMemoryStore,
|
||||
FsOrchestratorWatcher, FsPermissionStore, FsProfileStore, FsProjectStore,
|
||||
FsAssistantContextStore, FsBackgroundTaskStore, FsConversationLog, FsEmbedderProfileStore,
|
||||
FsEmbedderPromptStore, FsHandoffStore, FsIssueNumberAllocator, FsIssueStore, FsLiveStateStore,
|
||||
FsMemoryStore, FsOrchestratorWatcher, FsPermissionStore, FsProfileStore, FsProjectStore,
|
||||
FsProviderSessionStore, FsSkillStore, FsSprintStore, FsTemplateStore, Git2Repository,
|
||||
HeuristicHandoffSummarizer, IdeaiContextStore, InMemoryConversationRegistry, InMemoryMailbox,
|
||||
LocalFileSystem, LocalProcessSpawner, McpServer, MediatedInbox, NaiveMemoryRecall,
|
||||
OrchestratorWatchHandle, PortablePtyAdapter, RwFileGuard, StructuredSessionFactory,
|
||||
SystemClock, SystemMillisClock, TicketToolProvider, TokioBroadcastEventBus, TokioScheduler,
|
||||
UuidGenerator, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL, ONNX_CACHE_SUBDIR,
|
||||
RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||
ToolPolicyRegistry, UuidGenerator, VectorMemoryRecall, DEFAULT_OLLAMA_BASE_URL,
|
||||
ONNX_CACHE_SUBDIR, RECOMMENDED_ONNX_MODELS, VECTOR_HTTP_ENABLED, VECTOR_ONNX_ENABLED,
|
||||
};
|
||||
|
||||
use crate::chat::ChatBridge;
|
||||
@ -822,6 +823,12 @@ pub struct AppState {
|
||||
pub unassign_ticket_from_sprint: Arc<UnassignTicketFromSprint>,
|
||||
/// MCP provider for the public `idea_ticket_*` tools.
|
||||
pub(crate) ticket_tool_provider: Arc<dyn TicketToolProvider>,
|
||||
/// Open an ephemeral AI assistant chat bound to one ticket.
|
||||
pub open_ticket_assistant: Arc<OpenTicketAssistant>,
|
||||
/// Close an ephemeral AI assistant chat bound to one ticket.
|
||||
pub close_ticket_assistant: Arc<CloseTicketAssistant>,
|
||||
/// Shared MCP tool policy registry used by ticket assistant sessions.
|
||||
pub tool_policy_registry: Arc<ToolPolicyRegistry>,
|
||||
/// Generic PTY↔Channel bridge registry (consumed by L3).
|
||||
pub pty_bridge: Arc<PtyBridge>,
|
||||
/// Registre des sessions structurées (IA / cellules chat, §17.5). Partagé avec
|
||||
@ -1279,6 +1286,26 @@ impl AppState {
|
||||
unlink: Arc::clone(&unlink_issues),
|
||||
list_sprints: Arc::clone(&list_sprints),
|
||||
});
|
||||
let tool_policy_registry = Arc::new(ToolPolicyRegistry::new());
|
||||
let tool_policy_store = Arc::clone(&tool_policy_registry) as Arc<dyn AgentToolPolicyStore>;
|
||||
let assistant_context_provider = Arc::new(FsAssistantContextStore::new(
|
||||
Arc::clone(&fs_port),
|
||||
app_data_dir.to_string_lossy().into_owned(),
|
||||
)) as Arc<dyn AssistantContextProvider>;
|
||||
let open_ticket_assistant = Arc::new(OpenTicketAssistant::new(
|
||||
Arc::clone(&issue_store_port),
|
||||
Arc::clone(&profile_store_port),
|
||||
assistant_context_provider,
|
||||
Arc::clone(&session_factory),
|
||||
Arc::clone(&structured_sessions),
|
||||
Arc::clone(&tool_policy_store),
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
let close_ticket_assistant = Arc::new(CloseTicketAssistant::new(
|
||||
Arc::clone(&structured_sessions),
|
||||
tool_policy_store,
|
||||
Arc::clone(&events_port),
|
||||
));
|
||||
|
||||
// --- Project permissions (LP1) ---
|
||||
let permission_store = Arc::new(FsPermissionStore::new(Arc::clone(&fs_port)));
|
||||
@ -2178,6 +2205,9 @@ impl AppState {
|
||||
assign_ticket_to_sprint,
|
||||
unassign_ticket_from_sprint,
|
||||
ticket_tool_provider,
|
||||
open_ticket_assistant,
|
||||
close_ticket_assistant,
|
||||
tool_policy_registry,
|
||||
pty_bridge,
|
||||
structured_sessions,
|
||||
chat_bridge,
|
||||
@ -2337,7 +2367,8 @@ impl AppState {
|
||||
McpServer::new(Arc::clone(&self.orchestrator_service), project.clone())
|
||||
.with_events(events)
|
||||
.with_ready_sink(ready_sink)
|
||||
.with_ticket_tools(Arc::clone(&self.ticket_tool_provider)),
|
||||
.with_ticket_tools(Arc::clone(&self.ticket_tool_provider))
|
||||
.with_tool_policies(Arc::clone(&self.tool_policy_registry)),
|
||||
endpoint,
|
||||
listener,
|
||||
project_id,
|
||||
|
||||
@ -14,15 +14,16 @@ use tauri::State;
|
||||
use uuid::Uuid;
|
||||
|
||||
use application::{
|
||||
AppError, AssignIssueAgentInput, AssignTicketToSprintInput, CreateIssueInput,
|
||||
CreateSprintInput, DeleteSprintInput, LinkIssuesInput, ListIssuesInput, ListSprintsInput,
|
||||
OpenProjectInput, ReadIssueCarnetInput, ReadIssueInput, RenameSprintInput, ReorderSprintsInput,
|
||||
UnassignTicketFromSprintInput, UnlinkIssuesInput, UpdateIssueCarnetInput, UpdateIssueInput,
|
||||
AppError, AssignIssueAgentInput, AssignTicketToSprintInput, CloseTicketAssistantInput,
|
||||
CreateIssueInput, CreateSprintInput, DeleteSprintInput, LinkIssuesInput, ListIssuesInput,
|
||||
ListSprintsInput, OpenProjectInput, OpenTicketAssistantInput, ReadIssueCarnetInput,
|
||||
ReadIssueInput, RenameSprintInput, ReorderSprintsInput, UnassignTicketFromSprintInput,
|
||||
UnlinkIssuesInput, UpdateIssueCarnetInput, UpdateIssueInput,
|
||||
};
|
||||
use domain::{
|
||||
AgentId, AgentIssueRole, Issue, IssueActor, IssueCarnet, IssueIndexEntry, IssueLink,
|
||||
IssueLinkKind, IssueListFilter, IssuePriority, IssueRef, IssueStatus, IssueVersion, Project,
|
||||
ProjectId, SprintId, SprintStatus, SprintVersion,
|
||||
IssueLinkKind, IssueListFilter, IssuePriority, IssueRef, IssueStatus, IssueVersion, ProfileId,
|
||||
Project, ProjectId, SprintId, SprintStatus, SprintVersion,
|
||||
};
|
||||
use infrastructure::{TicketToolError, TicketToolProvider};
|
||||
|
||||
@ -106,6 +107,29 @@ pub struct SprintListDto {
|
||||
pub items: Vec<SprintDto>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct OpenTicketChatRequestDto {
|
||||
pub project_id: String,
|
||||
pub issue_ref: String,
|
||||
pub profile_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CloseTicketChatRequestDto {
|
||||
pub project_id: String,
|
||||
pub issue_ref: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct TicketChatDto {
|
||||
pub session_id: String,
|
||||
pub requester: String,
|
||||
pub issue_ref: String,
|
||||
}
|
||||
|
||||
/// Public link DTO.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@ -588,6 +612,43 @@ pub async fn ticket_read(
|
||||
Ok(TicketDto::from_issue(issue, carnet))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn open_ticket_chat(
|
||||
request: OpenTicketChatRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<TicketChatDto, ErrorDto> {
|
||||
let project = resolve_project(&state, &request.project_id).await?;
|
||||
let output = state
|
||||
.open_ticket_assistant
|
||||
.execute(OpenTicketAssistantInput {
|
||||
project,
|
||||
issue_ref: parse_ref_dto(&request.issue_ref)?,
|
||||
profile_id: parse_profile_id_dto(&request.profile_id)?,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)?;
|
||||
Ok(TicketChatDto {
|
||||
session_id: output.session_id.to_string(),
|
||||
requester: output.requester,
|
||||
issue_ref: output.issue_ref.to_string(),
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn close_ticket_chat(
|
||||
request: CloseTicketChatRequestDto,
|
||||
state: State<'_, AppState>,
|
||||
) -> Result<(), ErrorDto> {
|
||||
let _project = resolve_project(&state, &request.project_id).await?;
|
||||
state
|
||||
.close_ticket_assistant
|
||||
.execute(CloseTicketAssistantInput {
|
||||
issue_ref: parse_ref_dto(&request.issue_ref)?,
|
||||
})
|
||||
.await
|
||||
.map_err(ErrorDto::from)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn ticket_list(
|
||||
request: TicketListRequestDto,
|
||||
@ -1234,6 +1295,12 @@ fn parse_agent_id_dto(raw: &str) -> Result<AgentId, ErrorDto> {
|
||||
.map_err(|e| ErrorDto::invalid(format!("invalid agent id: {e}")))
|
||||
}
|
||||
|
||||
fn parse_profile_id_dto(raw: &str) -> Result<ProfileId, ErrorDto> {
|
||||
Uuid::parse_str(raw)
|
||||
.map(ProfileId::from_uuid)
|
||||
.map_err(|e| ErrorDto::invalid(format!("invalid profile id: {e}")))
|
||||
}
|
||||
|
||||
fn parse_sprint_id_dto(raw: &str) -> Result<SprintId, ErrorDto> {
|
||||
Uuid::parse_str(raw)
|
||||
.map(SprintId::from_uuid)
|
||||
|
||||
Reference in New Issue
Block a user