@@ -198,6 +198,23 @@ impl LlmClient {
198198
199199 let original_spec_text = original_spec. unwrap_or ( "(No original description)" ) ;
200200
201+ // Detect language from task name and events to respond in same language
202+ let is_cjk = task_name. chars ( ) . any ( |c| {
203+ matches ! ( c,
204+ '\u{4E00}' ..='\u{9FFF}' | // CJK Unified Ideographs
205+ '\u{3400}' ..='\u{4DBF}' | // CJK Extension A
206+ '\u{3040}' ..='\u{309F}' | // Hiragana
207+ '\u{30A0}' ..='\u{30FF}' | // Katakana
208+ '\u{AC00}' ..='\u{D7AF}' // Hangul
209+ )
210+ } ) ;
211+
212+ let language_instruction = if is_cjk {
213+ "Respond in Chinese (中文)."
214+ } else {
215+ "Respond in English."
216+ } ;
217+
201218 // Construct the prompt
202219 let prompt = format ! (
203220 r#"You are summarizing a completed task based on its execution history.
@@ -215,8 +232,10 @@ Synthesize a clear, structured description capturing:
2152324. Outcome (what was delivered?)
216233
217234Use markdown format with ## headers. Be concise but preserve critical context.
218- Output ONLY the markdown summary, no preamble or explanation."# ,
219- task_name, original_spec_text, events_text
235+ Output ONLY the markdown summary, no preamble or explanation.
236+
237+ IMPORTANT: {}"# ,
238+ task_name, original_spec_text, events_text, language_instruction
220239 ) ;
221240
222241 self . chat ( & prompt) . await
@@ -345,4 +364,160 @@ mod tests {
345364 assert ! ( json. contains( "\" role\" :\" user\" " ) ) ;
346365 assert ! ( json. contains( "\" content\" :\" Hello\" " ) ) ;
347366 }
367+
368+ #[ tokio:: test]
369+ async fn test_synthesize_task_description_when_unconfigured ( ) {
370+ let ctx = TestContext :: new ( ) . await ;
371+
372+ // Create a simple event for testing
373+ use chrono:: Utc ;
374+ let event = crate :: db:: models:: Event {
375+ id : 1 ,
376+ task_id : 1 ,
377+ log_type : "decision" . to_string ( ) ,
378+ discussion_data : "Test decision" . to_string ( ) ,
379+ timestamp : Utc :: now ( ) ,
380+ } ;
381+
382+ // Should return None when LLM not configured
383+ let result =
384+ synthesize_task_description ( ctx. pool ( ) , "Test Task" , Some ( "Original spec" ) , & [ event] )
385+ . await
386+ . unwrap ( ) ;
387+
388+ assert ! (
389+ result. is_none( ) ,
390+ "Should return None when LLM not configured"
391+ ) ;
392+ }
393+
394+ #[ tokio:: test]
395+ async fn test_synthesize_prompt_includes_task_info ( ) {
396+ // This test verifies the prompt structure without calling actual LLM
397+ use chrono:: Utc ;
398+
399+ let events = vec ! [
400+ crate :: db:: models:: Event {
401+ id: 1 ,
402+ task_id: 1 ,
403+ log_type: "decision" . to_string( ) ,
404+ discussion_data: "Chose approach A" . to_string( ) ,
405+ timestamp: Utc :: now( ) ,
406+ } ,
407+ crate :: db:: models:: Event {
408+ id: 2 ,
409+ task_id: 1 ,
410+ log_type: "milestone" . to_string( ) ,
411+ discussion_data: "Completed phase 1" . to_string( ) ,
412+ timestamp: Utc :: now( ) ,
413+ } ,
414+ ] ;
415+
416+ // Create a mock client (we can't test actual synthesis without LLM endpoint)
417+ // But we can verify the prompt construction logic
418+ let events_text: String = events
419+ . iter ( )
420+ . map ( |e| {
421+ format ! (
422+ "[{}] {} - {}" ,
423+ e. log_type,
424+ e. timestamp. format( "%Y-%m-%d %H:%M" ) ,
425+ e. discussion_data
426+ )
427+ } )
428+ . collect :: < Vec < _ > > ( )
429+ . join ( "\n " ) ;
430+
431+ // Verify event formatting
432+ assert ! ( events_text. contains( "decision" ) ) ;
433+ assert ! ( events_text. contains( "Chose approach A" ) ) ;
434+ assert ! ( events_text. contains( "milestone" ) ) ;
435+ assert ! ( events_text. contains( "Completed phase 1" ) ) ;
436+ }
437+
438+ #[ tokio:: test]
439+ async fn test_synthesize_with_empty_events ( ) {
440+ // Verify handling of tasks with no events
441+ let events: Vec < crate :: db:: models:: Event > = vec ! [ ] ;
442+
443+ // Should handle empty events gracefully
444+ // (actual synthesis would still work, just with "No events recorded")
445+ assert_eq ! ( events. len( ) , 0 ) ;
446+ }
447+
448+ #[ tokio:: test]
449+ async fn test_synthesize_with_no_original_spec ( ) {
450+ use chrono:: Utc ;
451+
452+ let original_spec: Option < & str > = None ;
453+ let events = vec ! [ crate :: db:: models:: Event {
454+ id: 1 ,
455+ task_id: 1 ,
456+ log_type: "note" . to_string( ) ,
457+ discussion_data: "Some work done" . to_string( ) ,
458+ timestamp: Utc :: now( ) ,
459+ } ] ;
460+
461+ // Should handle missing original spec
462+ // (prompt would use "(No original description)")
463+ assert ! ( original_spec. is_none( ) ) ;
464+ assert_eq ! ( events. len( ) , 1 ) ;
465+ }
466+
467+ #[ test]
468+ fn test_language_detection ( ) {
469+ // Test CJK detection logic
470+ let chinese_task = "实现用户认证" ;
471+ let english_task = "Implement authentication" ;
472+ let japanese_task = "認証を実装する" ;
473+ let korean_task = "인증 구현" ;
474+
475+ // Chinese
476+ let is_cjk = chinese_task. chars ( ) . any ( |c| {
477+ matches ! ( c,
478+ '\u{4E00}' ..='\u{9FFF}' |
479+ '\u{3400}' ..='\u{4DBF}' |
480+ '\u{3040}' ..='\u{309F}' |
481+ '\u{30A0}' ..='\u{30FF}' |
482+ '\u{AC00}' ..='\u{D7AF}'
483+ )
484+ } ) ;
485+ assert ! ( is_cjk, "Should detect Chinese characters" ) ;
486+
487+ // English
488+ let is_cjk = english_task. chars ( ) . any ( |c| {
489+ matches ! ( c,
490+ '\u{4E00}' ..='\u{9FFF}' |
491+ '\u{3400}' ..='\u{4DBF}' |
492+ '\u{3040}' ..='\u{309F}' |
493+ '\u{30A0}' ..='\u{30FF}' |
494+ '\u{AC00}' ..='\u{D7AF}'
495+ )
496+ } ) ;
497+ assert ! ( !is_cjk, "Should not detect CJK in English text" ) ;
498+
499+ // Japanese
500+ let is_cjk = japanese_task. chars ( ) . any ( |c| {
501+ matches ! ( c,
502+ '\u{4E00}' ..='\u{9FFF}' |
503+ '\u{3400}' ..='\u{4DBF}' |
504+ '\u{3040}' ..='\u{309F}' |
505+ '\u{30A0}' ..='\u{30FF}' |
506+ '\u{AC00}' ..='\u{D7AF}'
507+ )
508+ } ) ;
509+ assert ! ( is_cjk, "Should detect Japanese characters" ) ;
510+
511+ // Korean
512+ let is_cjk = korean_task. chars ( ) . any ( |c| {
513+ matches ! ( c,
514+ '\u{4E00}' ..='\u{9FFF}' |
515+ '\u{3400}' ..='\u{4DBF}' |
516+ '\u{3040}' ..='\u{309F}' |
517+ '\u{30A0}' ..='\u{30FF}' |
518+ '\u{AC00}' ..='\u{D7AF}'
519+ )
520+ } ) ;
521+ assert ! ( is_cjk, "Should detect Korean characters" ) ;
522+ }
348523}
0 commit comments