1use std::collections::{HashMap, HashSet};
27use std::sync::Arc;
28
29use bevy_ecs::lifecycle::HookContext;
30use bevy_ecs::prelude::*;
31use bevy_ecs::system::SystemParam;
32use bevy_ecs::world::DeferredWorld;
33use bevy_reflect::prelude::*;
34use rig_core::catalog::ModelSpec;
35use rig_core::completion::Message;
36use rig_core::conversation::{Aside, AsideId, Author, AuthorKind, Entry, Halt};
37use rig_core::effect::{EffectKind, HandlerKey};
38use rig_core::error::{ErrorKind, ErrorReport};
39use rig_core::message::UserContent;
40use serde::{Deserialize, Serialize};
41use web_time::SystemTime;
42
43use super::agent::{
44 ActiveTurn, Agent, CallOf, Conversation, EffectParent, Notice, TurnOf, TurnOutcome,
45};
46use super::model::Connection;
47#[cfg(doc)]
48use super::stage::Stage;
49use super::stage::{
50 AgentWork, Decision, Effect, ForAgent, Holds, Outcome, ReadyToStart, Settled, Started,
51 SupersededBy, moved,
52};
53use super::tasks::Wake;
54use super::turn::{Exiting, TurnCall, spawn_turn_call, take_outcome};
55
56#[derive(Reflect, Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum DeliveryMode {
62 #[default]
64 Steer,
65 Queue,
68 #[serde(rename = "note")]
73 Inform,
74}
75
76#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
80#[reflect(opaque, Clone, Debug)]
81pub struct Attachment {
82 pub label: String,
84 pub content: UserContent,
86}
87
88#[derive(Reflect, Clone, Debug, Serialize, Deserialize)]
90#[reflect(Clone, Debug)]
91#[serde(rename_all = "snake_case")]
92pub enum MessagePart {
93 Text(String),
95 Attachment(Attachment),
97}
98
99#[derive(Component, Reflect, Clone, Debug, Serialize, Deserialize)]
126#[component(on_add = sent)]
127#[reflect(Component, Clone, Debug)]
128#[require(Effect, Name = Name::new("message"))]
129pub struct Incoming {
130 pub parts: Vec<MessagePart>,
132 #[reflect(ignore)]
134 pub author: Author,
135 pub mode: DeliveryMode,
137 pub literal: bool,
140}
141
142impl Incoming {
143 pub fn new(text: impl Into<String>, mode: DeliveryMode) -> Self {
146 Self {
147 parts: vec![MessagePart::Text(text.into())],
148 author: Author::user(),
149 mode,
150 literal: false,
151 }
152 }
153
154 pub fn text(&self) -> &str {
156 texts(&self.parts).next().unwrap_or_default()
157 }
158
159 pub fn text_mut(&mut self) -> &mut String {
161 let first = self
162 .parts
163 .iter()
164 .position(|part| matches!(part, MessagePart::Text(_)));
165 let (at, part) = match first {
166 Some(at) => (at, self.parts.remove(at)),
167 None => (self.parts.len(), MessagePart::Text(String::new())),
168 };
169 match self.parts.insert_mut(at, part) {
170 MessagePart::Text(text) => text,
171 MessagePart::Attachment(attachment) => &mut attachment.label,
173 }
174 }
175
176 fn is_empty(&self) -> bool {
178 self.parts.iter().all(|part| match part {
179 MessagePart::Text(text) => text.trim().is_empty(),
180 MessagePart::Attachment(_) => false,
181 })
182 }
183
184 #[must_use]
186 pub fn with_author(self, author: Author) -> Self {
187 Self { author, ..self }
188 }
189
190 #[must_use]
192 pub fn literal(self) -> Self {
193 Self {
194 literal: true,
195 ..self
196 }
197 }
198
199 pub fn command(&self) -> Option<&str> {
203 let user = self.author.kind == AuthorKind::User && !self.literal;
204 user.then(|| self.text().trim_start().strip_prefix('/'))
205 .flatten()
206 }
207}
208
209#[derive(EntityEvent, Reflect, Clone, Debug)]
214#[reflect(Event, Clone, Debug)]
215pub struct Deliver {
216 pub entity: Entity,
218 pub message: Incoming,
220}
221
222impl Deliver {
223 pub fn new(agent: Entity, text: impl Into<String>, mode: DeliveryMode) -> Self {
226 Self {
227 entity: agent,
228 message: Incoming::new(text, mode),
229 }
230 }
231
232 #[must_use]
234 pub fn with_author(self, author: Author) -> Self {
235 Self {
236 message: self.message.with_author(author),
237 ..self
238 }
239 }
240
241 #[must_use]
243 pub fn literal(self) -> Self {
244 Self {
245 message: self.message.literal(),
246 ..self
247 }
248 }
249}
250
251#[derive(Message, Clone, Debug)]
255pub struct Recalled {
256 pub agent: Entity,
258 pub text: String,
260 pub why: Option<String>,
262}
263
264#[derive(Component, Reflect, Clone, Copy, Debug)]
271#[reflect(Component, Clone, Debug)]
272pub struct DeliveredIn(pub Entity);
273
274#[derive(Component, Clone, Debug)]
284#[component(on_insert = moved::<Unsent>)]
285pub struct Unsent {
286 pub ended: Option<TurnOutcome>,
288}
289
290const INBOX_MARK: &str = "rig.inbox";
293
294#[derive(Component, Clone, Copy, Debug)]
296pub(crate) struct InboxMark(AsideId);
297
298fn mark(conversation: &mut Conversation, message: &Incoming) -> InboxMark {
301 let after = conversation.entries().next_back().map(|entry| entry.id);
302 let value = serde_json::to_value(message).unwrap_or_default();
303 let aside = Aside::new(INBOX_MARK, Author::rig(), value).after(after);
304 InboxMark(conversation.add_aside(aside))
305}
306
307pub(crate) fn kept(conversation: &Conversation) -> Vec<(InboxMark, Incoming)> {
309 let marks = conversation
310 .asides()
311 .filter(|(_, aside)| aside.kind == INBOX_MARK);
312 let read = |aside: &Aside| Incoming::deserialize(&aside.value).ok();
313 marks
314 .filter_map(|(id, aside)| Some((InboxMark(id), read(aside)?)))
315 .collect()
316}
317
318pub(crate) fn unmark(
320 answered: On<Insert<Outcome>>,
321 marked: Query<(&InboxMark, &ForAgent)>,
322 mut agents: Query<&mut Conversation>,
323) {
324 if let Ok((&InboxMark(id), &ForAgent(agent))) = marked.get(answered.entity)
325 && let Ok(mut conversation) = agents.get_mut(agent)
326 {
327 conversation.remove_aside(id);
328 }
329}
330
331pub(crate) fn on_deliver(
333 deliver: On<Deliver>,
334 agents: Query<(), With<Agent>>,
335 mut commands: Commands,
336) {
337 if agents.contains(deliver.entity) {
338 commands.spawn((deliver.message.clone(), ForAgent(deliver.entity)));
339 }
340}
341
342fn sent(mut world: DeferredWorld, context: HookContext) {
346 let entity = context.entity;
347 let parent = world
348 .get::<ForAgent>(entity)
349 .and_then(|agent| world.get::<EffectParent>(agent.0))
350 .map(|parent| parent.0);
351 if let Some(parent) = parent
352 && let Some(mut effect) = world.get_mut::<Effect>(entity)
353 && effect.parent.is_none()
354 {
355 effect.parent = Some(parent);
356 }
357 if let Some(wake) = world.get_resource::<Wake>() {
358 wake.wake();
359 }
360}
361
362pub(crate) fn accept(
368 ready: Query<(Entity, &Effect, &Incoming, Option<&ForAgent>), ReadyToStart>,
369 mut agents: Query<(&mut Conversation, Option<&Connection>, Has<ActiveTurn>), With<Agent>>,
370 mut commands: Commands,
371 mut notices: MessageWriter<Notice>,
372) {
373 let mut ready: Vec<_> = ready.iter().collect();
374 ready.sort_unstable_by_key(|(_, effect, ..)| effect.id());
375 let at = SystemTime::now();
376 let mut starting = HashMap::new();
378 for (entity, effect, message, agent) in ready {
379 let agent = agent.map(|agent| agent.0);
380 let Some((agent, (mut conversation, connection, busy))) =
381 agent.and_then(|agent| Some((agent, agents.get_mut(agent).ok()?)))
382 else {
383 let why = ErrorReport::new(ErrorKind::Internal, "a message needs an agent");
384 commands.entity(entity).try_insert_if_new(Outcome(Err(why)));
385 continue;
386 };
387 if message.is_empty() {
388 commands
389 .entity(entity)
390 .try_insert_if_new(Outcome::custom("empty"));
391 continue;
392 }
393 let joins = starting.get(&agent).copied();
394 if busy && joins.is_none() {
395 let mark = mark(&mut conversation, message);
396 commands.entity(entity).try_insert((Started { at }, mark));
397 continue;
398 }
399 let spec = connection.map(|connection| &*connection.spec);
400 let turn = match joins {
402 None if message.mode == DeliveryMode::Inform => None,
403 None => Some(commands.spawn(TurnOf(agent)).id()),
404 joins => joins,
405 };
406 commands.entity(entity).try_insert(Started { at });
407 let conversation = &mut *conversation;
409 let into = (turn, &mut commands);
410 deliver(
411 agent,
412 spec,
413 [(entity, message)],
414 conversation,
415 &mut notices,
416 into,
417 );
418 match (turn, joins) {
419 (None, _) => {
420 conversation.halt(Halt::Kept);
421 }
422 (Some(turn), None) => {
423 starting.insert(agent, turn);
424 commands.spawn((
426 TurnCall,
427 CallOf(turn),
428 ForAgent(agent),
429 Effect::with_parent_id(Some(effect.id())),
430 ));
431 }
432 (Some(_), Some(_)) => {}
433 }
434 }
435}
436
437pub(crate) fn carry_on_accepted(
442 accepted: Query<(&Incoming, &ForAgent), (Added<Started>, Without<Outcome>)>,
443 mut agents: Query<(&mut Conversation, Option<&Connection>, &ActiveTurn)>,
444 turns: Query<(&TurnOutcome, Option<&Holds>)>,
445 inboxes: Inboxes,
446 mut commands: Commands,
447 mut notices: MessageWriter<Notice>,
448) {
449 let mut carried = HashSet::new();
450 for (message, &ForAgent(agent)) in &accepted {
451 if message.mode == DeliveryMode::Inform || !carried.insert(agent) {
452 continue;
453 }
454 let Ok((mut conversation, connection, active)) = agents.get_mut(agent) else {
455 continue;
456 };
457 let turn = active.turn();
458 let Ok((TurnOutcome::Answered(_), holds)) = turns.get(turn) else {
459 continue;
460 };
461 let Some(next) = carry_on(inboxes.of(agent)) else {
462 continue;
463 };
464 let spec = connection.map(|connection| &*connection.spec);
465 let conversation = &mut *conversation;
466 let into = (Some(turn), &mut commands);
467 deliver(agent, spec, next, conversation, &mut notices, into);
468 take_outcome(&mut commands, turn, holds);
469 spawn_turn_call(&mut commands, turn, agent);
470 }
471}
472
473#[derive(SystemParam)]
477pub(crate) struct Inboxes<'w, 's> {
478 work: Query<'w, 's, &'static AgentWork>,
479 accepted: Query<
480 'w,
481 's,
482 (
483 Entity,
484 &'static Effect,
485 &'static Incoming,
486 &'static Decision,
487 ),
488 (With<Started>, Without<Outcome>, Without<Settled>),
489 >,
490 on_the_way: Query<
493 'w,
494 's,
495 (&'static Incoming, &'static Decision),
496 (
497 Without<Started>,
498 Without<Outcome>,
499 Without<Settled>,
500 Without<SupersededBy>,
501 ),
502 >,
503}
504
505impl Inboxes<'_, '_> {
506 pub(crate) fn of(&self, agent: Entity) -> Vec<(Entity, &Incoming)> {
508 let work = self.work.get(agent).into_iter().flat_map(AgentWork::iter);
509 let accepted = self.accepted.iter_many(work).flatten();
510 let mut waiting: Vec<_> = accepted
511 .filter(|(.., decision)| !decision.is_decided())
512 .collect();
513 waiting.sort_unstable_by_key(|(_, effect, ..)| effect.id());
514 waiting
515 .into_iter()
516 .map(|(entity, _, message, _)| (entity, message))
517 .collect()
518 }
519
520 pub(crate) fn coming(&self, agent: Entity) -> bool {
523 let work = self.work.get(agent).into_iter().flat_map(AgentWork::iter);
524 self.on_the_way
525 .iter_many(work)
526 .flatten()
527 .any(|(message, decision)| {
528 message.mode != DeliveryMode::Inform && !decision.is_decided()
529 })
530 }
531}
532
533pub(crate) fn carry_on(waiting: Vec<(Entity, &Incoming)>) -> Option<Vec<(Entity, &Incoming)>> {
537 let steering = waiting
538 .iter()
539 .any(|(_, message)| message.mode == DeliveryMode::Steer);
540 let carries = match steering {
541 true => DeliveryMode::Steer,
542 false => DeliveryMode::Queue,
543 };
544 let next: Vec<_> = waiting
545 .into_iter()
546 .filter(|(_, message)| {
547 matches!(message.mode, DeliveryMode::Inform) || message.mode == carries
548 })
549 .collect();
550 next.iter()
551 .any(|(_, message)| message.mode == carries)
552 .then_some(next)
553}
554
555pub(crate) fn unsent_on_turn_end(
558 end: On<Remove<TurnOf>>,
559 turns: Query<(&TurnOf, Option<&TurnOutcome>)>,
560 inboxes: Inboxes,
561 exiting: Option<Res<Exiting>>,
562 mut commands: Commands,
563) {
564 let Ok((&TurnOf(agent), ended)) = turns.get(end.entity) else {
565 return;
566 };
567 if exiting.is_some() {
568 return;
569 }
570 for (message, _) in inboxes.of(agent) {
571 let ended = ended.cloned();
572 commands.entity(message).try_insert(Unsent { ended });
573 }
574}
575
576pub(crate) fn settle_unsent(
583 unsent: Query<
584 (Entity, &Effect, &Incoming, &ForAgent, &Decision),
585 (With<Unsent>, Without<Outcome>, Without<Holds>),
586 >,
587 mut agents: Query<(&mut Conversation, Option<&Connection>, Has<ActiveTurn>)>,
588 mut commands: Commands,
589 mut recalled: MessageWriter<Recalled>,
590 mut notices: MessageWriter<Notice>,
591) {
592 let mut unsent: Vec<_> = unsent.iter().collect();
593 unsent.sort_unstable_by_key(|(_, effect, ..)| effect.id());
594 let mut typed: HashMap<Entity, Vec<&str>> = HashMap::new();
595 for (entity, _, message, &ForAgent(agent), decision) in unsent {
596 let Ok((mut conversation, connection, busy)) = agents.get_mut(agent) else {
597 continue;
598 };
599 if decision.is_decided() {
600 continue;
601 }
602 if busy {
603 commands.entity(entity).try_remove::<Unsent>();
604 } else if message.author.kind == AuthorKind::User {
605 commands
606 .entity(entity)
607 .try_insert_if_new(Outcome::custom("recalled"));
608 typed.entry(agent).or_default().push(message.text().trim());
609 } else {
610 let spec = connection.map(|connection| &*connection.spec);
611 let conversation = &mut *conversation;
612 let into = (None, &mut commands);
613 deliver(
614 agent,
615 spec,
616 [(entity, message)],
617 conversation,
618 &mut notices,
619 into,
620 );
621 conversation.halt(Halt::Kept);
622 }
623 }
624 for (agent, typed) in typed {
625 recalled.write(Recalled {
626 agent,
627 text: typed.join("\n\n"),
628 why: None,
629 });
630 }
631}
632
633#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
636pub struct MessageRecord {
637 pub text: String,
639 pub author: Author,
641 pub mode: DeliveryMode,
643}
644
645impl MessageRecord {
646 pub const KIND: &str = "message";
648
649 pub fn read(kind: &EffectKind) -> Option<Self> {
651 match kind {
652 EffectKind::Custom { kind, payload } if &**kind == Self::KIND => {
653 serde_json::from_value(payload.clone()).ok()
654 }
655 _ => None,
656 }
657 }
658}
659
660pub(crate) fn message_kind(message: &Incoming) -> (HandlerKey, EffectKind) {
662 let record = MessageRecord {
663 text: texts(&message.parts).collect::<Vec<_>>().join("\n"),
664 author: message.author.clone(),
665 mode: message.mode,
666 };
667 let kind = EffectKind::Custom {
668 kind: Arc::from(MessageRecord::KIND),
669 payload: serde_json::to_value(record).unwrap_or_default(),
670 };
671 (HandlerKey::new(MessageRecord::KIND), kind)
672}
673
674pub(crate) fn deliver<'a>(
678 agent: Entity,
679 spec: Option<&ModelSpec>,
680 messages: impl IntoIterator<Item = (Entity, &'a Incoming)>,
681 conversation: &mut Conversation,
682 notices: &mut MessageWriter<Notice>,
683 (into, commands): (Option<Entity>, &mut Commands),
684) {
685 for (entity, message) in messages {
686 push_messages(agent, spec, [message], conversation, notices);
687 let mut delivered = commands.entity(entity);
688 delivered.try_insert_if_new(Outcome::custom("delivered"));
689 if let Some(turn) = into {
690 delivered.try_insert(DeliveredIn(turn));
691 }
692 }
693}
694
695fn texts(parts: &[MessagePart]) -> impl Iterator<Item = &str> {
697 parts.iter().filter_map(|part| match part {
698 MessagePart::Text(text) => Some(text.as_str()),
699 MessagePart::Attachment(_) => None,
700 })
701}
702
703pub(crate) fn push_messages<'a>(
710 agent: Entity,
711 spec: Option<&ModelSpec>,
712 messages: impl IntoIterator<Item = &'a Incoming>,
713 conversation: &mut Conversation,
714 notices: &mut MessageWriter<Notice>,
715) {
716 for message in messages {
717 let mut header = message.author.header();
718 let mut content = Vec::with_capacity(message.parts.len());
719 let mut refused = Vec::new();
720 for part in &message.parts {
721 let attachment = match part {
722 MessagePart::Text(text) => {
723 let text = text.trim();
724 let text = match header.take() {
725 Some(header) if text.is_empty() => header,
726 Some(header) => format!("{header}\n{text}"),
727 None => text.to_owned(),
728 };
729 if !text.is_empty() {
730 content.push(UserContent::text(text));
731 }
732 continue;
733 }
734 MessagePart::Attachment(attachment) => attachment,
735 };
736 let Attachment {
737 label,
738 content: item,
739 } = attachment;
740 let why = match (item, spec) {
741 (UserContent::Image(_), Some(spec)) if !spec.input.image => Some(format!(
742 "{} does not read images, so {label} is sent as its name only.",
743 spec.display_name
744 )),
745 (UserContent::Image(_), None) => Some(format!(
746 "No model is connected, so {label} is sent as its name only."
747 )),
748 _ => None,
749 };
750 match why {
751 Some(note) => {
752 notices.write(Notice::info(agent, note));
753 refused.push(label.as_str());
754 }
755 None => content.push(item.clone()),
756 }
757 }
758 if content.is_empty() {
761 content.extend(refused.into_iter().map(UserContent::text));
762 }
763 if let Some(header) = header {
764 content.insert(0, UserContent::text(header));
765 }
766 let entry = Entry::new(Message::User { content });
767 let entry = match message.author == Author::user() {
768 true => entry,
769 false => entry.by(message.author.clone()),
770 };
771 conversation.push(entry);
772 }
773}
774
775#[cfg(test)]
776mod tests;