diff --git a/data/card_translation_dictionary.csv b/data/card_translation_dictionary.csv
index 00daed4..e3022bf 100644
--- a/data/card_translation_dictionary.csv
+++ b/data/card_translation_dictionary.csv
@@ -1,4 +1,7 @@
id,category,english,localized,inspected
+-lUSHwHPs0,action,Drew,그렸어요,False
+qn-eVMVboh,action,Met,만났어요,False
+T-w6AqR2WI,action,Wrote,썼어요,False
AtpX8pHl4M,action,after school,학교 끝나고,False
Z7PKuepeFz,action,apologize,사과해요,False
9xqfhNC0iY,action,argue,논해요,False
@@ -7,11 +10,13 @@ rFP_f6kugO,action,ask,물어봐요,False
S5ybpAoUkJ,action,at lunch,점심 때,False
C981hmNoSM,action,ate,먹었어요,True
PcgcuBvG2g,action,attacked,공격했어요,False
+WuY3NIdVc7,action,bored,지루해요,False
J3qH5TLi_n,action,bring,가져가요,True
QYBk1Wh9mw,action,built,지었어요,True
Bir4yNufTz,action,buy,사요,True
9ApkXNWhGk,action,chat,잡담해요,True
Z6-RUtuXwp,action,choose,선택해요,True
+Y8B-lSXI_c,action,climb,올라가요,False
r35xvR3VnP,action,climbed,올라갔어요,True
f7x6vYZGZH,action,color,색칠해요,True
eSlLLkKkpf,action,comfort,편안해요,False
@@ -21,6 +26,7 @@ c9nrRATbZC,action,create,만들어요,True
3xqmUR6WRo,action,created,만들었어요,True
ZM7wuj5mEf,action,cry,울어요,False
n-Tf8mfwDt,action,dance,춤춰요,True
+La-iP5y-_Y,action,describe,설명해요,False
O1Z_dMAq4X,action,did,했어요,True
qCj-Koppsp,action,discuss,토의해요,True
DWKV6w20jL,action,dislike,싫어요,True
@@ -35,6 +41,7 @@ qFMNNFSCX5,action,eat,먹어요,True
H67n9i7nOg,action,eating,먹어요,True
voBXCU3PyM,action,enjoy,즐겨요,True
bFnGzqF9Yj,action,enjoyed,즐겼어요,True
+eABl8ClFz9,action,excited,신나요,False
eM3DY40FFi,action,explain,설명해요,True
lnHBypcdND,action,explore,탐험해요,False
a3KelxlGJh,action,feel,느껴요,True
@@ -48,6 +55,7 @@ dcBKqBvDJT,action,forgive,용서해요,True
FG6t-MIGTa,action,forgot,잊어버렸어요,False
-urVCbaa_L,action,go,가요,True
O6u0XAv7Vm,action,had,했어요,True
+5y7kANN0Oi,action,happy,행복해요,False
IIfEakpYmR,action,have,가져요,True
SQOjY1hhrK,action,help,도와줘요,True
LHl8HjriW6,action,helped,도와주었어요,True
@@ -75,6 +83,7 @@ FKx7TtcCcr,action,listened,들었어요,True
siwWKm9BV8,action,look,보아요,True
QHErJpKn02,action,lost,잃어버렸어요,False
6aCL61BwoT,action,love,사랑해요,True
+K6c6-F2e8w,action,made,만들었어요,False
eFapnHTSZ-,action,make,만들어요,True
CehJbDpG_e,action,meet,만나요,True
D5Pq-2-sTX,action,met,만났어요,True
@@ -100,6 +109,7 @@ RkGMYue-3T,action,sang,노래했어요,True
SvdWDngshX,action,saw,봤어요,True
CmuDetIOQk,action,say,말해요,False
0vELrai42a,action,scare,무서워요,False
+WdrrGIWji7,action,scared,무서워요,False
WKb3RbrCmU,action,see,보아요,True
1N_POFEXle,action,share,함께 나누어요,True
AvJsfa28_0,action,share with,공유해요,False
@@ -132,12 +142,14 @@ ssoybtBkSf,action,tease,놀려요,False
SVk91PDyct,action,tell,이야기해요,True
eQZL7MHvE9,action,threw,던졌어요,True
7iZI-C4C66,action,throw,던져요,True
+5M-KuPT-LW,action,tired,피곤해요,False
nLu2kQrCgk,action,try,시도해요,True
hpsevNf_wR,action,understand,이해해요,True
O68F6a00VU,action,upset,화나요,False
gtjJfAAdum,action,use,사용해요,True
3w7mNezvPA,action,visit,방문해요,True
EWpMoIaJI9,action,want,원해요,True
+6luWwXtp1D,action,wanted,원했어요,False
AOIFIQ4lZv,action,was boring,지루했어요,False
OECWt0H5PH,action,was fun,즐거웠어요,False
iLeKwAROSy,action,was hard,어려웠어요,False
@@ -168,6 +180,7 @@ yvtJ2-R-Dd,emotion,concentrated,집중해요,True
XNDF0_wSwd,emotion,content,만족해요,True
mh-UBb8kM7,emotion,curious,궁금해요,True
wrcjq2faU1,emotion,defiant,저항해요,False
+A3VbaDJAAi,emotion,delicious,맛있어요,False
91Ex6R4Ws0,emotion,difficult,어려워요,True
A3qnncaY1q,emotion,disappointed,실망했어요,True
shSNzAio8H,emotion,dislike,싫어해요,True
@@ -218,6 +231,7 @@ E3fWcTGiDH,emotion,sad,슬퍼요,True
VNwYfr00ga,emotion,shy,부끄러워요,True
hDt7NLgf2u,emotion,simple,간단해요,True
86201LBWef,emotion,sorry,미안해요,True
+uamsWkkXKf,emotion,stressed,스트레스받아요,False
2X0JOPBeuS,emotion,surprised,놀랐어요,True
kpnbdnPwqE,emotion,tired,피곤해요,True
OCaMDYq79-,emotion,tiring,피곤해요,True
@@ -246,6 +260,7 @@ tEDMBJYVR8,topic,blocks,블록,True
rwvFdtPeH0,topic,book,책,True
6dkvootrxa,topic,break,휴식,True
SPH_0gDzXd,topic,break time,쉬는 시간,True
+LsJBeFwtmA,topic,breaktime,쉬는 시간,False
2DNqirPt0_,topic,car,자동차,False
abPVau7epK,topic,cartoon,만화,True
ucBETq0Hhp,topic,cartoons,만화,True
@@ -255,6 +270,7 @@ lanXgQ4kc9,topic,classes,수업,True
uK0UmLUNHi,topic,classmate,친구,True
IKFZpU4ECQ,topic,classmates,동급생,True
L5n_Q6xRVF,topic,classroom,교실,True
+uG4JKN6TSL,topic,classroom activity,교실 활동,False
63BIuiTJAL,topic,color,색깔,True
ZMzrVSxPQy,topic,colors,색깔,True
ZqQmq0jgXj,topic,conversation,대화,True
@@ -278,6 +294,7 @@ XYd7spA6_e,topic,food,음식,True
LOPwB6gP7U,topic,football,축구,True
bXfu9JuvvL,topic,friend,친구,True
FzmfcoAijg,topic,friends,친구,True
+wYZ3hlqRaH,topic,fun,즐거움,False
kIjPY5r2mK,topic,game,놀이,True
HXUW6Aqul2,topic,games,게임,True
yzbNoUa1xE,topic,gluing,붙이기,True
@@ -294,7 +311,9 @@ yHMiG8vVr7,topic,lesson,수업,True
KqMbwnuQTE,topic,lessons,수업,True
7HjNTrqSKY,topic,listen,듣기,True
4PgUQB51Ht,topic,lunch,점심,True
+_0hjBhSJwK,topic,lunchtime,점심 시간,False
Vw7mR4BXYQ,topic,math,수학,True
+-Q6cagIwqO,topic,math class,수학 수업,False
vMmv5GwdN7,topic,meal,식사,True
92wk3FeCUg,topic,mom,엄마,True
0IK-m4gxPa,topic,movie,영화,True
@@ -345,6 +364,7 @@ ymcbqCiwH-,topic,songs,노래,False
Ddl43DNFUp,topic,sports,스포츠,True
lnpE__vNu7,topic,story,이야기,True
sX6NvIW7H0,topic,study,공부,True
+6JyUR7bg_T,topic,studying,공부,False
Oct0H6k8G7,topic,subject,과목,True
rzQIL0Zu5b,topic,subjects,과목,False
3L58S0C71G,topic,swing,그네,True
@@ -352,6 +372,7 @@ rjRieEWi9P,topic,talk,이야기,True
53eLBLSTuk,topic,teacher,선생님,True
8T9a-E4F7B,topic,theater,극장,True
fwevuioC5m,topic,tickets,티켓,True
+Cvu5PkRqny,topic,time,시간,False
UhU29qrEDh,topic,together,함께,True
05joagL419,topic,toy,장난감,True
pt-tfZEEK4,topic,toys,장난감,True
diff --git a/libs/py_core/py_core.iml b/libs/py_core/py_core.iml
index 5b0f629..9faa5f5 100644
--- a/libs/py_core/py_core.iml
+++ b/libs/py_core/py_core.iml
@@ -2,8 +2,10 @@
-
-
+
+
+
+
\ No newline at end of file
diff --git a/libs/py_core/py_core/cli.py b/libs/py_core/py_core/cli.py
index faebfe1..27bbaf7 100644
--- a/libs/py_core/py_core/cli.py
+++ b/libs/py_core/py_core/cli.py
@@ -2,7 +2,8 @@
from chatlib.utils.validator import make_non_empty_string_validator
from py_core import ModeratorSession
-from py_core.system.model import ChildCardRecommendationResult, ParentGuideRecommendationResult, CardInfo, DialogueRole
+from py_core.system.model import ChildCardRecommendationResult, ParentGuideRecommendationResult, CardInfo, DialogueRole, \
+ ParentGuideType, ParentExampleMessage
async def test_session_loop(session: ModeratorSession):
@@ -15,12 +16,33 @@ async def test_session_loop(session: ModeratorSession):
if current_parent_guide_recommendation_result is not None:
print(current_parent_guide_recommendation_result.model_dump_json(indent=2))
+ if len(current_parent_guide_recommendation_result.messaging_guides) > 0:
+ while True:
+ enter_parent_message = "Enter parent message"
+ choices = []
+ choices.extend([f"Show example message for \"{guide.guide_localized}\"" for guide in
+ current_parent_guide_recommendation_result.messaging_guides])
+ choices.append(enter_parent_message)
+ selection = await questionary.select(
+ message="Choose an option.",
+ choices=choices,
+ default="Enter parent message"
+ ).ask_async()
+
+ if choices.index(selection) < len(choices) - 1:
+ guide = current_parent_guide_recommendation_result.messaging_guides[choices.index(selection)]
+ example_message: ParentExampleMessage = await session.request_parent_example_message(
+ current_parent_guide_recommendation_result.id, guide_id=guide.id)
+ questionary.print(f"\"{example_message.message_localized}\" ({example_message.message})", style="bold italic fg:green")
+ else:
+ break
parent_message = await questionary.text(": ",
default="오늘 학교에서 뭐 했니?" if current_parent_guide_recommendation_result is None and current_card_recommendation_result is None else "",
validate=make_non_empty_string_validator(
"A message should not be empty."), qmark="*").ask_async()
- current_card_recommendation_result = await session.submit_parent_message(parent_message, current_parent_guide_recommendation_result)
+ current_card_recommendation_result = await session.submit_parent_message(parent_message,
+ current_parent_guide_recommendation_result)
current_interim_cards.clear()
continue
@@ -34,11 +56,11 @@ async def test_session_loop(session: ModeratorSession):
if submittable:
choices += ["[Submit]"]
- selection = await questionary.select(**{
- 'choices': choices,
- 'default': '[Refresh cards]',
- 'message': f'Choose a word card. {"" if current_interim_cards is None or len(current_interim_cards) == 0 else ("Current selection: " + ", ".join([card.simple_str() for card in current_interim_cards]))}...'
- }).ask_async()
+ selection = await questionary.select(
+ choices=choices,
+ default='[Refresh cards]',
+ message=f'Choose a word card. {"" if current_interim_cards is None or len(current_interim_cards) == 0 else ("Current selection: " + ", ".join([card.simple_str() for card in current_interim_cards]))}...'
+ ).ask_async()
if choices.index(selection) == 0:
# refresh
diff --git a/libs/py_core/py_core/system/guide_categories.py b/libs/py_core/py_core/system/guide_categories.py
new file mode 100644
index 0000000..c1b42d3
--- /dev/null
+++ b/libs/py_core/py_core/system/guide_categories.py
@@ -0,0 +1,78 @@
+from enum import StrEnum
+from functools import cache
+from typing import Optional
+
+from pydantic import BaseModel
+
+
+class CategoryWithDescription(BaseModel):
+ label: str
+ description: str
+ min_turns: Optional[int] = None # Min number of messages in dialogue to activate this category
+
+class DialogueInspectionCategory(StrEnum):
+ Blame = "blame"
+ Correction = "correction"
+ Complex = "complex"
+ Deviation = "deviation"
+
+ def __init__(self, value):
+ self.description: str = {
+ "blame": "When the parent criticizes or negatively evaluates the child's responds, like reprimanding or scolding",
+ "correction": "When the parent is compulsively correcting the child's response or pointing out that the child is wrong",
+ "complex": "When a parent's dialogue contains more than one goal or intent",
+ "deviation": "When both parent and child stray from the main topic of the conversation"
+ }[value]
+
+ min_turns_table = {
+ "deviation": 5
+ }
+
+ self.inspection_min_turns: int | None = min_turns_table[value] if value in min_turns_table else None
+
+ @classmethod
+ @cache
+ def values_with_desc(cls) -> list[CategoryWithDescription]:
+ return list(map(lambda c: CategoryWithDescription(label=c.value, description=c.description, min_turns=c.inspection_min_turns), cls))
+
+
+class ParentGuideCategory(StrEnum):
+ Intention="intention"
+ Specification="specification"
+ Choice="choice"
+ Clues="clues"
+ Coping="coping"
+ Stimulate="stimulate"
+ Share="share"
+ Empathize="empathize"
+ Encourage="encourage"
+ Emotion="emotion"
+ Extend="extend"
+ Terminate="terminate"
+
+ def __init__(self, value):
+ self.description={
+ "intention": "Check the intention behind the child’s response and ask back",
+ "specification": "Ask about \"what\" to specify the event",
+ "choice": "Provide choices for children to select their answers",
+ "clues": "Give clues that can be answered based on previously known information",
+ "coping": "Suggest coping strategies for specific situations to the child",
+ "stimulate": "Present information that contradicts what is known to stimulate the child's interest",
+ "share": "Share the parent's emotions and thoughts in simple language",
+ "empathize": "Empathize with the child's feelings",
+ "encourage": "Encourage the child's actions or emotions",
+ "emotion": "Asking about the child's feelings and emotions",
+ "extend": "Inducing an expansion or change of the conversation topic",
+ "terminate": "Inquiring about the desire to end the conversation"
+ }[value]
+
+ min_turns_table = {
+ "terminate": 5
+ }
+
+ self.active_min_turns: int | None = min_turns_table[value] if value in min_turns_table else None
+
+ @classmethod
+ @cache
+ def values_with_desc(cls) -> list[CategoryWithDescription]:
+ return list(map(lambda c: CategoryWithDescription(label=c.value, description=c.description, min_turns=c.active_min_turns), cls))
diff --git a/libs/py_core/py_core/system/model.py b/libs/py_core/py_core/system/model.py
index d6c5e5f..db6b8c5 100644
--- a/libs/py_core/py_core/system/model.py
+++ b/libs/py_core/py_core/system/model.py
@@ -1,10 +1,13 @@
from enum import StrEnum
+from functools import cached_property
from typing import TypeAlias, Optional
from chatlib.utils.time import get_timestamp
from nanoid import generate
from pydantic import BaseModel, ConfigDict, TypeAdapter, Field
+from py_core.system.guide_categories import ParentGuideCategory
+
def id_generator() -> str:
return generate(size=20)
@@ -29,7 +32,7 @@ class CardCategory(StrEnum):
class CardInfo(CardIdentity):
- model_config = ConfigDict(frozen=True)
+ model_config = ConfigDict(frozen=True, use_enum_values=True)
text: str = Field()
localized: str
@@ -58,29 +61,49 @@ class ParentGuideType(StrEnum):
class ParentGuideElement(BaseModel):
- model_config = ConfigDict(frozen=True)
+ model_config = ConfigDict(frozen=True, use_enum_values=True)
- category: str | None
+ id: str = Field(default_factory=lambda: generate(size=5))
+
+ category: ParentGuideCategory | None
guide: str
- guide_localized: Optional[str] =None
+ guide_localized: Optional[str] = None
type: ParentGuideType = ParentGuideType.Messaging
def with_guide_localized(self, localized: str) -> 'ParentGuideElement':
return self.model_copy(update=dict(guide_localized=localized))
@classmethod
- def messaging_guide(cls, category: str, guide: str) -> 'ParentGuideElement':
+ def messaging_guide(cls, category: ParentGuideCategory, guide: str) -> 'ParentGuideElement':
return ParentGuideElement(category=category, guide=guide, type=ParentGuideType.Messaging)
@classmethod
- def feedback(cls, category: str | None, guide: str) -> 'ParentGuideElement':
+ def feedback(cls, category: ParentGuideCategory | None, guide: str) -> 'ParentGuideElement':
return ParentGuideElement(category=category, guide=guide, type=ParentGuideType.Feedback)
class ParentGuideRecommendationResult(ModelWithIdAndTimestamp):
model_config = ConfigDict(frozen=True)
- recommendations: list[ParentGuideElement]
+ guides: list[ParentGuideElement]
+
+ @cached_property
+ def messaging_guides(self) -> list[ParentGuideElement]:
+ return [guide for guide in self.guides if guide.type == ParentGuideType.Messaging]
+
+ @cached_property
+ def feedback_guides(self) -> list[ParentGuideElement]:
+ return [guide for guide in self.guides if guide.type == ParentGuideType.Feedback]
+
+
+class ParentExampleMessage(ModelWithIdAndTimestamp):
+ model_config = ConfigDict(frozen=True)
+
+ recommendation_id: str | None = None
+ guide_id: str | None = None
+
+ message: str
+ message_localized: str | None = None
class DialogueRole(StrEnum):
diff --git a/libs/py_core/py_core/system/moderator.py b/libs/py_core/py_core/system/moderator.py
index 10cd701..de26857 100644
--- a/libs/py_core/py_core/system/moderator.py
+++ b/libs/py_core/py_core/system/moderator.py
@@ -1,14 +1,18 @@
import asyncio
+from dataclasses import dataclass
+
from nanoid import generate
from py_core.system.model import ChildCardRecommendationResult, DialogueMessage, DialogueRole, CardInfo, \
CardIdentity, \
- ParentGuideRecommendationResult
+ ParentGuideRecommendationResult, Dialogue, ParentGuideType, ParentExampleMessage, ParentGuideElement
from py_core.system.storage import SessionStorage
from py_core.system.task import ChildCardRecommendationGenerator
-from py_core.system.task.parent_guide_recommendation import ParentGuideRecommendationGenerator
+from py_core.system.task.parent_guide_recommendation import ParentGuideRecommendationGenerator, \
+ ParentExampleMessageGenerator
from py_core.system.task.parent_guide_recommendation.dialogue_inspector import DialogueInspector
from py_core.utils.deepl_translator import DeepLTranslator
+from py_core.utils.models import AsyncTaskInfo
def speaker(role: DialogueRole):
@@ -27,6 +31,12 @@ async def wrapper(self: 'ModeratorSession', *args, **kwargs):
return decorator
+@dataclass
+class ParentExampleGenerationTaskSet:
+ recommendation_id: str
+ tasks: dict[str, AsyncTaskInfo | None]
+
+
class ModeratorSession:
def __init__(self, storage: SessionStorage):
@@ -40,18 +50,45 @@ def __init__(self, storage: SessionStorage):
self.__deepl_translator = DeepLTranslator()
self.__dialogue_inspector = DialogueInspector()
- self.__dialogue_inspection_task: asyncio.Task | None = None
- self.__dialogue_inspection_task_id: str | None = None
+ self.__dialogue_inspection_task_info: AsyncTaskInfo | None = None
+
+ self.__parent_example_generation_tasks: ParentExampleGenerationTaskSet | None = None
+
+ self.__parent_example_generator = ParentExampleMessageGenerator()
@property
def next_speaker(self) -> DialogueRole:
return self.__next_speaker
+ def __clear_parent_example_generation_tasks(self):
+ if self.__parent_example_generation_tasks is not None:
+ for k, t in self.__parent_example_generation_tasks.tasks.items():
+ if not t.task.done():
+ t.task.cancel()
+ self.__parent_example_generation_tasks = None
+
+ async def __parent_example_generate_func(self, dialogue: Dialogue, guide: ParentGuideElement, recommendation_id: str) -> ParentExampleMessage:
+ message = await self.__parent_example_generator.generate(dialogue, guide, recommendation_id)
+ await self.__storage.add_parent_example_message(message)
+ return message
+
+ def __place_parent_example_generation_tasks(self, dialogue: Dialogue, recommendation: ParentGuideRecommendationResult):
+ self.__parent_example_generation_tasks = ParentExampleGenerationTaskSet(
+ recommendation_id=recommendation.id,
+ tasks={guide.id: AsyncTaskInfo(
+ task_id=guide.id,
+ task=asyncio.create_task(self.__parent_example_generate_func(dialogue, guide, recommendation.id))
+ ) for guide in recommendation.guides if guide.type == ParentGuideType.Messaging}
+ )
+
@speaker(DialogueRole.Parent)
async def submit_parent_message(self, parent_message: str,
current_parent_guide: ParentGuideRecommendationResult | None = None) -> ChildCardRecommendationResult:
try:
+ # Clear if there is a pending example generation task.
+ self.__clear_parent_example_generation_tasks()
+
print("Translate parent message..")
message_eng = await self.__deepl_translator.translate(
text=parent_message,
@@ -71,12 +108,15 @@ async def submit_parent_message(self, parent_message: str,
dialogue = await self.__storage.get_dialogue()
# Start a background task for inspection.
- if self.__dialogue_inspection_task is not None:
- self.__dialogue_inspection_task.cancel()
+ if self.__dialogue_inspection_task_info is not None:
+ self.__dialogue_inspection_task_info.task.cancel()
- self.__dialogue_inspection_task_id = generate(size=5)
+ inspection_task_id = generate(size=5)
- self.__dialogue_inspection_task = asyncio.create_task(self.__dialogue_inspector.inspect(dialogue, self.__dialogue_inspection_task_id))
+ self.__dialogue_inspection_task_info = AsyncTaskInfo(task_id=inspection_task_id,
+ task=asyncio.create_task(
+ self.__dialogue_inspector.inspect(dialogue,
+ inspection_task_id)))
recommendation = await self.__child_card_recommender.generate(dialogue, None, None)
@@ -88,7 +128,6 @@ async def submit_parent_message(self, parent_message: str,
except Exception as e:
raise e
-
async def __get_card_info_from_identities(self, cards: list[CardIdentity] | list[CardInfo]) -> list[CardInfo]:
cards = [card_identity if isinstance(card_identity, CardInfo) else (
await self.__storage.get_card_recommendation_result(card_identity.recommendation_id)).find_card_by_id(
@@ -123,22 +162,44 @@ async def submit_child_selected_card(self, selected_cards: list[CardIdentity] |
# Join a dialogue inspection task
dialogue_inspection_result = None
- if self.__dialogue_inspection_task_id is not None and self.__dialogue_inspection_task is not None:
- dialogue_inspection_result, task_id = await self.__dialogue_inspection_task
- if task_id != self.__dialogue_inspection_task_id:
+ if self.__dialogue_inspection_task_info is not None:
+ dialogue_inspection_result, task_id = await self.__dialogue_inspection_task_info.task
+ if task_id != self.__dialogue_inspection_task_info.task_id:
dialogue_inspection_result = None
# Clear
- self.__dialogue_inspection_task_id = None
- self.__dialogue_inspection_task = None
+ self.__dialogue_inspection_task_info = None
recommendation = await self.__parent_guide_recommender.generate(dialogue, dialogue_inspection_result)
await self.__storage.add_parent_guide_recommendation_result(recommendation)
+ # Invoke an example generation task in advance.
+ self.__clear_parent_example_generation_tasks()
+ self.__place_parent_example_generation_tasks(dialogue, recommendation)
+
self.__next_speaker = DialogueRole.Parent
return recommendation
except Exception as e:
raise e
+
+ @speaker(DialogueRole.Parent)
+ async def request_parent_example_message(self, recommendation_id: str, guide_id: str) -> ParentExampleMessage:
+ if (self.__parent_example_generation_tasks is not None
+ and self.__parent_example_generation_tasks.recommendation_id == recommendation_id):
+ example_message: ParentExampleMessage = await self.__parent_example_generation_tasks.tasks[guide_id].task
+ return example_message
+ else:
+ example_message: ParentExampleMessage = await self.__storage.get_parent_example_message(recommendation_id,
+ guide_id)
+ if example_message is not None:
+ return example_message
+ else:
+ dialogue = await self.__storage.get_dialogue()
+ recommendation = await self.__storage.get_parent_guide_recommendation_result(recommendation_id)
+ guide = [guide for guide in recommendation.guides if guide.id == guide_id][0]
+ example_message: ParentExampleMessage = await self.__parent_example_generate_func(dialogue, guide, recommendation.id)
+ return example_message
+
diff --git a/libs/py_core/py_core/system/storage/json.py b/libs/py_core/py_core/system/storage/json.py
index ddf168c..ca8cd12 100644
--- a/libs/py_core/py_core/system/storage/json.py
+++ b/libs/py_core/py_core/system/storage/json.py
@@ -6,7 +6,7 @@
from os import path, getcwd, makedirs
from py_core.system.model import ParentGuideRecommendationResult, ChildCardRecommendationResult, Dialogue, \
- DialogueMessage, DialogueTypeAdapter
+ DialogueMessage, DialogueTypeAdapter, ParentExampleMessage
from py_core.system.storage import SessionStorage
@@ -14,6 +14,7 @@ class SessionJsonStorage(SessionStorage):
TABLE_MESSAGES = "messages"
TABLE_CARD_RECOMMENDATIONS = "card_recommendations"
TABLE_PARENT_RECOMMENDATIONS = "parent_recommendations"
+ TABLE_PARENT_EXAMPLE_MESSAGES = "parent_example_messages"
def __init__(self, session_id: str | None = None):
super().__init__(session_id)
@@ -67,3 +68,16 @@ async def get_parent_guide_recommendation_result(self,
return ParentGuideRecommendationResult(**result[0])
else:
return None
+
+ async def add_parent_example_message(self, message: ParentExampleMessage):
+ table = self.db.table(self.TABLE_PARENT_EXAMPLE_MESSAGES)
+ table.insert(message.model_dump())
+
+ async def get_parent_example_message(self, recommendation_id: str, guide_id: str) -> ParentExampleMessage | None:
+ table = self.db.table(self.TABLE_PARENT_EXAMPLE_MESSAGES)
+ q = Query()
+ result = table.search((q.recommendation_id == recommendation_id) & (q.guide_id == guide_id))
+ if len(result) > 0:
+ return ParentExampleMessage(**result[0])
+ else:
+ return None
diff --git a/libs/py_core/py_core/system/storage/memory.py b/libs/py_core/py_core/system/storage/memory.py
index 01e5257..ee98f93 100644
--- a/libs/py_core/py_core/system/storage/memory.py
+++ b/libs/py_core/py_core/system/storage/memory.py
@@ -1,11 +1,10 @@
from py_core.system.model import Dialogue, ParentGuideRecommendationResult, ChildCardRecommendationResult, \
- DialogueMessage
+ DialogueMessage, ParentExampleMessage
from py_core.system.storage.session_storage import SessionStorage
class SessionMemoryStorage(SessionStorage):
-
def __init__(self, session_id: str | None = None):
super().__init__(session_id)
@@ -13,6 +12,8 @@ def __init__(self, session_id: str | None = None):
self.__parent_guide_recommendations: dict[str, ParentGuideRecommendationResult] = {}
self.__card_recommendations: dict[str, ChildCardRecommendationResult] = {}
+ self.__parent_example_messages: dict[tuple[str, str], ParentExampleMessage] = {}
+
async def add_dialogue_message(self, message: DialogueMessage):
self.__dialogue.append(message)
@@ -37,3 +38,12 @@ async def get_parent_guide_recommendation_result(self,
return self.__parent_guide_recommendations[recommendation_id]
else:
return None
+
+ async def add_parent_example_message(self, message: ParentExampleMessage):
+ self.__parent_example_messages[(message.recommendation_id, message.guide_id)] = message
+
+ async def get_parent_example_message(self, recommendation_id: str, guide_id: str) -> ParentExampleMessage | None:
+ if (recommendation_id, guide_id) in self.__parent_example_messages:
+ return self.__parent_example_messages[(recommendation_id, guide_id)]
+ else:
+ return None
diff --git a/libs/py_core/py_core/system/storage/session_storage.py b/libs/py_core/py_core/system/storage/session_storage.py
index af8131f..b477743 100644
--- a/libs/py_core/py_core/system/storage/session_storage.py
+++ b/libs/py_core/py_core/system/storage/session_storage.py
@@ -1,7 +1,7 @@
from abc import ABC, abstractmethod
from py_core.system.model import ChildCardRecommendationResult, Dialogue, DialogueMessage, \
- ParentGuideRecommendationResult, id_generator
+ ParentGuideRecommendationResult, id_generator, ParentExampleMessage
class SessionStorage(ABC):
@@ -29,6 +29,10 @@ async def add_card_recommendation_result(self, result: ChildCardRecommendationRe
async def add_parent_guide_recommendation_result(self, result: ParentGuideRecommendationResult):
pass
+ @abstractmethod
+ async def add_parent_example_message(self, message: ParentExampleMessage):
+ pass
+
@abstractmethod
async def get_card_recommendation_result(self, recommendation_id: str)->ChildCardRecommendationResult | None:
pass
@@ -37,5 +41,7 @@ async def get_card_recommendation_result(self, recommendation_id: str)->ChildCar
async def get_parent_guide_recommendation_result(self, recommendation_id: str)->ParentGuideRecommendationResult | None:
pass
-
+ @abstractmethod
+ async def get_parent_example_message(self, recommendation_id: str, guide_id: str) -> ParentExampleMessage | None:
+ pass
diff --git a/libs/py_core/py_core/system/task/parent_guide_recommendation/__init__.py b/libs/py_core/py_core/system/task/parent_guide_recommendation/__init__.py
index 3661ea4..7b627d5 100644
--- a/libs/py_core/py_core/system/task/parent_guide_recommendation/__init__.py
+++ b/libs/py_core/py_core/system/task/parent_guide_recommendation/__init__.py
@@ -1 +1,2 @@
-from .generator import ParentGuideRecommendationGenerator
+from .guide_generator import ParentGuideRecommendationGenerator
+from .example_generator import ParentExampleMessageGenerator
diff --git a/libs/py_core/py_core/system/task/parent_guide_recommendation/common.py b/libs/py_core/py_core/system/task/parent_guide_recommendation/common.py
index eba3bd7..9861718 100644
--- a/libs/py_core/py_core/system/task/parent_guide_recommendation/common.py
+++ b/libs/py_core/py_core/system/task/parent_guide_recommendation/common.py
@@ -1,88 +1,12 @@
-from enum import StrEnum
-from functools import cache
-from typing import TypeAlias, Optional
+from typing import TypeAlias
from pydantic import BaseModel
+from py_core.system.guide_categories import DialogueInspectionCategory
from py_core.system.model import ParentGuideElement
ParentGuideRecommendationAPIResult: TypeAlias = list[ParentGuideElement]
-class CategoryWithDescription(BaseModel):
- label: str
- description: str
- min_turns: Optional[int] = None # Min number of messages in dialogue to activate this category
-
-class DialogueInspectionCategory(StrEnum):
- Blame = "blame"
- Correction = "correction"
- Complex = "complex"
- Deviation = "deviation"
-
- def __init__(self, value):
- self.description: str = {
- "blame": "When the parent criticizes or negatively evaluates the child's responds, like reprimanding or scolding",
- "correction": "When the parent is compulsively correcting the child's response or pointing out that the child is wrong",
- "complex": "When a parent's dialogue contains more than one goal or intent",
- "deviation": "When both parent and child stray from the main topic of the conversation"
- }[value]
-
- min_turns_table = {
- "deviation": 5
- }
-
- self.inspection_min_turns: int | None = min_turns_table[value] if value in min_turns_table else None
-
- @classmethod
- @cache
- def values_with_desc(cls) -> list[CategoryWithDescription]:
- return list(map(lambda c: CategoryWithDescription(label=c.value, description=c.description, min_turns=c.inspection_min_turns), cls))
-
-
-class ParentGuideCategory(StrEnum):
- Intention="intention"
- Specification="specification"
- Choice="choice"
- Clues="clues"
- Coping="coping"
- Stimulate="stimulate"
- Share="share"
- Empathize="empathize"
- Encourage="encourage"
- Emotion="emotion"
- Extend="extend"
- Terminate="terminate"
-
- def __init__(self, value):
- self.description={
- "intention": "Check the intention behind the child’s response and ask back",
- "specification": "Ask about \"what\" to specify the event",
- "choice": "Provide choices for children to select their answers",
- "clues": "Give clues that can be answered based on previously known information",
- "coping": "Suggest coping strategies for specific situations to the child",
- "stimulate": "Present information that contradicts what is known to stimulate the child's interest",
- "share": "Share the parent's emotions and thoughts in simple language",
- "empathize": "Empathize with the child's feelings",
- "encourage": "Encourage the child's actions or emotions",
- "emotion": "Asking about the child's feelings and emotions",
- "extend": "Inducing an expansion or change of the conversation topic",
- "terminate": "Inquiring about the desire to end the conversation"
- }[value]
-
- min_turns_table = {
- "terminate": 5
- }
-
- self.active_min_turns: int | None = min_turns_table[value] if value in min_turns_table else None
-
- @classmethod
- @cache
- def values_with_desc(cls) -> list[CategoryWithDescription]:
- return list(map(lambda c: CategoryWithDescription(label=c.value, description=c.description, min_turns=c.active_min_turns), cls))
-
-
-
-
class DialogueInspectionResult(BaseModel):
categories: list[DialogueInspectionCategory]
diff --git a/libs/py_core/py_core/system/task/parent_guide_recommendation/dialogue_inspector.py b/libs/py_core/py_core/system/task/parent_guide_recommendation/dialogue_inspector.py
index 0eb6eb5..59efede 100644
--- a/libs/py_core/py_core/system/task/parent_guide_recommendation/dialogue_inspector.py
+++ b/libs/py_core/py_core/system/task/parent_guide_recommendation/dialogue_inspector.py
@@ -6,9 +6,9 @@
MapperInputOutputPair
from chatlib.llm.integration import GPTChatCompletionAPI, ChatGPTModel
+from py_core.system.guide_categories import DialogueInspectionCategory
from py_core.system.model import Dialogue, DialogueMessage, DialogueRole, CardCategory
-from py_core.system.task.parent_guide_recommendation.common import DialogueInspectionResult, \
- DialogueInspectionCategory
+from py_core.system.task.parent_guide_recommendation.common import DialogueInspectionResult
from py_core.system.task.stringify import DialogueToStrConversionFunction
_EXAMPLES = [
diff --git a/libs/py_core/py_core/system/task/parent_guide_recommendation/example_generator.py b/libs/py_core/py_core/system/task/parent_guide_recommendation/example_generator.py
new file mode 100644
index 0000000..8098ede
--- /dev/null
+++ b/libs/py_core/py_core/system/task/parent_guide_recommendation/example_generator.py
@@ -0,0 +1,68 @@
+from time import perf_counter
+
+from chatlib.llm.integration import GPTChatCompletionAPI, ChatGPTModel
+from chatlib.tool.converter import str_to_str_noop
+from chatlib.utils.jinja_utils import convert_to_jinja_template
+from chatlib.tool.versatile_mapper import ChatCompletionFewShotMapperParams, ChatCompletionFewShotMapper
+from pydantic import BaseModel
+
+from py_core.system.model import ParentGuideElement, ParentExampleMessage, Dialogue
+from py_core.system.task.parent_guide_recommendation.example_translator import ParentExampleMessageTranslator
+from py_core.system.task.stringify import DialogueToStrConversionFunction
+
+
+class ParentExampleMessageGenerationInput(BaseModel):
+ dialogue: Dialogue
+ guide: ParentGuideElement
+
+_dialogue_to_str = DialogueToStrConversionFunction()
+
+def _convert_input_to_str(input: ParentExampleMessageGenerationInput, params)->str:
+ return f"""{_dialogue_to_str(input.dialogue, params)}
+ {input.guide.guide}
+ """
+
+
+_prompt_template = convert_to_jinja_template("""
+You are a helpful assistant who helps facilitate communication between minimally verbal autistic children and their parents. The goal of their conversation is to help the child and parent elaborate on a topic together.
+[Task] Given a dialogue between a parent and a child and a message generation guide, suggest an example utterance that the parent can respond to the child's last message.
+
+[Input]
+: The current state of the conversation
+: A guidance for example utterance generation
+
+[Output]
+A text string containing an utterance of the parent that complies with the message generation guide.
+""")
+
+def _prompt_generator(input: ParentExampleMessageGenerationInput, params) -> str:
+ return _prompt_template.render()
+
+class ParentExampleMessageGenerator:
+ def __init__(self):
+ api = GPTChatCompletionAPI()
+ api.config().verbose = False
+
+ self.__mapper: ChatCompletionFewShotMapper[
+ ParentExampleMessageGenerationInput,
+ str,
+ ChatCompletionFewShotMapperParams] = ChatCompletionFewShotMapper(
+ api,
+ instruction_generator=_prompt_generator,
+ input_str_converter=_convert_input_to_str,
+ output_str_converter=str_to_str_noop,
+ str_output_converter=str_to_str_noop
+ )
+
+ self.__translator = ParentExampleMessageTranslator()
+
+ async def generate(self, dialogue: Dialogue, guide: ParentGuideElement, recommendation_id: str) -> ParentExampleMessage:
+
+ t_start = perf_counter()
+ utterance = await self.__mapper.run(None, ParentExampleMessageGenerationInput(dialogue=dialogue, guide=guide),
+ ChatCompletionFewShotMapperParams(model=ChatGPTModel.GPT_4_0613, api_params={}))
+ translated_utterance = await self.__translator.translate_example(utterance)
+ t_end = perf_counter()
+ # print(f"Example generation took {t_end - t_start} sec - {utterance} ({guide.category} - {guide.guide})")
+
+ return ParentExampleMessage(recommendation_id=recommendation_id, guide_id=guide.id, message=utterance, message_localized=translated_utterance)
diff --git a/libs/py_core/py_core/system/task/parent_guide_recommendation/example_translator.py b/libs/py_core/py_core/system/task/parent_guide_recommendation/example_translator.py
index dca900b..d67e7ba 100644
--- a/libs/py_core/py_core/system/task/parent_guide_recommendation/example_translator.py
+++ b/libs/py_core/py_core/system/task/parent_guide_recommendation/example_translator.py
@@ -1,58 +1,27 @@
-import asyncio
import re
-from time import perf_counter
from chatlib.llm.integration import GPTChatCompletionAPI, ChatGPTModel
-from chatlib.tool.versatile_mapper import ChatCompletionFewShotMapper, ChatCompletionFewShotMapperParams
+from chatlib.tool.versatile_mapper import ChatCompletionFewShotMapper, ChatCompletionFewShotMapperParams, \
+ MapperInputOutputPair
from chatlib.utils.jinja_utils import convert_to_jinja_template
+from time import perf_counter
from py_core.config import AACessTalkConfig
-from py_core.system.model import ParentGuideElement
from py_core.system.shared import vector_db
from py_core.system.task.parent_guide_recommendation.common import ParentGuideRecommendationAPIResult
-from py_core.utils.deepl_translator import DeepLTranslator
from py_core.utils.lookup_translator import LookupTranslator
from py_core.utils.models import DictionaryRow
-
-def convert_messages_to_xml(messages: list[str], params) -> str:
- content = "\n".join([f" {msg}" for i, msg in enumerate(messages)])
- return f"""
-{content}
-"""
-
-
-msg_regex = re.compile(r'(.*?)')
-
-
-def convert_xml_to_messages(xml: str, params=None) -> list[str]:
- matches = msg_regex.findall(xml)
- return matches
-
-
-template = convert_to_jinja_template("""The following XML list contains a list of messages for a parent talking with their child with ASD.
-Translate the following messages into Korean.
-Note that the messages are intended to be spoken by parent to a kid.
-Don't use honorific form of Korean.
-
-{%-if samples is not none and samples | length > 0 %}
-
-Input:
-{{stringify(samples | map(attribute='english'), none)}}
-
-Output:
-{{stringify(samples | map(attribute='localized'), none)}}
-{%-endif-%}
+template = convert_to_jinja_template("""You are a helpful translator who translates an utterance of a parent talking with their child with ASD.
+[Task]
+- Translate the following English message to Korean.
+- Note that the messages are intended to be spoken by parent to a kid.
+- Don't use honorific form of Korean.
""")
-class ParentGuideExampleTranslationParams(ChatCompletionFewShotMapperParams):
- samples: list[DictionaryRow]
-
-
-def _generate_prompt(input, params: ParentGuideExampleTranslationParams) -> str:
- r = template.render(samples=params.samples, stringify=convert_messages_to_xml)
- print(r)
+def _generate_prompt(input, params) -> str:
+ r = template.render()
return r
@@ -67,30 +36,23 @@ def __init__(self):
vector_db=vector_db,
verbose=True)
- self.__example_translator: ChatCompletionFewShotMapper[
- list[str], list[str], ParentGuideExampleTranslationParams] = ChatCompletionFewShotMapper(api,
- instruction_generator=_generate_prompt,
- input_str_converter=convert_messages_to_xml,
- output_str_converter=convert_messages_to_xml,
- str_output_converter=convert_xml_to_messages
- )
+ self.__example_translator: ChatCompletionFewShotMapper[str, str, ChatCompletionFewShotMapperParams] = ChatCompletionFewShotMapper.make_str_mapper(api,
+ instruction_generator=_generate_prompt)
- async def __translate_examples(self, examples: list[str]) -> list[str]:
+ async def translate_example(self, original_message: str) -> str:
t_start = perf_counter()
- samples = self.__dictionary.query_similar_rows(examples, None, k=5)
+ samples = self.__dictionary.query_similar_rows(original_message, None, k=3)
+
+ samples_formatted = [
+ MapperInputOutputPair(input=sample.english, output=sample.localized) for sample in samples
+ ]
- result = await self.__example_translator.run(None, examples, ParentGuideExampleTranslationParams(
- api_params={}, model=ChatGPTModel.GPT_3_5_0613, samples=samples))
+ result = await self.__example_translator.run(samples_formatted, original_message, ChatCompletionFewShotMapperParams(
+ api_params={}, model=ChatGPTModel.GPT_3_5_0613))
t_end = perf_counter()
- print(f"LLM translation took {t_end - t_start} sec.")
+ # print(f"LLM translation took {t_end - t_start} sec.")
return result
-
- async def translate(self, api_result: ParentGuideRecommendationAPIResult) -> ParentGuideRecommendationAPIResult:
- examples = [entry.example for entry in api_result]
-
- translated_examples = await self.__translate_examples(examples)
- pass
diff --git a/libs/py_core/py_core/system/task/parent_guide_recommendation/generator.py b/libs/py_core/py_core/system/task/parent_guide_recommendation/guide_generator.py
similarity index 97%
rename from libs/py_core/py_core/system/task/parent_guide_recommendation/generator.py
rename to libs/py_core/py_core/system/task/parent_guide_recommendation/guide_generator.py
index 9213396..d406fad 100644
--- a/libs/py_core/py_core/system/task/parent_guide_recommendation/generator.py
+++ b/libs/py_core/py_core/system/task/parent_guide_recommendation/guide_generator.py
@@ -5,10 +5,11 @@
from chatlib.utils.jinja_utils import convert_to_jinja_template
from time import perf_counter
+from py_core.system.guide_categories import ParentGuideCategory
from py_core.system.model import ParentGuideRecommendationResult, Dialogue, ParentGuideElement, DialogueMessage, \
CardCategory
from py_core.system.task.parent_guide_recommendation.common import ParentGuideRecommendationAPIResult, \
- DialogueInspectionResult, DialogueInspectionCategory, ParentGuideCategory
+ DialogueInspectionResult
from py_core.system.task.parent_guide_recommendation.guide_translator import GuideTranslator
from py_core.system.task.stringify import DialogueToStrConversionFunction
@@ -120,4 +121,4 @@ async def generate(self, dialogue: Dialogue, inspection_result: DialogueInspecti
t_end = perf_counter()
print(f"Translation took {t_end - t_trans} sec.")
print(f"Total latency: {t_end - t_start} sec.")
- return ParentGuideRecommendationResult(recommendations=translated_guide_list)
+ return ParentGuideRecommendationResult(guides=translated_guide_list)
diff --git a/libs/py_core/py_core/utils/models.py b/libs/py_core/py_core/utils/models.py
index 2a0f562..6ccbf72 100644
--- a/libs/py_core/py_core/utils/models.py
+++ b/libs/py_core/py_core/utils/models.py
@@ -1,3 +1,5 @@
+import asyncio
+from dataclasses import dataclass
from functools import cached_property
from typing import Optional
@@ -56,3 +58,8 @@ def empty_str_to_none(cls, v: str) -> str | None:
else:
return v
+
+@dataclass
+class AsyncTaskInfo:
+ task: asyncio.Task
+ task_id: str
diff --git a/libs/py_core/py_core/utils/vector_db.py b/libs/py_core/py_core/utils/vector_db.py
index 95538b9..7727d6a 100644
--- a/libs/py_core/py_core/utils/vector_db.py
+++ b/libs/py_core/py_core/utils/vector_db.py
@@ -53,7 +53,7 @@ def upsert(self, collection: str | Collection, dictionary_row: DictionaryRow | l
)
def query_similar_rows(self, collection: str | Collection, word: str | list[str], category: str | None, k: int = 5) -> list[DictionaryRow]:
- print(f"Query similar cards: {word}, {category}")
+ #print(f"Query similar cards: {word}, {category}")
collection_instance = (collection if isinstance(collection, Collection) else self.get_collection(collection))
query_result = collection_instance.query(query_texts=[word] if isinstance(word, str) else word,
n_results=k, where={"category": category} if category is not None else None)