bat-6 commited on
Commit
4552666
·
1 Parent(s): d47f2eb
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +1 -1
  2. src/recommendation_engine/__init__.py +17 -2
  3. src/recommendation_engine/__pycache__/__init__.cpython-313.pyc +0 -0
  4. src/recommendation_engine/__pycache__/chatbot_engine.cpython-313.pyc +0 -0
  5. src/recommendation_engine/__pycache__/command_handler.cpython-313.pyc +0 -0
  6. src/recommendation_engine/__pycache__/config.cpython-313.pyc +0 -0
  7. src/recommendation_engine/__pycache__/context_builder.cpython-313.pyc +0 -0
  8. src/recommendation_engine/__pycache__/feature_generator.cpython-313.pyc +0 -0
  9. src/recommendation_engine/__pycache__/full_project_generator.cpython-313.pyc +0 -0
  10. src/recommendation_engine/__pycache__/idea_generator.cpython-313.pyc +0 -0
  11. src/recommendation_engine/__pycache__/llm_client.cpython-313.pyc +0 -0
  12. src/recommendation_engine/__pycache__/llm_router.cpython-313.pyc +0 -0
  13. src/recommendation_engine/__pycache__/memory_store.cpython-313.pyc +0 -0
  14. src/recommendation_engine/__pycache__/novelty_checker.cpython-313.pyc +0 -0
  15. src/recommendation_engine/__pycache__/prompt_builder.cpython-313.pyc +0 -0
  16. src/recommendation_engine/__pycache__/response_formatter.cpython-313.pyc +0 -0
  17. src/recommendation_engine/__pycache__/state_manager.cpython-313.pyc +0 -0
  18. src/recommendation_engine/__pycache__/validator.cpython-313.pyc +0 -0
  19. src/recommendation_engine/chatbot_engine.py +161 -185
  20. src/recommendation_engine/command_handler.py +24 -34
  21. src/recommendation_engine/config.py +1 -47
  22. src/recommendation_engine/context_builder.py +24 -67
  23. src/recommendation_engine/feature_generator.py +38 -66
  24. src/recommendation_engine/full_project_generator.py +13 -25
  25. src/recommendation_engine/idea_generator.py +19 -40
  26. src/recommendation_engine/intent_classifier.py +18 -37
  27. src/recommendation_engine/llm_client.py +7 -45
  28. src/recommendation_engine/llm_router.py +13 -15
  29. src/recommendation_engine/local_llm.py +6 -9
  30. src/recommendation_engine/memory_store.py +6 -40
  31. src/recommendation_engine/novelty_checker.py +16 -54
  32. src/recommendation_engine/prompt_builder.py +1 -27
  33. src/recommendation_engine/response_formatter.py +29 -50
  34. src/recommendation_engine/semantic_intent_classifier.py +7 -17
  35. src/recommendation_engine/state_manager.py +16 -30
  36. src/recommendation_engine/test.py +0 -123
  37. src/recommendation_engine/validator.py +21 -61
  38. src/similarity_model/__init__.py +1 -1
  39. src/similarity_model/__pycache__/__init__.cpython-313.pyc +0 -0
  40. src/similarity_model/__pycache__/embedding_engine.cpython-313.pyc +0 -0
  41. src/similarity_model/__pycache__/feature_similarity.cpython-313.pyc +0 -0
  42. src/similarity_model/__pycache__/hybrid_ranker.cpython-313.pyc +0 -0
  43. src/similarity_model/__pycache__/preprocessing.cpython-313.pyc +0 -0
  44. src/similarity_model/__pycache__/semantic_search.cpython-313.pyc +0 -0
  45. src/similarity_model/__pycache__/similarity_engine.cpython-313.pyc +0 -0
  46. src/similarity_model/embedding_engine.py +1 -2
  47. src/similarity_model/evaluation.py +0 -34
  48. src/similarity_model/feature_similarity.py +1 -1
  49. src/similarity_model/hybrid_ranker.py +23 -102
  50. src/similarity_model/semantic_search.py +2 -2
.env CHANGED
@@ -1,2 +1,2 @@
1
- GEMINI_API_KEY=AIzaSyAkFsaN3BKoSQmRW4FzTahhZXbq-ldsDZ4
2
  GEMINI_MODEL_NAME=gemini-2.5-flash
 
1
+ GEMINI_API_KEY=AQ.Ab8RN6K-wFpuRR3mv7AUv7p6QqXf98Dlth0IRF3N42UIfjCEbw
2
  GEMINI_MODEL_NAME=gemini-2.5-flash
src/recommendation_engine/__init__.py CHANGED
@@ -1,2 +1,17 @@
1
- # __init__.py
2
- # empty file is totally fine
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from .chatbot_engine import chatbot
2
+ from .command_handler import handle_command, is_command
3
+ from .context_builder import build_project_context, build_domain_context, build_architecture_hints
4
+ from .feature_generator import generate_features
5
+ from .full_project_generator import generate_full_project
6
+ from .idea_generator import generate_ideas
7
+ from .intent_classifier import detect_intent, classify_with_llm
8
+ from .llm_client import generate_text, generate_list, LLMProviderError
9
+ from .llm_router import analyze_user_input
10
+ from .local_llm import generate_local
11
+ from .memory_store import get_user_memory, save_user_memory, get_all_chats, switch_chat, clear_user_memory
12
+ from .novelty_checker import score_feature_novelty, is_feature_novel, is_idea_novel
13
+ from .prompt_builder import build_chat_prompt, build_feature_prompt, build_idea_prompt, build_description_prompt, build_full_project_prompt
14
+ from .response_formatter import format_response, format_full_project
15
+ from .semantic_intent_classifier import detect_intent_semantic
16
+ from .state_manager import update_state, reset_for_new_idea
17
+ from .validator import validate_generated_list, filter_items
src/recommendation_engine/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/__init__.cpython-313.pyc and b/src/recommendation_engine/__pycache__/__init__.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/chatbot_engine.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/chatbot_engine.cpython-313.pyc and b/src/recommendation_engine/__pycache__/chatbot_engine.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/command_handler.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/command_handler.cpython-313.pyc and b/src/recommendation_engine/__pycache__/command_handler.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/config.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/config.cpython-313.pyc and b/src/recommendation_engine/__pycache__/config.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/context_builder.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/context_builder.cpython-313.pyc and b/src/recommendation_engine/__pycache__/context_builder.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/feature_generator.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/feature_generator.cpython-313.pyc and b/src/recommendation_engine/__pycache__/feature_generator.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/full_project_generator.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/full_project_generator.cpython-313.pyc and b/src/recommendation_engine/__pycache__/full_project_generator.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/idea_generator.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/idea_generator.cpython-313.pyc and b/src/recommendation_engine/__pycache__/idea_generator.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/llm_client.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/llm_client.cpython-313.pyc and b/src/recommendation_engine/__pycache__/llm_client.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/llm_router.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/llm_router.cpython-313.pyc and b/src/recommendation_engine/__pycache__/llm_router.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/memory_store.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/memory_store.cpython-313.pyc and b/src/recommendation_engine/__pycache__/memory_store.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/novelty_checker.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/novelty_checker.cpython-313.pyc and b/src/recommendation_engine/__pycache__/novelty_checker.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/prompt_builder.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/prompt_builder.cpython-313.pyc and b/src/recommendation_engine/__pycache__/prompt_builder.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/response_formatter.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/response_formatter.cpython-313.pyc and b/src/recommendation_engine/__pycache__/response_formatter.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/state_manager.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/state_manager.cpython-313.pyc and b/src/recommendation_engine/__pycache__/state_manager.cpython-313.pyc differ
 
src/recommendation_engine/__pycache__/validator.cpython-313.pyc CHANGED
Binary files a/src/recommendation_engine/__pycache__/validator.cpython-313.pyc and b/src/recommendation_engine/__pycache__/validator.cpython-313.pyc differ
 
src/recommendation_engine/chatbot_engine.py CHANGED
@@ -1,5 +1,3 @@
1
- # src.recommendation_engine.chatbot_engine.py
2
-
3
  from src.recommendation_engine.memory_store import (
4
  get_user_memory,
5
  save_user_memory,
@@ -28,17 +26,12 @@ from src.recommendation_engine.full_project_generator import (
28
 
29
  import re
30
 
31
-
32
- # =====================================================
33
- # Helpers
34
- # =====================================================
35
  def extract_number(text: str, default=5):
36
 
37
  nums = re.findall(r"\d+", text)
38
 
39
  return min(int(nums[0]), 20) if nums else default
40
 
41
-
42
  def is_weak_project_title(title: str) -> bool:
43
 
44
  if not title:
@@ -48,7 +41,7 @@ def is_weak_project_title(title: str) -> bool:
48
 
49
  words = title.split()
50
 
51
- # too short
52
  if len(words) < 4:
53
  return True
54
 
@@ -72,7 +65,6 @@ def is_weak_project_title(title: str) -> bool:
72
 
73
  return len(words) < 3
74
 
75
-
76
  def is_generic_project_reference(text: str) -> bool:
77
 
78
  text = text.strip().lower()
@@ -103,17 +95,17 @@ def looks_like_real_project_title(title: str) -> bool:
103
 
104
  words = title.split()
105
 
106
- # minimum words
107
  if len(words) < 2:
108
  return False
109
 
110
- # too repetitive
111
  unique_ratio = len(set(words)) / len(words)
112
 
113
  if unique_ratio < 0.5:
114
  return False
115
 
116
- # gibberish detection
117
  nonsense_patterns = [
118
  "asd",
119
  "qwe",
@@ -125,10 +117,9 @@ def looks_like_real_project_title(title: str) -> bool:
125
 
126
  lowered = title.lower()
127
 
128
-
129
- # =========================
130
- # QUESTION / NORMAL CHAT FILTER
131
- # =========================
132
  question_starts = (
133
  "how ",
134
  "what ",
@@ -150,10 +141,10 @@ def looks_like_real_project_title(title: str) -> bool:
150
  if p in lowered:
151
  return False
152
 
153
- # at least one meaningful keyword
154
  keywords = {
155
 
156
- # AI / Systems
157
  "management",
158
  "analysis",
159
  "detection",
@@ -172,7 +163,7 @@ def looks_like_real_project_title(title: str) -> bool:
172
  "authentication",
173
  "optimization",
174
 
175
- # domains
176
  "healthcare",
177
  "fintech",
178
  "education",
@@ -184,7 +175,7 @@ def looks_like_real_project_title(title: str) -> bool:
184
  "agriculture",
185
  "transport",
186
 
187
- # technologies
188
  "ai",
189
  "iot",
190
  "blockchain",
@@ -193,7 +184,7 @@ def looks_like_real_project_title(title: str) -> bool:
193
  "vision",
194
  "embedded",
195
 
196
- # project words
197
  "system",
198
  "platform",
199
  "application",
@@ -208,7 +199,6 @@ def looks_like_real_project_title(title: str) -> bool:
208
 
209
  return True
210
 
211
-
212
  FOLLOWUP_WORDS = [
213
  "another",
214
  "more",
@@ -219,9 +209,6 @@ FOLLOWUP_WORDS = [
219
  "another features"
220
  ]
221
 
222
- # =====================================================
223
- # RESPONSE FINALIZER
224
- # =====================================================
225
  def finalize_response(
226
  user_input,
227
  response,
@@ -249,19 +236,18 @@ def finalize_response(
249
 
250
  return response
251
 
252
-
253
  def is_gibberish_text(text: str) -> bool:
254
 
255
  text = text.strip().lower()
256
 
257
- # allow menu choices
258
  if text in {"1", "2", "3"}:
259
  return False
260
 
261
- # very short non-numeric random text
262
  if len(text) < 3:
263
 
264
- # allow short meaningful commands
265
  allowed_short = {
266
  "hi",
267
  "hey",
@@ -297,7 +283,7 @@ def is_gibberish_text(text: str) -> bool:
297
 
298
  words = text.split()
299
 
300
- # repeated same words
301
  if len(words) >= 3:
302
 
303
  unique_ratio = len(set(words)) / len(words)
@@ -313,7 +299,7 @@ def is_project_related(text: str) -> bool:
313
 
314
  keywords = [
315
 
316
- # project words
317
  "project",
318
  "system",
319
  "platform",
@@ -323,7 +309,7 @@ def is_project_related(text: str) -> bool:
323
  "dashboard",
324
  "management",
325
 
326
- # technical
327
  "ai",
328
  "ml",
329
  "machine learning",
@@ -338,7 +324,7 @@ def is_project_related(text: str) -> bool:
338
  "database",
339
  "api",
340
 
341
- # actions
342
  "generate",
343
  "feature",
344
  "features",
@@ -349,7 +335,7 @@ def is_project_related(text: str) -> bool:
349
  "technologies",
350
  "architecture",
351
 
352
- # domains
353
  "healthcare",
354
  "education",
355
  "fintech",
@@ -366,17 +352,13 @@ def is_project_related(text: str) -> bool:
366
  for keyword in keywords
367
  )
368
 
369
- # =====================================================
370
- # MAIN CHATBOT
371
- # =====================================================
372
  def chatbot(user_id: str, user_input: str):
373
 
374
  text = user_input.lower().strip()
375
 
376
-
377
- # =========================================
378
- # EXPLICIT IDEA REQUESTS
379
- # =========================================
380
  explicit_idea_requests = [
381
  "idea",
382
  "new idea",
@@ -407,14 +389,14 @@ def chatbot(user_id: str, user_input: str):
407
  "🎯 Specify a domain (AI, healthcare, fintech...)"
408
  )
409
 
410
- # =================================================
411
- # ALLOW MENU NUMBERS
412
- # =================================================
413
  if text in {"1", "2", "3"}:
414
  pass
415
- # =================================================
416
- # GIBBERISH DETECTION
417
- # =================================================
418
  if is_gibberish_text(text):
419
 
420
  return (
@@ -425,22 +407,22 @@ def chatbot(user_id: str, user_input: str):
425
  "- improve my project\n"
426
  "- suggest technologies for fintech app"
427
  )
428
- # =================================================
429
- # COMMANDS
430
- # =================================================
431
  if is_command(user_input):
432
  return handle_command(user_input)
433
 
434
- # =================================================
435
- # MEMORY
436
- # =================================================
437
  memory = get_user_memory(user_id)
438
 
439
  history = memory.get("history", [])
440
 
441
  state = memory.get("state") or default_state()
442
 
443
- # ensure keys
444
  state.setdefault("menu_mode", False)
445
  state.setdefault("selected_option", None)
446
  state.setdefault("waiting_for_domain", False)
@@ -453,9 +435,9 @@ def chatbot(user_id: str, user_input: str):
453
  state.setdefault("waiting_for_full_project_domain", False)
454
  state.setdefault("waiting_for_full_project_selection", False)
455
 
456
- # =================================================
457
- # GREETING
458
- # =================================================
459
  if text in ["","hi", "hello", "hey"]:
460
 
461
  save_user_memory(user_id, {
@@ -477,9 +459,9 @@ def chatbot(user_id: str, user_input: str):
477
  "- improve my graduation project\n"
478
  "- suggest technologies for fintech app"
479
  )
480
- # =================================================
481
- # FULL PROJECT STARTER
482
- # =================================================
483
  if any(x in text for x in [
484
 
485
  "generate full project",
@@ -491,12 +473,12 @@ def chatbot(user_id: str, user_input: str):
491
 
492
  ]):
493
 
494
- # ============================================
495
- # IF PROJECT TITLE ALREADY EXISTS
496
- # ============================================
497
  if state.get("project_title"):
498
 
499
- # Generate features automatically if missing
500
  if not state.get("features"):
501
 
502
  feature_result = generate_features(
@@ -517,7 +499,7 @@ def chatbot(user_id: str, user_input: str):
517
  []
518
  )
519
 
520
- # Generate full project
521
  result = generate_full_project(
522
 
523
  title=state.get("project_title"),
@@ -586,9 +568,9 @@ def chatbot(user_id: str, user_input: str):
586
  user_id
587
  )
588
 
589
- # ============================================
590
- # NO PROJECT TITLE → ASK FOR DOMAIN
591
- # ============================================
592
  state["waiting_for_full_project_domain"] = True
593
 
594
  save_user_memory(user_id, {
@@ -604,9 +586,9 @@ def chatbot(user_id: str, user_input: str):
604
  "- education\n"
605
  "- fintech"
606
  )
607
- # =================================================
608
- # MENU HANDLER
609
- # =================================================
610
  if state.get("menu_mode"):
611
 
612
  if text in ["1", "2", "3"]:
@@ -673,9 +655,9 @@ def chatbot(user_id: str, user_input: str):
673
 
674
  state["menu_mode"] = False
675
 
676
- # =================================================
677
- # PROJECT ACTION HANDLER
678
- # =================================================
679
  if state.get("waiting_for_project_action"):
680
 
681
  feature_words = [
@@ -706,9 +688,9 @@ def chatbot(user_id: str, user_input: str):
706
  "final project"
707
  ]
708
 
709
- # =========================================
710
- # FEATURE GENERATION
711
- # =========================================
712
  if any(w in text for w in feature_words):
713
 
714
  state["waiting_for_project_action"] = False
@@ -720,9 +702,9 @@ def chatbot(user_id: str, user_input: str):
720
 
721
  return chatbot(user_id, "generate features")
722
 
723
- # =========================================
724
- # PROJECT DISCUSSION
725
- # =========================================
726
  elif any(w in text for w in chat_words):
727
 
728
  state["waiting_for_project_action"] = False
@@ -740,9 +722,9 @@ def chatbot(user_id: str, user_input: str):
740
  "You can now discuss or improve the project."
741
  )
742
 
743
- # =========================================
744
- # FEATURE GENERATION
745
- # =========================================
746
  if any(w in text for w in feature_words):
747
 
748
  state["waiting_for_project_action"] = False
@@ -754,10 +736,9 @@ def chatbot(user_id: str, user_input: str):
754
 
755
  return chatbot(user_id, "generate features")
756
 
757
-
758
- # =========================================
759
- # PROJECT DISCUSSION
760
- # =========================================
761
  elif any(w in text for w in chat_words):
762
 
763
  state["waiting_for_project_action"] = False
@@ -775,15 +756,14 @@ def chatbot(user_id: str, user_input: str):
775
  "You can now discuss or improve the project."
776
  )
777
 
778
-
779
- # =========================================
780
- # FULL PROJECT GENERATION
781
- # =========================================
782
  elif any(w in text for w in full_project_words):
783
 
784
  state["waiting_for_project_action"] = False
785
 
786
- # Generate features first if missing
787
  if not state.get("features"):
788
 
789
  feature_result = generate_features(
@@ -804,7 +784,7 @@ def chatbot(user_id: str, user_input: str):
804
  []
805
  )
806
 
807
- # Generate full project
808
  result = generate_full_project(
809
 
810
  title=state.get("project_title"),
@@ -873,9 +853,9 @@ def chatbot(user_id: str, user_input: str):
873
  user_id
874
  )
875
 
876
- # =================================================
877
- # WEAK TITLE IMPROVEMENT HANDLER
878
- # =================================================
879
  if state.get("weak_title_candidate"):
880
 
881
  improve_words = [
@@ -933,16 +913,16 @@ def chatbot(user_id: str, user_input: str):
933
  )
934
 
935
 
936
- # =================================================
937
- # WAITING FOR FEATURE TITLE
938
- # =================================================
939
  if state.get("waiting_for_feature_title"):
940
 
941
  state["waiting_for_feature_title"] = False
942
 
943
  possible_title = user_input.strip()
944
 
945
- # weak title
946
  if is_weak_project_title(possible_title):
947
 
948
  state["weak_title_candidate"] = possible_title
@@ -962,7 +942,7 @@ def chatbot(user_id: str, user_input: str):
962
  "2️⃣ Enter another title"
963
  )
964
 
965
- # valid title
966
  state["project_title"] = possible_title
967
 
968
  save_user_memory(user_id, {
@@ -972,11 +952,11 @@ def chatbot(user_id: str, user_input: str):
972
 
973
  return chatbot(user_id, "generate features")
974
 
975
- # =================================================
976
- # DIRECT FEATURE TITLE EXTRACTION
977
- # =================================================
978
  feature_patterns = [
979
- # standard
980
  "generate features for",
981
  "generate feature for",
982
  "features for",
@@ -984,36 +964,36 @@ def chatbot(user_id: str, user_input: str):
984
  "create features for",
985
  "suggest features for",
986
 
987
- # casual
988
  "give me features for",
989
  "show features for",
990
  "i need features for",
991
  "can you generate features for",
992
  "can you suggest features for",
993
 
994
- # project related
995
  "add features to",
996
  "improve features for",
997
  "feature ideas for",
998
  "smart features for",
999
 
1000
- # short forms
1001
  "features:",
1002
  "project:",
1003
  "idea:",
1004
 
1005
- # typo tolerance
1006
  "gen features for",
1007
  "generate faetures for",
1008
  "fetures for",
1009
 
1010
- # direct requests
1011
  "make features for",
1012
  "build features for",
1013
  "what features for",
1014
  "best features for",
1015
 
1016
- # ai chatbot style
1017
  "i have project",
1018
  "my project is",
1019
  "project title is",
@@ -1041,9 +1021,9 @@ def chatbot(user_id: str, user_input: str):
1041
 
1042
  break
1043
 
1044
- # =================================================
1045
- # SIMPLE PROJECT TITLE DETECTION (BEFORE LLM)
1046
- # =================================================
1047
  if analysis is None:
1048
 
1049
  blocked_starts = (
@@ -1078,7 +1058,7 @@ def chatbot(user_id: str, user_input: str):
1078
 
1079
  command_phrases = {
1080
 
1081
- # idea commands
1082
  "new idea",
1083
  "another idea",
1084
  "generate idea",
@@ -1087,18 +1067,18 @@ def chatbot(user_id: str, user_input: str):
1087
  "more ideas",
1088
  "idea",
1089
 
1090
- # feature commands
1091
  "generate features",
1092
  "generate feature",
1093
  "more features",
1094
  "features",
1095
 
1096
- # full project
1097
  "generate full project",
1098
  "full project",
1099
  "complete project",
1100
 
1101
- # generic commands
1102
  "another",
1103
  "again",
1104
  "more",
@@ -1111,7 +1091,7 @@ def chatbot(user_id: str, user_input: str):
1111
 
1112
  clean_text = text.strip()
1113
 
1114
- # detect short titles directly
1115
  words = clean_text.lower().split()
1116
 
1117
  if (
@@ -1133,9 +1113,9 @@ def chatbot(user_id: str, user_input: str):
1133
  else:
1134
  analysis = analyze_user_input(user_input, state)
1135
 
1136
- # =================================================
1137
- # IDEA SELECTION DETECTION
1138
- # =================================================
1139
  existing_ideas = state.get("ideas", [])
1140
 
1141
  normalized_input = text.strip().lower()
@@ -1146,7 +1126,7 @@ def chatbot(user_id: str, user_input: str):
1146
 
1147
  input_words = set(normalized_input.split())
1148
 
1149
- # ignore extremely short inputs
1150
  if len(input_words) < 2:
1151
  continue
1152
 
@@ -1202,10 +1182,9 @@ Choose what you want:
1202
  user_id
1203
  )
1204
 
1205
-
1206
- # =====================================
1207
- # NORMAL IDEA SELECTION
1208
- # =====================================
1209
  state["project_title"] = idea
1210
  state["waiting_for_project_action"] = True
1211
  state["project_chat_mode"] = False
@@ -1225,25 +1204,25 @@ Choose what you want:
1225
  "- discuss project"
1226
  )
1227
 
1228
- # =================================================
1229
- # FOLLOW-UP CONTINUATION
1230
- # =================================================
1231
  if any(w in text for w in FOLLOWUP_WORDS):
1232
 
1233
- # continue ideas
1234
  if state.get("last_action") == "idea":
1235
 
1236
  analysis["intent"] = "idea"
1237
  analysis["domain"] = state.get("domain")
1238
 
1239
- # continue features
1240
  elif state.get("last_action") == "feature":
1241
 
1242
  analysis["intent"] = "feature"
1243
 
1244
- # =================================================
1245
- # DOMAIN FOLLOW-UP
1246
- # =================================================
1247
  if state.get("waiting_for_domain"):
1248
 
1249
  detected = extract_domain(user_input)
@@ -1268,9 +1247,9 @@ Choose what you want:
1268
 
1269
  user_project = analysis.get("project_title")
1270
 
1271
- # =================================================
1272
- # FULL PROJECT DOMAIN HANDLER
1273
- # =================================================
1274
  if state.get("waiting_for_full_project_domain"):
1275
 
1276
  domain = extract_domain(user_input)
@@ -1310,15 +1289,15 @@ Choose what you want:
1310
  for idea in ideas
1311
  )
1312
  )
1313
- # =================================================
1314
- # FALLBACK DOMAIN
1315
- # =================================================
1316
  if not domain:
1317
  domain = extract_domain(text)
1318
 
1319
- # =================================================
1320
- # DOMAIN-ONLY INPUT DETECTION
1321
- # =================================================
1322
  full_project_commands = [
1323
  "fill all",
1324
  "fill all columns",
@@ -1340,9 +1319,9 @@ Choose what you want:
1340
  and not any(cmd in text for cmd in full_project_commands)
1341
  ):
1342
  intent = "idea"
1343
- # =================================================
1344
- # FALLBACK PROJECT TITLE DETECTION
1345
- # =================================================
1346
  if (
1347
  not user_project
1348
  and intent == "chat"
@@ -1364,7 +1343,7 @@ Choose what you want:
1364
  "more ideas",
1365
 
1366
  ]
1367
- # reject obvious commands/questions
1368
  blocked_starts = (
1369
  "how ",
1370
  "what ",
@@ -1411,16 +1390,16 @@ Choose what you want:
1411
  )
1412
  ):
1413
 
1414
- # treat short non-question text as project title
1415
  user_project = user_input.strip()
1416
- # =================================================
1417
- # USER PROVIDED PROJECT
1418
- # =================================================
1419
  if user_project:
1420
 
1421
- # =============================================
1422
- # WEAK TITLE VALIDATION
1423
- # =============================================
1424
  if is_weak_project_title(user_project):
1425
 
1426
  state["weak_title_candidate"] = user_project
@@ -1465,9 +1444,9 @@ Choose what you want:
1465
  "- discuss project\n"
1466
  "- generate full project"
1467
  )
1468
- # =================================================
1469
- # FEATURE REQUEST WITHOUT TITLE
1470
- # =================================================
1471
  feature_request_phrases = [
1472
  "generate features",
1473
  "generate feature",
@@ -1498,9 +1477,9 @@ Choose what you want:
1498
  "- AI-powered smart library recommendation system\n"
1499
  "- Smart hospital emergency response platform"
1500
  )
1501
- # =================================================
1502
- # IDEA GENERATION
1503
- # =================================================
1504
  if intent == "idea":
1505
 
1506
  if not domain:
@@ -1541,14 +1520,14 @@ Choose what you want:
1541
  user_id
1542
  )
1543
 
1544
- # =================================================
1545
- # FEATURE GENERATION
1546
- # =================================================
1547
  elif intent == "feature":
1548
 
1549
- # =========================================
1550
- # DIRECT PROJECT TITLE FROM USER INPUT
1551
- # =========================================
1552
  if analysis.get("project_title"):
1553
 
1554
  state["project_title"] = (
@@ -1579,8 +1558,8 @@ Choose what you want:
1579
  []
1580
  )
1581
 
1582
- # IMPORTANT:
1583
- # Replace old features completely
1584
  state["features"] = features
1585
 
1586
  state["last_action"] = "feature"
@@ -1594,11 +1573,10 @@ Choose what you want:
1594
  user_id
1595
  )
1596
 
1597
-
1598
 
1599
- # =================================================
1600
- # FULL PROJECT GENERATION
1601
- # =================================================
1602
  elif any(x in text for x in [
1603
 
1604
  "fill all",
@@ -1692,9 +1670,9 @@ Choose what you want:
1692
  user_id
1693
  )
1694
 
1695
- # =================================================
1696
- # POSSIBLE NEW PROJECT DETECTION
1697
- # =================================================
1698
  elif (
1699
  state.get("project_title")
1700
  and len(text.split()) >= 3
@@ -1716,7 +1694,7 @@ Choose what you want:
1716
  / max(1, len(new_input_words))
1717
  )
1718
 
1719
- # low overlap may indicate new project/topic
1720
  if (
1721
  overlap_ratio < 0.2
1722
  and analysis.get("intent") == "chat"
@@ -1765,9 +1743,9 @@ User:
1765
  state,
1766
  user_id
1767
  )
1768
- # =================================================
1769
- # NON PROJECT FILTER
1770
- # =================================================
1771
  if not is_project_related(text):
1772
 
1773
  return (
@@ -1778,9 +1756,9 @@ User:
1778
  "- suggest technologies for fintech app\n"
1779
  "- improve my graduation project"
1780
  )
1781
- # =================================================
1782
- # GENERAL CHAT
1783
- # =================================================
1784
  else:
1785
 
1786
  system_prompt = build_chat_prompt(state)
@@ -1817,5 +1795,3 @@ User:
1817
  state,
1818
  user_id
1819
  )
1820
-
1821
-
 
 
 
1
  from src.recommendation_engine.memory_store import (
2
  get_user_memory,
3
  save_user_memory,
 
26
 
27
  import re
28
 
 
 
 
 
29
  def extract_number(text: str, default=5):
30
 
31
  nums = re.findall(r"\d+", text)
32
 
33
  return min(int(nums[0]), 20) if nums else default
34
 
 
35
  def is_weak_project_title(title: str) -> bool:
36
 
37
  if not title:
 
41
 
42
  words = title.split()
43
 
44
+
45
  if len(words) < 4:
46
  return True
47
 
 
65
 
66
  return len(words) < 3
67
 
 
68
  def is_generic_project_reference(text: str) -> bool:
69
 
70
  text = text.strip().lower()
 
95
 
96
  words = title.split()
97
 
98
+
99
  if len(words) < 2:
100
  return False
101
 
102
+
103
  unique_ratio = len(set(words)) / len(words)
104
 
105
  if unique_ratio < 0.5:
106
  return False
107
 
108
+
109
  nonsense_patterns = [
110
  "asd",
111
  "qwe",
 
117
 
118
  lowered = title.lower()
119
 
120
+
121
+
122
+
 
123
  question_starts = (
124
  "how ",
125
  "what ",
 
141
  if p in lowered:
142
  return False
143
 
144
+
145
  keywords = {
146
 
147
+
148
  "management",
149
  "analysis",
150
  "detection",
 
163
  "authentication",
164
  "optimization",
165
 
166
+
167
  "healthcare",
168
  "fintech",
169
  "education",
 
175
  "agriculture",
176
  "transport",
177
 
178
+
179
  "ai",
180
  "iot",
181
  "blockchain",
 
184
  "vision",
185
  "embedded",
186
 
187
+
188
  "system",
189
  "platform",
190
  "application",
 
199
 
200
  return True
201
 
 
202
  FOLLOWUP_WORDS = [
203
  "another",
204
  "more",
 
209
  "another features"
210
  ]
211
 
 
 
 
212
  def finalize_response(
213
  user_input,
214
  response,
 
236
 
237
  return response
238
 
 
239
  def is_gibberish_text(text: str) -> bool:
240
 
241
  text = text.strip().lower()
242
 
243
+
244
  if text in {"1", "2", "3"}:
245
  return False
246
 
247
+
248
  if len(text) < 3:
249
 
250
+
251
  allowed_short = {
252
  "hi",
253
  "hey",
 
283
 
284
  words = text.split()
285
 
286
+
287
  if len(words) >= 3:
288
 
289
  unique_ratio = len(set(words)) / len(words)
 
299
 
300
  keywords = [
301
 
302
+
303
  "project",
304
  "system",
305
  "platform",
 
309
  "dashboard",
310
  "management",
311
 
312
+
313
  "ai",
314
  "ml",
315
  "machine learning",
 
324
  "database",
325
  "api",
326
 
327
+
328
  "generate",
329
  "feature",
330
  "features",
 
335
  "technologies",
336
  "architecture",
337
 
338
+
339
  "healthcare",
340
  "education",
341
  "fintech",
 
352
  for keyword in keywords
353
  )
354
 
 
 
 
355
  def chatbot(user_id: str, user_input: str):
356
 
357
  text = user_input.lower().strip()
358
 
359
+
360
+
361
+
 
362
  explicit_idea_requests = [
363
  "idea",
364
  "new idea",
 
389
  "🎯 Specify a domain (AI, healthcare, fintech...)"
390
  )
391
 
392
+
393
+
394
+
395
  if text in {"1", "2", "3"}:
396
  pass
397
+
398
+
399
+
400
  if is_gibberish_text(text):
401
 
402
  return (
 
407
  "- improve my project\n"
408
  "- suggest technologies for fintech app"
409
  )
410
+
411
+
412
+
413
  if is_command(user_input):
414
  return handle_command(user_input)
415
 
416
+
417
+
418
+
419
  memory = get_user_memory(user_id)
420
 
421
  history = memory.get("history", [])
422
 
423
  state = memory.get("state") or default_state()
424
 
425
+
426
  state.setdefault("menu_mode", False)
427
  state.setdefault("selected_option", None)
428
  state.setdefault("waiting_for_domain", False)
 
435
  state.setdefault("waiting_for_full_project_domain", False)
436
  state.setdefault("waiting_for_full_project_selection", False)
437
 
438
+
439
+
440
+
441
  if text in ["","hi", "hello", "hey"]:
442
 
443
  save_user_memory(user_id, {
 
459
  "- improve my graduation project\n"
460
  "- suggest technologies for fintech app"
461
  )
462
+
463
+
464
+
465
  if any(x in text for x in [
466
 
467
  "generate full project",
 
473
 
474
  ]):
475
 
476
+
477
+
478
+
479
  if state.get("project_title"):
480
 
481
+
482
  if not state.get("features"):
483
 
484
  feature_result = generate_features(
 
499
  []
500
  )
501
 
502
+
503
  result = generate_full_project(
504
 
505
  title=state.get("project_title"),
 
568
  user_id
569
  )
570
 
571
+
572
+
573
+
574
  state["waiting_for_full_project_domain"] = True
575
 
576
  save_user_memory(user_id, {
 
586
  "- education\n"
587
  "- fintech"
588
  )
589
+
590
+
591
+
592
  if state.get("menu_mode"):
593
 
594
  if text in ["1", "2", "3"]:
 
655
 
656
  state["menu_mode"] = False
657
 
658
+
659
+
660
+
661
  if state.get("waiting_for_project_action"):
662
 
663
  feature_words = [
 
688
  "final project"
689
  ]
690
 
691
+
692
+
693
+
694
  if any(w in text for w in feature_words):
695
 
696
  state["waiting_for_project_action"] = False
 
702
 
703
  return chatbot(user_id, "generate features")
704
 
705
+
706
+
707
+
708
  elif any(w in text for w in chat_words):
709
 
710
  state["waiting_for_project_action"] = False
 
722
  "You can now discuss or improve the project."
723
  )
724
 
725
+
726
+
727
+
728
  if any(w in text for w in feature_words):
729
 
730
  state["waiting_for_project_action"] = False
 
736
 
737
  return chatbot(user_id, "generate features")
738
 
739
+
740
+
741
+
 
742
  elif any(w in text for w in chat_words):
743
 
744
  state["waiting_for_project_action"] = False
 
756
  "You can now discuss or improve the project."
757
  )
758
 
759
+
760
+
761
+
 
762
  elif any(w in text for w in full_project_words):
763
 
764
  state["waiting_for_project_action"] = False
765
 
766
+
767
  if not state.get("features"):
768
 
769
  feature_result = generate_features(
 
784
  []
785
  )
786
 
787
+
788
  result = generate_full_project(
789
 
790
  title=state.get("project_title"),
 
853
  user_id
854
  )
855
 
856
+
857
+
858
+
859
  if state.get("weak_title_candidate"):
860
 
861
  improve_words = [
 
913
  )
914
 
915
 
916
+
917
+
918
+
919
  if state.get("waiting_for_feature_title"):
920
 
921
  state["waiting_for_feature_title"] = False
922
 
923
  possible_title = user_input.strip()
924
 
925
+
926
  if is_weak_project_title(possible_title):
927
 
928
  state["weak_title_candidate"] = possible_title
 
942
  "2️⃣ Enter another title"
943
  )
944
 
945
+
946
  state["project_title"] = possible_title
947
 
948
  save_user_memory(user_id, {
 
952
 
953
  return chatbot(user_id, "generate features")
954
 
955
+
956
+
957
+
958
  feature_patterns = [
959
+
960
  "generate features for",
961
  "generate feature for",
962
  "features for",
 
964
  "create features for",
965
  "suggest features for",
966
 
967
+
968
  "give me features for",
969
  "show features for",
970
  "i need features for",
971
  "can you generate features for",
972
  "can you suggest features for",
973
 
974
+
975
  "add features to",
976
  "improve features for",
977
  "feature ideas for",
978
  "smart features for",
979
 
980
+
981
  "features:",
982
  "project:",
983
  "idea:",
984
 
985
+
986
  "gen features for",
987
  "generate faetures for",
988
  "fetures for",
989
 
990
+
991
  "make features for",
992
  "build features for",
993
  "what features for",
994
  "best features for",
995
 
996
+
997
  "i have project",
998
  "my project is",
999
  "project title is",
 
1021
 
1022
  break
1023
 
1024
+
1025
+
1026
+
1027
  if analysis is None:
1028
 
1029
  blocked_starts = (
 
1058
 
1059
  command_phrases = {
1060
 
1061
+
1062
  "new idea",
1063
  "another idea",
1064
  "generate idea",
 
1067
  "more ideas",
1068
  "idea",
1069
 
1070
+
1071
  "generate features",
1072
  "generate feature",
1073
  "more features",
1074
  "features",
1075
 
1076
+
1077
  "generate full project",
1078
  "full project",
1079
  "complete project",
1080
 
1081
+
1082
  "another",
1083
  "again",
1084
  "more",
 
1091
 
1092
  clean_text = text.strip()
1093
 
1094
+
1095
  words = clean_text.lower().split()
1096
 
1097
  if (
 
1113
  else:
1114
  analysis = analyze_user_input(user_input, state)
1115
 
1116
+
1117
+
1118
+
1119
  existing_ideas = state.get("ideas", [])
1120
 
1121
  normalized_input = text.strip().lower()
 
1126
 
1127
  input_words = set(normalized_input.split())
1128
 
1129
+
1130
  if len(input_words) < 2:
1131
  continue
1132
 
 
1182
  user_id
1183
  )
1184
 
1185
+
1186
+
1187
+
 
1188
  state["project_title"] = idea
1189
  state["waiting_for_project_action"] = True
1190
  state["project_chat_mode"] = False
 
1204
  "- discuss project"
1205
  )
1206
 
1207
+
1208
+
1209
+
1210
  if any(w in text for w in FOLLOWUP_WORDS):
1211
 
1212
+
1213
  if state.get("last_action") == "idea":
1214
 
1215
  analysis["intent"] = "idea"
1216
  analysis["domain"] = state.get("domain")
1217
 
1218
+
1219
  elif state.get("last_action") == "feature":
1220
 
1221
  analysis["intent"] = "feature"
1222
 
1223
+
1224
+
1225
+
1226
  if state.get("waiting_for_domain"):
1227
 
1228
  detected = extract_domain(user_input)
 
1247
 
1248
  user_project = analysis.get("project_title")
1249
 
1250
+
1251
+
1252
+
1253
  if state.get("waiting_for_full_project_domain"):
1254
 
1255
  domain = extract_domain(user_input)
 
1289
  for idea in ideas
1290
  )
1291
  )
1292
+
1293
+
1294
+
1295
  if not domain:
1296
  domain = extract_domain(text)
1297
 
1298
+
1299
+
1300
+
1301
  full_project_commands = [
1302
  "fill all",
1303
  "fill all columns",
 
1319
  and not any(cmd in text for cmd in full_project_commands)
1320
  ):
1321
  intent = "idea"
1322
+
1323
+
1324
+
1325
  if (
1326
  not user_project
1327
  and intent == "chat"
 
1343
  "more ideas",
1344
 
1345
  ]
1346
+
1347
  blocked_starts = (
1348
  "how ",
1349
  "what ",
 
1390
  )
1391
  ):
1392
 
1393
+
1394
  user_project = user_input.strip()
1395
+
1396
+
1397
+
1398
  if user_project:
1399
 
1400
+
1401
+
1402
+
1403
  if is_weak_project_title(user_project):
1404
 
1405
  state["weak_title_candidate"] = user_project
 
1444
  "- discuss project\n"
1445
  "- generate full project"
1446
  )
1447
+
1448
+
1449
+
1450
  feature_request_phrases = [
1451
  "generate features",
1452
  "generate feature",
 
1477
  "- AI-powered smart library recommendation system\n"
1478
  "- Smart hospital emergency response platform"
1479
  )
1480
+
1481
+
1482
+
1483
  if intent == "idea":
1484
 
1485
  if not domain:
 
1520
  user_id
1521
  )
1522
 
1523
+
1524
+
1525
+
1526
  elif intent == "feature":
1527
 
1528
+
1529
+
1530
+
1531
  if analysis.get("project_title"):
1532
 
1533
  state["project_title"] = (
 
1558
  []
1559
  )
1560
 
1561
+
1562
+
1563
  state["features"] = features
1564
 
1565
  state["last_action"] = "feature"
 
1573
  user_id
1574
  )
1575
 
 
1576
 
1577
+
1578
+
1579
+
1580
  elif any(x in text for x in [
1581
 
1582
  "fill all",
 
1670
  user_id
1671
  )
1672
 
1673
+
1674
+
1675
+
1676
  elif (
1677
  state.get("project_title")
1678
  and len(text.split()) >= 3
 
1694
  / max(1, len(new_input_words))
1695
  )
1696
 
1697
+
1698
  if (
1699
  overlap_ratio < 0.2
1700
  and analysis.get("intent") == "chat"
 
1743
  state,
1744
  user_id
1745
  )
1746
+
1747
+
1748
+
1749
  if not is_project_related(text):
1750
 
1751
  return (
 
1756
  "- suggest technologies for fintech app\n"
1757
  "- improve my graduation project"
1758
  )
1759
+
1760
+
1761
+
1762
  else:
1763
 
1764
  system_prompt = build_chat_prompt(state)
 
1795
  state,
1796
  user_id
1797
  )
 
 
src/recommendation_engine/command_handler.py CHANGED
@@ -1,11 +1,5 @@
1
- # src/recommendation_engine/command_handler.py
2
-
3
  import re
4
 
5
-
6
- # =====================================================
7
- # Command Detection (Improved)
8
- # =====================================================
9
  def is_command(text: str) -> bool:
10
  """
11
  Detect if input is a system/terminal command
@@ -16,44 +10,40 @@ def is_command(text: str) -> bool:
16
 
17
  text = text.strip().lower()
18
 
19
- # =========================================
20
- # Exact commands
21
- # =========================================
22
  if text in {"exit", "quit", "clear"}:
23
  return True
24
 
25
- # =========================================
26
- # Command-like patterns (regex)
27
- # =========================================
28
  command_patterns = [
29
- r"^python\s+\S+\.py", # python file.py
30
- r"^pip\s+install", # pip install
31
- r"^npm\s+install", # npm install
32
- r"^node\s+\S+", # node file.js
33
- r"^cd\s+.+", # cd folder
34
- r"^ls\b", # ls
35
- r"^dir\b", # dir
36
- r"^git\s+.+", # git command
37
- r"^sudo\s+.+", # sudo command
38
- r".+\.py\s*$", # ends with file.py (ONLY if standalone)
39
  ]
40
 
41
  for pattern in command_patterns:
42
  if re.match(pattern, text):
43
  return True
44
 
45
- # =========================================
46
- # Inline execution attempts
47
- # =========================================
48
  if "run" in text and re.search(r"\.py\b", text):
49
  return True
50
 
51
  return False
52
 
53
-
54
- # =====================================================
55
- # Command Handling
56
- # =====================================================
57
  def handle_command(text: str) -> str:
58
  """
59
  Return safe response for command-like inputs
@@ -61,26 +51,26 @@ def handle_command(text: str) -> str:
61
 
62
  text = text.lower().strip()
63
 
64
- # exit commands
65
  if text in {"exit", "quit"}:
66
  return "👋 Session ended. Start a new chat anytime."
67
 
68
- # python execution
69
  if re.search(r"python\s+\S+\.py", text):
70
  return (
71
  "⚠️ This looks like a code execution command.\n"
72
  "I only help with graduation project ideas and development."
73
  )
74
 
75
- # package installation
76
  if "pip install" in text or "npm install" in text:
77
  return (
78
  "⚠️ Installation commands are outside my scope.\n"
79
  "I can help you design your graduation project instead."
80
  )
81
 
82
- # generic fallback
83
  return (
84
  "⚠️ This looks like a system command.\n"
85
  "Please ask about graduation projects (ideas, features, or system design)."
86
- )
 
 
 
1
  import re
2
 
 
 
 
 
3
  def is_command(text: str) -> bool:
4
  """
5
  Detect if input is a system/terminal command
 
10
 
11
  text = text.strip().lower()
12
 
13
+
14
+
15
+
16
  if text in {"exit", "quit", "clear"}:
17
  return True
18
 
19
+
20
+
21
+
22
  command_patterns = [
23
+ r"^python\s+\S+\.py",
24
+ r"^pip\s+install",
25
+ r"^npm\s+install",
26
+ r"^node\s+\S+",
27
+ r"^cd\s+.+",
28
+ r"^ls\b",
29
+ r"^dir\b",
30
+ r"^git\s+.+",
31
+ r"^sudo\s+.+",
32
+ r".+\.py\s*$",
33
  ]
34
 
35
  for pattern in command_patterns:
36
  if re.match(pattern, text):
37
  return True
38
 
39
+
40
+
41
+
42
  if "run" in text and re.search(r"\.py\b", text):
43
  return True
44
 
45
  return False
46
 
 
 
 
 
47
  def handle_command(text: str) -> str:
48
  """
49
  Return safe response for command-like inputs
 
51
 
52
  text = text.lower().strip()
53
 
54
+
55
  if text in {"exit", "quit"}:
56
  return "👋 Session ended. Start a new chat anytime."
57
 
58
+
59
  if re.search(r"python\s+\S+\.py", text):
60
  return (
61
  "⚠️ This looks like a code execution command.\n"
62
  "I only help with graduation project ideas and development."
63
  )
64
 
65
+
66
  if "pip install" in text or "npm install" in text:
67
  return (
68
  "⚠️ Installation commands are outside my scope.\n"
69
  "I can help you design your graduation project instead."
70
  )
71
 
72
+
73
  return (
74
  "⚠️ This looks like a system command.\n"
75
  "Please ask about graduation projects (ideas, features, or system design)."
76
+ )
src/recommendation_engine/config.py CHANGED
@@ -1,27 +1,16 @@
1
- # src/recommendation_engine/config.py
2
-
3
  import os
4
  from pathlib import Path
5
  from dotenv import load_dotenv
6
 
7
- # =====================================================
8
- # Load Environment Variables
9
- # =====================================================
10
  BASE_DIR = Path(__file__).resolve().parent.parent.parent
11
  ENV_PATH = BASE_DIR / ".env"
12
 
13
  if ENV_PATH.exists():
14
  load_dotenv(ENV_PATH)
15
 
16
- # =====================================================
17
- # App Mode
18
- # =====================================================
19
  ENV = os.getenv("ENV", "development")
20
  DEBUG_MODE = os.getenv("DEBUG_MODE", "true").lower() == "true"
21
 
22
- # =====================================================
23
- # Gemini API Config
24
- # =====================================================
25
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
26
 
27
  MODEL_CANDIDATES = [
@@ -31,22 +20,17 @@ MODEL_CANDIDATES = [
31
  os.getenv("QUALITY_MODEL", "gemini-2.5-pro"),
32
  ]
33
 
34
- # remove duplicates safely
35
  _seen = set()
36
  MODEL_CANDIDATES = [
37
  m for m in MODEL_CANDIDATES
38
  if m and not (m in _seen or _seen.add(m))
39
  ]
40
 
41
- # =====================================================
42
- # Generation Settings
43
- # =====================================================
44
  IDEA_TEMPERATURE = float(os.getenv("IDEA_TEMPERATURE", 0.9))
45
  FEATURE_TEMPERATURE = float(os.getenv("FEATURE_TEMPERATURE", 0.6))
46
  CHAT_TEMPERATURE = float(os.getenv("CHAT_TEMPERATURE", 0.7))
47
  INTENT_TEMPERATURE = float(os.getenv("INTENT_TEMPERATURE", 0.0))
48
 
49
- # Max tokens
50
  IDEA_MAX_TOKENS = int(os.getenv("IDEA_MAX_TOKENS", 800))
51
  FEATURE_MAX_TOKENS = int(os.getenv("FEATURE_MAX_TOKENS", 600))
52
  CHAT_MAX_TOKENS = int(os.getenv("CHAT_MAX_TOKENS", 700))
@@ -55,23 +39,14 @@ INTENT_MAX_TOKENS = int(os.getenv("INTENT_MAX_TOKENS", 20))
55
  TOP_P = float(os.getenv("TOP_P", 0.95))
56
  TOP_K = int(os.getenv("TOP_K", 40))
57
 
58
- # =====================================================
59
- # Retry / Timeout
60
- # =====================================================
61
  REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", 30))
62
  MAX_RETRIES = int(os.getenv("MAX_RETRIES", 3))
63
  RETRY_DELAY_SECONDS = int(os.getenv("RETRY_DELAY_SECONDS", 2))
64
 
65
- # =====================================================
66
- # Recommendation Defaults
67
- # =====================================================
68
  DEFAULT_FEATURE_COUNT = int(os.getenv("DEFAULT_FEATURE_COUNT", 5))
69
  DEFAULT_IDEA_COUNT = int(os.getenv("DEFAULT_IDEA_COUNT", 5))
70
  GENERATION_BATCH_SIZE = int(os.getenv("GENERATION_BATCH_SIZE", 10))
71
 
72
- # =====================================================
73
- # Duplicate Detection
74
- # =====================================================
75
  IDEA_DUPLICATE_THRESHOLD = float(
76
  os.getenv("IDEA_DUPLICATE_THRESHOLD", 0.82)
77
  )
@@ -80,48 +55,27 @@ FEATURE_DUPLICATE_THRESHOLD = float(
80
  os.getenv("FEATURE_DUPLICATE_THRESHOLD", 0.75)
81
  )
82
 
83
- # =====================================================
84
- # Idea Generator Control
85
- # =====================================================
86
  MAX_IDEA_RETRIES = int(os.getenv("MAX_IDEA_RETRIES", 3))
87
  MAX_IDEA_LIMIT = int(os.getenv("MAX_IDEA_LIMIT", 20))
88
 
89
- # =====================================================
90
- # Similarity Engine
91
- # =====================================================
92
  SIMILARITY_TOP_K = int(os.getenv("SIMILARITY_TOP_K", 5))
93
 
94
- # =====================================================
95
- # State / Memory Control
96
- # =====================================================
97
  MAX_HISTORY = int(os.getenv("MAX_HISTORY", 20))
98
  MAX_FEATURES = int(os.getenv("MAX_FEATURES", 20))
99
  MAX_IDEAS = int(os.getenv("MAX_IDEAS", 10))
100
 
101
- # =====================================================
102
- # Semantic Routing
103
- # =====================================================
104
  ENABLE_SEMANTIC_INTENT = os.getenv(
105
  "ENABLE_SEMANTIC_INTENT", "true"
106
  ).lower() == "true"
107
 
108
- # =====================================================
109
- # Logging
110
- # =====================================================
111
  ENABLE_LOGGING = os.getenv("ENABLE_LOGGING", "true").lower() == "true"
112
  LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
113
 
114
- # =====================================================
115
- # Safety Check (🔥 IMPORTANT)
116
- # =====================================================
117
  if not GEMINI_API_KEY:
118
  raise ValueError(
119
  "❌ GEMINI_API_KEY is missing. Please set it in .env file."
120
  )
121
 
122
- # =====================================================
123
- # Debug Print
124
- # =====================================================
125
  if DEBUG_MODE and ENV == "development":
126
  print("\n CONFIG LOADED:")
127
  print(f"ENV: {ENV}")
@@ -129,4 +83,4 @@ if DEBUG_MODE and ENV == "development":
129
  print(f"MODELS: {MODEL_CANDIDATES}")
130
  print(f"MAX_RETRIES: {MAX_RETRIES}")
131
  print(f"IDEA_TEMP: {IDEA_TEMPERATURE}")
132
- print("=================================\n")
 
 
 
1
  import os
2
  from pathlib import Path
3
  from dotenv import load_dotenv
4
 
 
 
 
5
  BASE_DIR = Path(__file__).resolve().parent.parent.parent
6
  ENV_PATH = BASE_DIR / ".env"
7
 
8
  if ENV_PATH.exists():
9
  load_dotenv(ENV_PATH)
10
 
 
 
 
11
  ENV = os.getenv("ENV", "development")
12
  DEBUG_MODE = os.getenv("DEBUG_MODE", "true").lower() == "true"
13
 
 
 
 
14
  GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "")
15
 
16
  MODEL_CANDIDATES = [
 
20
  os.getenv("QUALITY_MODEL", "gemini-2.5-pro"),
21
  ]
22
 
 
23
  _seen = set()
24
  MODEL_CANDIDATES = [
25
  m for m in MODEL_CANDIDATES
26
  if m and not (m in _seen or _seen.add(m))
27
  ]
28
 
 
 
 
29
  IDEA_TEMPERATURE = float(os.getenv("IDEA_TEMPERATURE", 0.9))
30
  FEATURE_TEMPERATURE = float(os.getenv("FEATURE_TEMPERATURE", 0.6))
31
  CHAT_TEMPERATURE = float(os.getenv("CHAT_TEMPERATURE", 0.7))
32
  INTENT_TEMPERATURE = float(os.getenv("INTENT_TEMPERATURE", 0.0))
33
 
 
34
  IDEA_MAX_TOKENS = int(os.getenv("IDEA_MAX_TOKENS", 800))
35
  FEATURE_MAX_TOKENS = int(os.getenv("FEATURE_MAX_TOKENS", 600))
36
  CHAT_MAX_TOKENS = int(os.getenv("CHAT_MAX_TOKENS", 700))
 
39
  TOP_P = float(os.getenv("TOP_P", 0.95))
40
  TOP_K = int(os.getenv("TOP_K", 40))
41
 
 
 
 
42
  REQUEST_TIMEOUT = int(os.getenv("REQUEST_TIMEOUT", 30))
43
  MAX_RETRIES = int(os.getenv("MAX_RETRIES", 3))
44
  RETRY_DELAY_SECONDS = int(os.getenv("RETRY_DELAY_SECONDS", 2))
45
 
 
 
 
46
  DEFAULT_FEATURE_COUNT = int(os.getenv("DEFAULT_FEATURE_COUNT", 5))
47
  DEFAULT_IDEA_COUNT = int(os.getenv("DEFAULT_IDEA_COUNT", 5))
48
  GENERATION_BATCH_SIZE = int(os.getenv("GENERATION_BATCH_SIZE", 10))
49
 
 
 
 
50
  IDEA_DUPLICATE_THRESHOLD = float(
51
  os.getenv("IDEA_DUPLICATE_THRESHOLD", 0.82)
52
  )
 
55
  os.getenv("FEATURE_DUPLICATE_THRESHOLD", 0.75)
56
  )
57
 
 
 
 
58
  MAX_IDEA_RETRIES = int(os.getenv("MAX_IDEA_RETRIES", 3))
59
  MAX_IDEA_LIMIT = int(os.getenv("MAX_IDEA_LIMIT", 20))
60
 
 
 
 
61
  SIMILARITY_TOP_K = int(os.getenv("SIMILARITY_TOP_K", 5))
62
 
 
 
 
63
  MAX_HISTORY = int(os.getenv("MAX_HISTORY", 20))
64
  MAX_FEATURES = int(os.getenv("MAX_FEATURES", 20))
65
  MAX_IDEAS = int(os.getenv("MAX_IDEAS", 10))
66
 
 
 
 
67
  ENABLE_SEMANTIC_INTENT = os.getenv(
68
  "ENABLE_SEMANTIC_INTENT", "true"
69
  ).lower() == "true"
70
 
 
 
 
71
  ENABLE_LOGGING = os.getenv("ENABLE_LOGGING", "true").lower() == "true"
72
  LOG_LEVEL = os.getenv("LOG_LEVEL", "INFO")
73
 
 
 
 
74
  if not GEMINI_API_KEY:
75
  raise ValueError(
76
  "❌ GEMINI_API_KEY is missing. Please set it in .env file."
77
  )
78
 
 
 
 
79
  if DEBUG_MODE and ENV == "development":
80
  print("\n CONFIG LOADED:")
81
  print(f"ENV: {ENV}")
 
83
  print(f"MODELS: {MODEL_CANDIDATES}")
84
  print(f"MAX_RETRIES: {MAX_RETRIES}")
85
  print(f"IDEA_TEMP: {IDEA_TEMPERATURE}")
86
+ print("=================================\n")
src/recommendation_engine/context_builder.py CHANGED
@@ -1,5 +1,3 @@
1
- # src/recommendation_engine/context_builder.py
2
-
3
  import logging
4
  import re
5
 
@@ -22,10 +20,6 @@ from src.recommendation_engine.config import (
22
 
23
  logger = logging.getLogger(__name__)
24
 
25
-
26
- # =====================================================
27
- # DOMAIN DEFINITIONS
28
- # =====================================================
29
  DOMAIN_KEYWORDS = {
30
  "artificial intelligence": [
31
  "ai",
@@ -104,10 +98,6 @@ DOMAIN_KEYWORDS = {
104
  ]
105
  }
106
 
107
-
108
- # =====================================================
109
- # Helpers
110
- # =====================================================
111
  def normalize(text: str) -> str:
112
 
113
  text = str(text).lower().strip()
@@ -118,7 +108,6 @@ def normalize(text: str) -> str:
118
 
119
  return text
120
 
121
-
122
  def clean_list(
123
  items: List[str],
124
  limit: int = 20
@@ -143,10 +132,6 @@ def clean_list(
143
 
144
  return final[:limit]
145
 
146
-
147
- # =====================================================
148
- # Multi-domain detection
149
- # =====================================================
150
  def detect_domains(text: str) -> List[str]:
151
 
152
  text = normalize(text)
@@ -159,7 +144,7 @@ def detect_domains(text: str) -> List[str]:
159
 
160
  for w in words:
161
 
162
- # phrase
163
  if " " in w:
164
 
165
  if w in text:
@@ -167,7 +152,7 @@ def detect_domains(text: str) -> List[str]:
167
  detected.append(domain)
168
  break
169
 
170
- # word
171
  else:
172
 
173
  if w in words_in_text:
@@ -177,10 +162,6 @@ def detect_domains(text: str) -> List[str]:
177
 
178
  return clean_list(detected, limit=3)
179
 
180
-
181
- # =====================================================
182
- # Main domain extractor
183
- # =====================================================
184
  def extract_domain(text: str) -> str:
185
 
186
  if not text:
@@ -188,20 +169,20 @@ def extract_domain(text: str) -> str:
188
 
189
  text = normalize(text)
190
 
191
- # hard normalization
192
  if text in ["ai", "ml"]:
193
  return "artificial intelligence"
194
 
195
- # direct domain match
196
  if text in DOMAIN_KEYWORDS:
197
  return text
198
 
199
- # multi-domain detection
200
  domains = detect_domains(text)
201
 
202
  if domains:
203
 
204
- # prioritize non-general
205
  for d in domains:
206
 
207
  if d != "general":
@@ -209,7 +190,7 @@ def extract_domain(text: str) -> str:
209
 
210
  return domains[0]
211
 
212
- # fuzzy typo matching
213
  all_words = []
214
 
215
  word_map = {}
@@ -232,7 +213,7 @@ def extract_domain(text: str) -> str:
232
  if match:
233
  return word_map[match[0]]
234
 
235
- # partial match
236
  for domain, words in DOMAIN_KEYWORDS.items():
237
 
238
  for w in words:
@@ -243,10 +224,6 @@ def extract_domain(text: str) -> str:
243
 
244
  return ""
245
 
246
-
247
- # =====================================================
248
- # Similarity cache
249
- # =====================================================
250
  @lru_cache(maxsize=100)
251
  def cached_similarity(
252
  title: str,
@@ -259,10 +236,6 @@ def cached_similarity(
259
  top_k=SIMILARITY_TOP_K
260
  )
261
 
262
-
263
- # =====================================================
264
- # Common feature extraction
265
- # =====================================================
266
  def extract_common_features(
267
  results: pd.DataFrame
268
  ) -> List[str]:
@@ -300,10 +273,6 @@ def extract_common_features(
300
  in counter.most_common(12)
301
  ]
302
 
303
-
304
- # =====================================================
305
- # Similar titles
306
- # =====================================================
307
  def extract_titles(
308
  results: pd.DataFrame
309
  ) -> List[str]:
@@ -319,10 +288,6 @@ def extract_titles(
319
 
320
  return clean_list(titles, limit=10)
321
 
322
-
323
- # =====================================================
324
- # Architecture hints
325
- # =====================================================
326
  def build_architecture_hints(
327
  domains: List[str]
328
  ) -> List[str]:
@@ -359,10 +324,6 @@ def build_architecture_hints(
359
 
360
  return clean_list(hints, limit=10)
361
 
362
-
363
- # =====================================================
364
- # Project Context
365
- # =====================================================
366
  def build_project_context(
367
  title: str,
368
  description: str,
@@ -380,9 +341,9 @@ def build_project_context(
380
  f"{description}"
381
  )
382
 
383
- # =========================================
384
- # Domains
385
- # =========================================
386
  domains = detect_domains(full_text)
387
 
388
  main_domain = (
@@ -391,9 +352,9 @@ def build_project_context(
391
  else "general"
392
  )
393
 
394
- # =========================================
395
- # Auto features
396
- # =========================================
397
  auto_features = extract_features(
398
  full_text
399
  )
@@ -403,9 +364,9 @@ def build_project_context(
403
  MAX_FEATURES
404
  )
405
 
406
- # =========================================
407
- # Similarity search
408
- # =========================================
409
  try:
410
 
411
  results = cached_similarity(
@@ -421,9 +382,9 @@ def build_project_context(
421
 
422
  results = None
423
 
424
- # =========================================
425
- # Empty fallback
426
- # =========================================
427
  if (
428
  not isinstance(results, pd.DataFrame)
429
  or len(results) == 0
@@ -443,9 +404,9 @@ def build_project_context(
443
  "context_strength": 0.0
444
  }
445
 
446
- # =========================================
447
- # Rich context
448
- # =========================================
449
  similar_titles = extract_titles(results)
450
 
451
  common_features = extract_common_features(
@@ -487,10 +448,6 @@ def build_project_context(
487
  "context_strength": round(context_strength, 4)
488
  }
489
 
490
-
491
- # =====================================================
492
- # Domain Context
493
- # =====================================================
494
  def build_domain_context(
495
  domain: str
496
  ) -> Dict[str, Any]:
@@ -553,4 +510,4 @@ def build_domain_context(
553
  float(hybrid_scores.mean()),
554
  4
555
  )
556
- }
 
 
 
1
  import logging
2
  import re
3
 
 
20
 
21
  logger = logging.getLogger(__name__)
22
 
 
 
 
 
23
  DOMAIN_KEYWORDS = {
24
  "artificial intelligence": [
25
  "ai",
 
98
  ]
99
  }
100
 
 
 
 
 
101
  def normalize(text: str) -> str:
102
 
103
  text = str(text).lower().strip()
 
108
 
109
  return text
110
 
 
111
  def clean_list(
112
  items: List[str],
113
  limit: int = 20
 
132
 
133
  return final[:limit]
134
 
 
 
 
 
135
  def detect_domains(text: str) -> List[str]:
136
 
137
  text = normalize(text)
 
144
 
145
  for w in words:
146
 
147
+
148
  if " " in w:
149
 
150
  if w in text:
 
152
  detected.append(domain)
153
  break
154
 
155
+
156
  else:
157
 
158
  if w in words_in_text:
 
162
 
163
  return clean_list(detected, limit=3)
164
 
 
 
 
 
165
  def extract_domain(text: str) -> str:
166
 
167
  if not text:
 
169
 
170
  text = normalize(text)
171
 
172
+
173
  if text in ["ai", "ml"]:
174
  return "artificial intelligence"
175
 
176
+
177
  if text in DOMAIN_KEYWORDS:
178
  return text
179
 
180
+
181
  domains = detect_domains(text)
182
 
183
  if domains:
184
 
185
+
186
  for d in domains:
187
 
188
  if d != "general":
 
190
 
191
  return domains[0]
192
 
193
+
194
  all_words = []
195
 
196
  word_map = {}
 
213
  if match:
214
  return word_map[match[0]]
215
 
216
+
217
  for domain, words in DOMAIN_KEYWORDS.items():
218
 
219
  for w in words:
 
224
 
225
  return ""
226
 
 
 
 
 
227
  @lru_cache(maxsize=100)
228
  def cached_similarity(
229
  title: str,
 
236
  top_k=SIMILARITY_TOP_K
237
  )
238
 
 
 
 
 
239
  def extract_common_features(
240
  results: pd.DataFrame
241
  ) -> List[str]:
 
273
  in counter.most_common(12)
274
  ]
275
 
 
 
 
 
276
  def extract_titles(
277
  results: pd.DataFrame
278
  ) -> List[str]:
 
288
 
289
  return clean_list(titles, limit=10)
290
 
 
 
 
 
291
  def build_architecture_hints(
292
  domains: List[str]
293
  ) -> List[str]:
 
324
 
325
  return clean_list(hints, limit=10)
326
 
 
 
 
 
327
  def build_project_context(
328
  title: str,
329
  description: str,
 
341
  f"{description}"
342
  )
343
 
344
+
345
+
346
+
347
  domains = detect_domains(full_text)
348
 
349
  main_domain = (
 
352
  else "general"
353
  )
354
 
355
+
356
+
357
+
358
  auto_features = extract_features(
359
  full_text
360
  )
 
364
  MAX_FEATURES
365
  )
366
 
367
+
368
+
369
+
370
  try:
371
 
372
  results = cached_similarity(
 
382
 
383
  results = None
384
 
385
+
386
+
387
+
388
  if (
389
  not isinstance(results, pd.DataFrame)
390
  or len(results) == 0
 
404
  "context_strength": 0.0
405
  }
406
 
407
+
408
+
409
+
410
  similar_titles = extract_titles(results)
411
 
412
  common_features = extract_common_features(
 
448
  "context_strength": round(context_strength, 4)
449
  }
450
 
 
 
 
 
451
  def build_domain_context(
452
  domain: str
453
  ) -> Dict[str, Any]:
 
510
  float(hybrid_scores.mean()),
511
  4
512
  )
513
+ }
src/recommendation_engine/feature_generator.py CHANGED
@@ -1,5 +1,3 @@
1
- # src/recommendation_engine/feature_generator.py
2
-
3
  import logging
4
  from typing import List, Dict, Any, Set
5
 
@@ -20,20 +18,11 @@ logger = logging.getLogger(__name__)
20
 
21
  MAX_RETRIES = 5
22
 
23
- # 🔥 stronger duplicate control
24
  SIMILARITY_THRESHOLD_LOCAL = 0.82
25
 
26
-
27
- # =====================================================
28
- # Helpers
29
- # =====================================================
30
  def normalize(text: str) -> str:
31
  return " ".join(str(text).strip().lower().split())
32
 
33
-
34
- # =====================================================
35
- # Generic feature detector
36
- # =====================================================
37
  GENERIC_PATTERNS = [
38
  "dashboard",
39
  "login",
@@ -47,7 +36,6 @@ GENERIC_PATTERNS = [
47
  "user management"
48
  ]
49
 
50
-
51
  def is_generic_feature(text: str) -> bool:
52
 
53
  low = normalize(text)
@@ -61,10 +49,6 @@ def is_generic_feature(text: str) -> bool:
61
 
62
  return False
63
 
64
-
65
- # =====================================================
66
- # Feature cleaner
67
- # =====================================================
68
  def clean_features(features: List[str]) -> List[str]:
69
 
70
  final = []
@@ -78,7 +62,7 @@ def clean_features(features: List[str]) -> List[str]:
78
 
79
  words = clean.split()
80
 
81
- # 🔥 ideal feature size
82
  if len(words) < 3 or len(words) > 10:
83
  continue
84
 
@@ -89,10 +73,6 @@ def clean_features(features: List[str]) -> List[str]:
89
 
90
  return final
91
 
92
-
93
- # =====================================================
94
- # Semantic duplicate checker
95
- # =====================================================
96
  def is_duplicate_local(feature: str, existing: List[str]) -> bool:
97
 
98
  for old in existing:
@@ -105,17 +85,13 @@ def is_duplicate_local(feature: str, existing: List[str]) -> bool:
105
 
106
  return False
107
 
108
-
109
- # =====================================================
110
- # Better contextual fallback
111
- # =====================================================
112
  def fallback_features(title: str) -> List[str]:
113
 
114
  title = (title or "").lower()
115
 
116
- # =========================================
117
- # Healthcare
118
- # =========================================
119
  if any(k in title for k in ["health", "hospital", "medical", "clinic"]):
120
 
121
  return [
@@ -126,9 +102,9 @@ def fallback_features(title: str) -> List[str]:
126
  "Predictive patient risk analysis"
127
  ]
128
 
129
- # =========================================
130
- # Education
131
- # =========================================
132
  if any(k in title for k in ["education", "learning", "student", "school"]):
133
 
134
  return [
@@ -139,9 +115,9 @@ def fallback_features(title: str) -> List[str]:
139
  "Personalized study path generation"
140
  ]
141
 
142
- # =========================================
143
- # Security
144
- # =========================================
145
  if any(k in title for k in ["security", "cyber", "threat"]):
146
 
147
  return [
@@ -152,9 +128,9 @@ def fallback_features(title: str) -> List[str]:
152
  "Risk prediction analytics"
153
  ]
154
 
155
- # =========================================
156
- # Default
157
- # =========================================
158
  return [
159
  "Real-time intelligent monitoring",
160
  "Predictive analytics engine",
@@ -163,10 +139,6 @@ def fallback_features(title: str) -> List[str]:
163
  "Dynamic performance optimization"
164
  ]
165
 
166
-
167
- # =====================================================
168
- # Main Generator
169
- # =====================================================
170
  def generate_features(
171
  title: str,
172
  description: str,
@@ -183,9 +155,9 @@ def generate_features(
183
 
184
  logger.info(f"Starting feature generation | title={title}")
185
 
186
- # =========================================
187
- # Build Context
188
- # =========================================
189
  context = build_project_context(
190
  title=title,
191
  description=description,
@@ -210,9 +182,9 @@ def generate_features(
210
 
211
  attempts = 0
212
 
213
- # =========================================
214
- # Generation Loop
215
- # =========================================
216
  while len(final_features) < top_k and attempts < MAX_RETRIES:
217
 
218
  attempts += 1
@@ -224,18 +196,18 @@ def generate_features(
224
  GENERATION_BATCH_SIZE
225
  )
226
 
227
- # =====================================
228
- # Build Prompt
229
- # =====================================
230
  prompt = build_feature_prompt(
231
  context=context,
232
  count=generation_count,
233
  previous_features=previous_generated_features
234
  )
235
 
236
- # =====================================
237
- # LLM Generation
238
- # =====================================
239
  raw_text = generate_text(
240
  prompt,
241
  task="feature"
@@ -245,9 +217,9 @@ def generate_features(
245
  logger.warning("Empty feature response")
246
  continue
247
 
248
- # =====================================
249
- # Validation
250
- # =====================================
251
  generated = validate_generated_list(
252
  text=raw_text,
253
  top_k=generation_count
@@ -257,9 +229,9 @@ def generate_features(
257
 
258
  logger.info(f"Generated {len(generated)} candidate features")
259
 
260
- # =====================================
261
- # Filtering
262
- # =====================================
263
  for feat in generated:
264
 
265
  norm = normalize(feat)
@@ -267,7 +239,7 @@ def generate_features(
267
  if not norm:
268
  continue
269
 
270
- # exact duplicate
271
  if (
272
  norm in final_norm_set
273
  or norm in existing_norm
@@ -275,11 +247,11 @@ def generate_features(
275
  ):
276
  continue
277
 
278
- # semantic novelty
279
  if not is_feature_novel(feat, existing_features):
280
  continue
281
 
282
- # local semantic duplicate
283
  if is_duplicate_local(feat, final_features):
284
  continue
285
 
@@ -291,9 +263,9 @@ def generate_features(
291
  if len(final_features) >= top_k:
292
  break
293
 
294
- # =========================================
295
- # Fallback
296
- # =========================================
297
  if len(final_features) < top_k:
298
 
299
  logger.warning("Using fallback features")
@@ -323,4 +295,4 @@ def generate_features(
323
  "recommended_features": final_features,
324
  "originality_score": context.get("originality_score", 1.0),
325
  "similar_projects": context.get("similar_titles", [])
326
- }
 
 
 
1
  import logging
2
  from typing import List, Dict, Any, Set
3
 
 
18
 
19
  MAX_RETRIES = 5
20
 
 
21
  SIMILARITY_THRESHOLD_LOCAL = 0.82
22
 
 
 
 
 
23
  def normalize(text: str) -> str:
24
  return " ".join(str(text).strip().lower().split())
25
 
 
 
 
 
26
  GENERIC_PATTERNS = [
27
  "dashboard",
28
  "login",
 
36
  "user management"
37
  ]
38
 
 
39
  def is_generic_feature(text: str) -> bool:
40
 
41
  low = normalize(text)
 
49
 
50
  return False
51
 
 
 
 
 
52
  def clean_features(features: List[str]) -> List[str]:
53
 
54
  final = []
 
62
 
63
  words = clean.split()
64
 
65
+
66
  if len(words) < 3 or len(words) > 10:
67
  continue
68
 
 
73
 
74
  return final
75
 
 
 
 
 
76
  def is_duplicate_local(feature: str, existing: List[str]) -> bool:
77
 
78
  for old in existing:
 
85
 
86
  return False
87
 
 
 
 
 
88
  def fallback_features(title: str) -> List[str]:
89
 
90
  title = (title or "").lower()
91
 
92
+
93
+
94
+
95
  if any(k in title for k in ["health", "hospital", "medical", "clinic"]):
96
 
97
  return [
 
102
  "Predictive patient risk analysis"
103
  ]
104
 
105
+
106
+
107
+
108
  if any(k in title for k in ["education", "learning", "student", "school"]):
109
 
110
  return [
 
115
  "Personalized study path generation"
116
  ]
117
 
118
+
119
+
120
+
121
  if any(k in title for k in ["security", "cyber", "threat"]):
122
 
123
  return [
 
128
  "Risk prediction analytics"
129
  ]
130
 
131
+
132
+
133
+
134
  return [
135
  "Real-time intelligent monitoring",
136
  "Predictive analytics engine",
 
139
  "Dynamic performance optimization"
140
  ]
141
 
 
 
 
 
142
  def generate_features(
143
  title: str,
144
  description: str,
 
155
 
156
  logger.info(f"Starting feature generation | title={title}")
157
 
158
+
159
+
160
+
161
  context = build_project_context(
162
  title=title,
163
  description=description,
 
182
 
183
  attempts = 0
184
 
185
+
186
+
187
+
188
  while len(final_features) < top_k and attempts < MAX_RETRIES:
189
 
190
  attempts += 1
 
196
  GENERATION_BATCH_SIZE
197
  )
198
 
199
+
200
+
201
+
202
  prompt = build_feature_prompt(
203
  context=context,
204
  count=generation_count,
205
  previous_features=previous_generated_features
206
  )
207
 
208
+
209
+
210
+
211
  raw_text = generate_text(
212
  prompt,
213
  task="feature"
 
217
  logger.warning("Empty feature response")
218
  continue
219
 
220
+
221
+
222
+
223
  generated = validate_generated_list(
224
  text=raw_text,
225
  top_k=generation_count
 
229
 
230
  logger.info(f"Generated {len(generated)} candidate features")
231
 
232
+
233
+
234
+
235
  for feat in generated:
236
 
237
  norm = normalize(feat)
 
239
  if not norm:
240
  continue
241
 
242
+
243
  if (
244
  norm in final_norm_set
245
  or norm in existing_norm
 
247
  ):
248
  continue
249
 
250
+
251
  if not is_feature_novel(feat, existing_features):
252
  continue
253
 
254
+
255
  if is_duplicate_local(feat, final_features):
256
  continue
257
 
 
263
  if len(final_features) >= top_k:
264
  break
265
 
266
+
267
+
268
+
269
  if len(final_features) < top_k:
270
 
271
  logger.warning("Using fallback features")
 
295
  "recommended_features": final_features,
296
  "originality_score": context.get("originality_score", 1.0),
297
  "similar_projects": context.get("similar_titles", [])
298
+ }
src/recommendation_engine/full_project_generator.py CHANGED
@@ -5,10 +5,6 @@ from src.recommendation_engine.prompt_builder import (
5
 
6
  import re
7
 
8
-
9
- # =====================================================
10
- # SECTION EXTRACTOR
11
- # =====================================================
12
  def extract_section(text, section_name):
13
 
14
  text = text.strip()
@@ -48,10 +44,6 @@ def extract_section(text, section_name):
48
 
49
  return text[start:end].strip()
50
 
51
-
52
- # =====================================================
53
- # BULLET PARSER
54
- # =====================================================
55
  def parse_bullets(text):
56
 
57
  lines = text.splitlines()
@@ -76,10 +68,6 @@ def parse_bullets(text):
76
 
77
  return final
78
 
79
-
80
- # =====================================================
81
- # FULL PROJECT GENERATOR
82
- # =====================================================
83
  def generate_full_project(
84
  title,
85
  features,
@@ -103,9 +91,9 @@ def generate_full_project(
103
 
104
  result = {
105
 
106
- # =============================================
107
- # BASIC
108
- # =============================================
109
  "project_title": title,
110
 
111
  "category":
@@ -117,9 +105,9 @@ def generate_full_project(
117
  "description":
118
  extract_section(raw, "DESCRIPTION"),
119
 
120
- # =============================================
121
- # LISTS
122
- # =============================================
123
  "technologies":
124
  parse_bullets(
125
  extract_section(raw, "TECHNOLOGIES")
@@ -140,9 +128,9 @@ def generate_full_project(
140
  extract_section(raw, "FUTURE_WORK")
141
  ),
142
 
143
- # =============================================
144
- # TEXT FIELDS
145
- # =============================================
146
  "problem_statement":
147
  extract_section(
148
  raw,
@@ -168,9 +156,9 @@ def generate_full_project(
168
  )
169
  }
170
 
171
- # =================================================
172
- # FALLBACKS
173
- # =================================================
174
 
175
  if not result.get("category"):
176
  result["category"] = "General AI System"
@@ -224,4 +212,4 @@ def generate_full_project(
224
  f"automation, monitoring, and predictive analysis."
225
  )
226
 
227
- return result
 
5
 
6
  import re
7
 
 
 
 
 
8
  def extract_section(text, section_name):
9
 
10
  text = text.strip()
 
44
 
45
  return text[start:end].strip()
46
 
 
 
 
 
47
  def parse_bullets(text):
48
 
49
  lines = text.splitlines()
 
68
 
69
  return final
70
 
 
 
 
 
71
  def generate_full_project(
72
  title,
73
  features,
 
91
 
92
  result = {
93
 
94
+
95
+
96
+
97
  "project_title": title,
98
 
99
  "category":
 
105
  "description":
106
  extract_section(raw, "DESCRIPTION"),
107
 
108
+
109
+
110
+
111
  "technologies":
112
  parse_bullets(
113
  extract_section(raw, "TECHNOLOGIES")
 
128
  extract_section(raw, "FUTURE_WORK")
129
  ),
130
 
131
+
132
+
133
+
134
  "problem_statement":
135
  extract_section(
136
  raw,
 
156
  )
157
  }
158
 
159
+
160
+
161
+
162
 
163
  if not result.get("category"):
164
  result["category"] = "General AI System"
 
212
  f"automation, monitoring, and predictive analysis."
213
  )
214
 
215
+ return result
src/recommendation_engine/idea_generator.py CHANGED
@@ -1,5 +1,3 @@
1
- # src/recommendation_engine/idea_generator.py
2
-
3
  import logging
4
  from typing import Dict, Any, List, Set
5
 
@@ -16,16 +14,11 @@ logger = logging.getLogger(__name__)
16
  MAX_RETRIES = 5
17
  SIMILARITY_THRESHOLD_LOCAL = 0.60
18
 
19
-
20
- # =========================================
21
- # 🔥 LOAD DATASET TITLES (ONCE)
22
- # =========================================
23
  DATA_TITLES = set()
24
 
25
  def normalize_idea(text: str) -> str:
26
  return " ".join(text.lower().strip().split())
27
 
28
-
29
  def init_dataset_titles():
30
  global DATA_TITLES
31
 
@@ -39,24 +32,15 @@ def init_dataset_titles():
39
 
40
  init_dataset_titles()
41
 
42
-
43
- # =========================================
44
- # HELPERS
45
- # =========================================
46
  def clean_ideas(ideas: List[str]) -> List[str]:
47
  return [i.strip() for i in ideas if 2 <= len(i.split()) <= 8]
48
 
49
-
50
  def is_duplicate_local(idea: str, existing: Set[str]) -> bool:
51
  for old in existing:
52
  if compare_two_ideas(idea, old) >= SIMILARITY_THRESHOLD_LOCAL:
53
  return True
54
  return False
55
 
56
-
57
- # =========================================
58
- # FALLBACK
59
- # =========================================
60
  def fallback_by_domain(domain: str) -> List[str]:
61
 
62
  domain = (domain or "general").lower()
@@ -93,10 +77,6 @@ def fallback_by_domain(domain: str) -> List[str]:
93
  "Predictive analytics engine"
94
  ])
95
 
96
-
97
- # =========================================
98
- # MAIN GENERATOR
99
- # =========================================
100
  def generate_ideas(
101
  domain: str,
102
  top_k: int = DEFAULT_IDEA_COUNT,
@@ -120,9 +100,9 @@ def generate_ideas(
120
  all_generated: List[str] = []
121
  attempts = 0
122
 
123
- # =========================================
124
- # GENERATION LOOP
125
- # =========================================
126
  while len(final_ideas) < top_k and attempts < MAX_RETRIES:
127
 
128
  attempts += 1
@@ -152,9 +132,9 @@ def generate_ideas(
152
  logger.info(f"Generated {len(generated)} ideas")
153
  all_generated.extend(generated)
154
 
155
- # =========================================
156
- # FILTERING
157
- # =========================================
158
  for idea in generated:
159
 
160
  normalized = normalize_idea(idea)
@@ -162,15 +142,15 @@ def generate_ideas(
162
  if not normalized:
163
  continue
164
 
165
- # ❌ dataset duplicate
166
- # 🔥 dataset exact match
167
  if normalized in DATA_TITLES:
168
  logger.info(f"[SKIP DATASET EXACT] {idea}")
169
  continue
170
 
171
- # 🔥 dataset semantic match
172
  skip_dataset_similar = False
173
- for data_title in list(DATA_TITLES)[:500]: # limit for speed
174
  if compare_two_ideas(idea, data_title) >= 0.85:
175
  logger.info(f"[SKIP DATASET SIMILAR] {idea}")
176
  skip_dataset_similar = True
@@ -179,17 +159,16 @@ def generate_ideas(
179
  if skip_dataset_similar:
180
  continue
181
 
182
- # ❌ duplicate inside current batch
183
  if normalized in final_set:
184
  continue
185
 
186
- # ❌ duplicate from previous session
187
  if normalized in previous_norm:
188
  logger.info(f"[SKIP PREVIOUS] {idea}")
189
  continue
190
 
191
-
192
- # 🔥 semantic check with previous ideas
193
  skip_similar_prev = False
194
  for old in previous_generated_ideas:
195
  if compare_two_ideas(idea, old) >= 0.85:
@@ -200,12 +179,12 @@ def generate_ideas(
200
  if skip_similar_prev:
201
  continue
202
 
203
- # ❌ semantic duplicate
204
  if is_duplicate_local(idea, final_set):
205
  logger.info(f"[SKIP SIMILAR] {idea}")
206
  continue
207
 
208
- # ✅ accept idea
209
  logger.info(f"[NEW IDEA] {idea}")
210
 
211
  final_ideas.append(idea)
@@ -214,9 +193,9 @@ def generate_ideas(
214
  if len(final_ideas) >= top_k:
215
  break
216
 
217
- # =========================================
218
- # FALLBACK
219
- # =========================================
220
  if len(final_ideas) < top_k:
221
 
222
  logger.warning("Using fallback ideas")
@@ -240,4 +219,4 @@ def generate_ideas(
240
  "domain": domain,
241
  "generated_ideas": all_generated,
242
  "final_ideas": final_ideas
243
- }
 
 
 
1
  import logging
2
  from typing import Dict, Any, List, Set
3
 
 
14
  MAX_RETRIES = 5
15
  SIMILARITY_THRESHOLD_LOCAL = 0.60
16
 
 
 
 
 
17
  DATA_TITLES = set()
18
 
19
  def normalize_idea(text: str) -> str:
20
  return " ".join(text.lower().strip().split())
21
 
 
22
  def init_dataset_titles():
23
  global DATA_TITLES
24
 
 
32
 
33
  init_dataset_titles()
34
 
 
 
 
 
35
  def clean_ideas(ideas: List[str]) -> List[str]:
36
  return [i.strip() for i in ideas if 2 <= len(i.split()) <= 8]
37
 
 
38
  def is_duplicate_local(idea: str, existing: Set[str]) -> bool:
39
  for old in existing:
40
  if compare_two_ideas(idea, old) >= SIMILARITY_THRESHOLD_LOCAL:
41
  return True
42
  return False
43
 
 
 
 
 
44
  def fallback_by_domain(domain: str) -> List[str]:
45
 
46
  domain = (domain or "general").lower()
 
77
  "Predictive analytics engine"
78
  ])
79
 
 
 
 
 
80
  def generate_ideas(
81
  domain: str,
82
  top_k: int = DEFAULT_IDEA_COUNT,
 
100
  all_generated: List[str] = []
101
  attempts = 0
102
 
103
+
104
+
105
+
106
  while len(final_ideas) < top_k and attempts < MAX_RETRIES:
107
 
108
  attempts += 1
 
132
  logger.info(f"Generated {len(generated)} ideas")
133
  all_generated.extend(generated)
134
 
135
+
136
+
137
+
138
  for idea in generated:
139
 
140
  normalized = normalize_idea(idea)
 
142
  if not normalized:
143
  continue
144
 
145
+
146
+
147
  if normalized in DATA_TITLES:
148
  logger.info(f"[SKIP DATASET EXACT] {idea}")
149
  continue
150
 
151
+
152
  skip_dataset_similar = False
153
+ for data_title in list(DATA_TITLES)[:500]:
154
  if compare_two_ideas(idea, data_title) >= 0.85:
155
  logger.info(f"[SKIP DATASET SIMILAR] {idea}")
156
  skip_dataset_similar = True
 
159
  if skip_dataset_similar:
160
  continue
161
 
162
+
163
  if normalized in final_set:
164
  continue
165
 
166
+
167
  if normalized in previous_norm:
168
  logger.info(f"[SKIP PREVIOUS] {idea}")
169
  continue
170
 
171
+
 
172
  skip_similar_prev = False
173
  for old in previous_generated_ideas:
174
  if compare_two_ideas(idea, old) >= 0.85:
 
179
  if skip_similar_prev:
180
  continue
181
 
182
+
183
  if is_duplicate_local(idea, final_set):
184
  logger.info(f"[SKIP SIMILAR] {idea}")
185
  continue
186
 
187
+
188
  logger.info(f"[NEW IDEA] {idea}")
189
 
190
  final_ideas.append(idea)
 
193
  if len(final_ideas) >= top_k:
194
  break
195
 
196
+
197
+
198
+
199
  if len(final_ideas) < top_k:
200
 
201
  logger.warning("Using fallback ideas")
 
219
  "domain": domain,
220
  "generated_ideas": all_generated,
221
  "final_ideas": final_ideas
222
+ }
src/recommendation_engine/intent_classifier.py CHANGED
@@ -1,5 +1,3 @@
1
- # src/recommendation_engine/intent_classifier.py
2
-
3
  import logging
4
  from typing import Dict
5
 
@@ -7,10 +5,6 @@ from src.recommendation_engine.llm_client import generate_text
7
 
8
  logger = logging.getLogger(__name__)
9
 
10
-
11
- # =========================================
12
- # VALID INTENTS
13
- # =========================================
14
  VALID_INTENTS = {
15
  "idea",
16
  "feature",
@@ -18,10 +12,6 @@ VALID_INTENTS = {
18
  "chat"
19
  }
20
 
21
-
22
- # =========================================
23
- # SAFE NORMALIZER
24
- # =========================================
25
  def normalize_intent(text: str) -> str:
26
 
27
  if not text:
@@ -34,10 +24,6 @@ def normalize_intent(text: str) -> str:
34
 
35
  return "chat"
36
 
37
-
38
- # =========================================
39
- # LLM INTENT CLASSIFIER
40
- # =========================================
41
  def classify_with_llm(user_input: str, state: Dict) -> str:
42
 
43
  prompt = f"""
@@ -76,10 +62,6 @@ Rules:
76
  logger.warning(f"[INTENT ERROR] {e}")
77
  return "chat"
78
 
79
-
80
- # =========================================
81
- # HYBRID DETECTION (FINAL 🔥)
82
- # =========================================
83
  def detect_intent(text: str, state: dict = None) -> str:
84
 
85
  if state is None:
@@ -90,48 +72,47 @@ def detect_intent(text: str, state: dict = None) -> str:
90
  has_project = bool(state.get("project_title"))
91
  has_features = bool(state.get("features"))
92
 
93
- # =========================================
94
- # HARD RULES (NO FLOW FORCING ❌)
95
- # =========================================
96
 
97
- # explicit idea requests
98
  if any(x in text_clean for x in [
99
  "idea", "project idea", "new idea", "another idea", "suggest"
100
  ]):
101
  return "idea"
102
 
103
- # explicit feature requests
104
  if "feature" in text_clean:
105
  return "feature"
106
 
107
-
108
- # full project
109
  if any(x in text_clean for x in [
110
  "full project", "complete", "all details"
111
  ]):
112
  return "full_project"
113
 
114
- # =========================================
115
- # USER PROVIDED IDEA (🔥 important)
116
- # =========================================
117
- # If user writes sentence like a project idea
118
  if not has_project and len(text_clean.split()) >= 3:
119
  return "feature"
120
 
121
- # =========================================
122
- # LLM fallback
123
- # =========================================
124
  intent = classify_with_llm(text, state)
125
 
126
  logger.info(f"[INTENT] {intent}")
127
 
128
- # =========================================
129
- # SAFETY FIXES
130
- # =========================================
131
  if intent == "feature" and not has_project:
132
- return "feature" # user likely gave idea
133
 
134
  if intent == "description" and not has_project:
135
  return "idea"
136
 
137
- return intent
 
 
 
1
  import logging
2
  from typing import Dict
3
 
 
5
 
6
  logger = logging.getLogger(__name__)
7
 
 
 
 
 
8
  VALID_INTENTS = {
9
  "idea",
10
  "feature",
 
12
  "chat"
13
  }
14
 
 
 
 
 
15
  def normalize_intent(text: str) -> str:
16
 
17
  if not text:
 
24
 
25
  return "chat"
26
 
 
 
 
 
27
  def classify_with_llm(user_input: str, state: Dict) -> str:
28
 
29
  prompt = f"""
 
62
  logger.warning(f"[INTENT ERROR] {e}")
63
  return "chat"
64
 
 
 
 
 
65
  def detect_intent(text: str, state: dict = None) -> str:
66
 
67
  if state is None:
 
72
  has_project = bool(state.get("project_title"))
73
  has_features = bool(state.get("features"))
74
 
75
+
76
+
77
+
78
 
79
+
80
  if any(x in text_clean for x in [
81
  "idea", "project idea", "new idea", "another idea", "suggest"
82
  ]):
83
  return "idea"
84
 
85
+
86
  if "feature" in text_clean:
87
  return "feature"
88
 
89
+
 
90
  if any(x in text_clean for x in [
91
  "full project", "complete", "all details"
92
  ]):
93
  return "full_project"
94
 
95
+
96
+
97
+
98
+
99
  if not has_project and len(text_clean.split()) >= 3:
100
  return "feature"
101
 
102
+
103
+
104
+
105
  intent = classify_with_llm(text, state)
106
 
107
  logger.info(f"[INTENT] {intent}")
108
 
109
+
110
+
111
+
112
  if intent == "feature" and not has_project:
113
+ return "feature"
114
 
115
  if intent == "description" and not has_project:
116
  return "idea"
117
 
118
+ return intent
src/recommendation_engine/llm_client.py CHANGED
@@ -1,5 +1,3 @@
1
- # src/recommendation_engine/llm_client.py
2
-
3
  import time
4
  import logging
5
  from typing import List
@@ -26,9 +24,6 @@ from src.recommendation_engine.config import (
26
 
27
  from src.recommendation_engine.validator import validate_generated_list
28
 
29
- # =========================================
30
- # Logging
31
- # =========================================
32
  logger = logging.getLogger(__name__)
33
 
34
  if ENABLE_LOGGING:
@@ -37,14 +32,12 @@ if ENABLE_LOGGING:
37
  format="%(asctime)s | %(levelname)s | %(message)s"
38
  )
39
 
40
-
41
  class LLMProviderError(Exception):
42
  def __init__(self, message: str, status_code: int = 503):
43
  super().__init__(message)
44
  self.message = message
45
  self.status_code = status_code
46
 
47
-
48
  def classify_provider_error(error: Exception):
49
  text = str(error).lower()
50
 
@@ -72,26 +65,19 @@ def classify_provider_error(error: Exception):
72
 
73
  return None
74
 
75
- # =========================================
76
- # Init Gemini
77
- # =========================================
78
  client = genai.Client(api_key=GEMINI_API_KEY)
79
 
80
-
81
- # =========================================
82
- # Extract Text (Robust)
83
- # =========================================
84
  def extract_text(response) -> str:
85
 
86
  if not response:
87
  return ""
88
 
89
- # direct
90
  text = getattr(response, "text", None)
91
  if text:
92
  return text.strip()
93
 
94
- # fallback
95
  try:
96
  candidates = getattr(response, "candidates", [])
97
  if candidates:
@@ -104,10 +90,6 @@ def extract_text(response) -> str:
104
 
105
  return ""
106
 
107
-
108
- # =========================================
109
- # Temperature Selector
110
- # =========================================
111
  def get_temperature(task: str) -> float:
112
 
113
  return {
@@ -116,10 +98,6 @@ def get_temperature(task: str) -> float:
116
  "intent": INTENT_TEMPERATURE,
117
  }.get(task, CHAT_TEMPERATURE)
118
 
119
-
120
- # =========================================
121
- # Max Tokens Selector
122
- # =========================================
123
  def get_max_tokens(task: str) -> int:
124
 
125
  return {
@@ -128,17 +106,9 @@ def get_max_tokens(task: str) -> int:
128
  "intent": INTENT_MAX_TOKENS,
129
  }.get(task, CHAT_MAX_TOKENS)
130
 
131
-
132
- # =========================================
133
- # Prompt Safety
134
- # =========================================
135
  def safe_prompt(prompt: str, max_chars: int = 12000) -> str:
136
  return prompt[-max_chars:]
137
 
138
-
139
- # =========================================
140
- # Response Quality Check (🔥 NEW)
141
- # =========================================
142
  def is_bad_response(text: str) -> bool:
143
 
144
  if not text:
@@ -146,11 +116,11 @@ def is_bad_response(text: str) -> bool:
146
 
147
  text = text.strip()
148
 
149
- # ❌ only reject VERY bad outputs
150
  if len(text) < 3:
151
  return True
152
 
153
- # ❌ reject only pure assistant chatter
154
  bad_phrases = [
155
  "as an ai",
156
  "i can help you",
@@ -164,10 +134,6 @@ def is_bad_response(text: str) -> bool:
164
 
165
  return False
166
 
167
-
168
- # =========================================
169
- # Main Generate Function
170
- # =========================================
171
  def generate_text(
172
  prompt: str,
173
  task: str = "chat",
@@ -202,10 +168,10 @@ def generate_text(
202
 
203
  text = extract_text(response)
204
 
205
- # 🔥 skip garbage responses
206
  if is_bad_response(text):
207
  logger.warning("[LLM] Weak response, using anyway")
208
- return text # 🔥 DO NOT RETRY
209
 
210
  return text
211
 
@@ -216,7 +182,7 @@ def generate_text(
216
  if provider_error:
217
  raise provider_error
218
 
219
- # exponential backoff
220
  time.sleep(RETRY_DELAY_SECONDS * (attempt + 1))
221
 
222
  logger.info(f"[LLM] switching model...")
@@ -225,10 +191,6 @@ def generate_text(
225
 
226
  return ""
227
 
228
-
229
- # =========================================
230
- # Generate List (🔥 FIXED)
231
- # =========================================
232
  def generate_list(prompt: str, task="chat") -> List[str]:
233
 
234
  text = generate_text(prompt, task=task)
 
 
 
1
  import time
2
  import logging
3
  from typing import List
 
24
 
25
  from src.recommendation_engine.validator import validate_generated_list
26
 
 
 
 
27
  logger = logging.getLogger(__name__)
28
 
29
  if ENABLE_LOGGING:
 
32
  format="%(asctime)s | %(levelname)s | %(message)s"
33
  )
34
 
 
35
  class LLMProviderError(Exception):
36
  def __init__(self, message: str, status_code: int = 503):
37
  super().__init__(message)
38
  self.message = message
39
  self.status_code = status_code
40
 
 
41
  def classify_provider_error(error: Exception):
42
  text = str(error).lower()
43
 
 
65
 
66
  return None
67
 
 
 
 
68
  client = genai.Client(api_key=GEMINI_API_KEY)
69
 
 
 
 
 
70
  def extract_text(response) -> str:
71
 
72
  if not response:
73
  return ""
74
 
75
+
76
  text = getattr(response, "text", None)
77
  if text:
78
  return text.strip()
79
 
80
+
81
  try:
82
  candidates = getattr(response, "candidates", [])
83
  if candidates:
 
90
 
91
  return ""
92
 
 
 
 
 
93
  def get_temperature(task: str) -> float:
94
 
95
  return {
 
98
  "intent": INTENT_TEMPERATURE,
99
  }.get(task, CHAT_TEMPERATURE)
100
 
 
 
 
 
101
  def get_max_tokens(task: str) -> int:
102
 
103
  return {
 
106
  "intent": INTENT_MAX_TOKENS,
107
  }.get(task, CHAT_MAX_TOKENS)
108
 
 
 
 
 
109
  def safe_prompt(prompt: str, max_chars: int = 12000) -> str:
110
  return prompt[-max_chars:]
111
 
 
 
 
 
112
  def is_bad_response(text: str) -> bool:
113
 
114
  if not text:
 
116
 
117
  text = text.strip()
118
 
119
+
120
  if len(text) < 3:
121
  return True
122
 
123
+
124
  bad_phrases = [
125
  "as an ai",
126
  "i can help you",
 
134
 
135
  return False
136
 
 
 
 
 
137
  def generate_text(
138
  prompt: str,
139
  task: str = "chat",
 
168
 
169
  text = extract_text(response)
170
 
171
+
172
  if is_bad_response(text):
173
  logger.warning("[LLM] Weak response, using anyway")
174
+ return text
175
 
176
  return text
177
 
 
182
  if provider_error:
183
  raise provider_error
184
 
185
+
186
  time.sleep(RETRY_DELAY_SECONDS * (attempt + 1))
187
 
188
  logger.info(f"[LLM] switching model...")
 
191
 
192
  return ""
193
 
 
 
 
 
194
  def generate_list(prompt: str, task="chat") -> List[str]:
195
 
196
  text = generate_text(prompt, task=task)
src/recommendation_engine/llm_router.py CHANGED
@@ -1,7 +1,6 @@
1
  import re
2
  from src.recommendation_engine.context_builder import extract_domain
3
 
4
-
5
  def word_to_number(text: str):
6
  mapping = {
7
  "one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
@@ -20,14 +19,13 @@ def word_to_number(text: str):
20
 
21
  return None
22
 
23
-
24
  def analyze_user_input(user_input: str, state: dict) -> dict:
25
 
26
  text = user_input.lower()
27
 
28
- # =============================
29
- # INTENT DETECTION (RULE BASED)
30
- # =============================
31
  if any(w in text for w in ["idea", "project", "suggest", "recommend"]):
32
  intent = "idea"
33
 
@@ -40,19 +38,19 @@ def analyze_user_input(user_input: str, state: dict) -> dict:
40
  else:
41
  intent = "chat"
42
 
43
- # =============================
44
- # DOMAIN DETECTION
45
- # =============================
46
  domain = extract_domain(text)
47
 
48
- # =============================
49
- # NUMBER DETECTION (🔥 NEW)
50
- # =============================
51
  number = word_to_number(text)
52
 
53
- # =============================
54
- # PROJECT TITLE DETECTION
55
- # =============================
56
  project_title = None
57
 
58
  if intent == "chat" and len(text.split()) > 4:
@@ -63,4 +61,4 @@ def analyze_user_input(user_input: str, state: dict) -> dict:
63
  "domain": domain,
64
  "project_title": project_title,
65
  "number": number
66
- }
 
1
  import re
2
  from src.recommendation_engine.context_builder import extract_domain
3
 
 
4
  def word_to_number(text: str):
5
  mapping = {
6
  "one": 1, "two": 2, "three": 3, "four": 4, "five": 5,
 
19
 
20
  return None
21
 
 
22
  def analyze_user_input(user_input: str, state: dict) -> dict:
23
 
24
  text = user_input.lower()
25
 
26
+
27
+
28
+
29
  if any(w in text for w in ["idea", "project", "suggest", "recommend"]):
30
  intent = "idea"
31
 
 
38
  else:
39
  intent = "chat"
40
 
41
+
42
+
43
+
44
  domain = extract_domain(text)
45
 
46
+
47
+
48
+
49
  number = word_to_number(text)
50
 
51
+
52
+
53
+
54
  project_title = None
55
 
56
  if intent == "chat" and len(text.split()) > 4:
 
61
  "domain": domain,
62
  "project_title": project_title,
63
  "number": number
64
+ }
src/recommendation_engine/local_llm.py CHANGED
@@ -1,17 +1,15 @@
1
-
2
  import requests
3
  import time
4
 
5
  OLLAMA_URL = "http://localhost:11434/api/generate"
6
  MODEL_NAME = "mistral"
7
 
8
-
9
  def generate_local(prompt: str, retries: int = 2) -> str:
10
  """
11
  Stable + fast local LLM call (Ollama)
12
  """
13
 
14
- # 🔥 cut prompt (مهم جدًا للسرعة)
15
  prompt = prompt[:1200]
16
 
17
  payload = {
@@ -20,7 +18,7 @@ def generate_local(prompt: str, retries: int = 2) -> str:
20
  "stream": False,
21
  "options": {
22
  "temperature": 0.7,
23
- "num_predict": 150, # يقلل الوقت
24
  "top_p": 0.9
25
  }
26
  }
@@ -37,13 +35,13 @@ def generate_local(prompt: str, retries: int = 2) -> str:
37
 
38
  duration = round(time.time() - start_time, 2)
39
 
40
- # ❌ bad status
41
  if response.status_code != 200:
42
  print(f"❌ Local status {response.status_code}")
43
  time.sleep(1)
44
  continue
45
 
46
- # ❌ invalid json
47
  try:
48
  data = response.json()
49
  except Exception:
@@ -53,7 +51,7 @@ def generate_local(prompt: str, retries: int = 2) -> str:
53
 
54
  result = data.get("response", "").strip()
55
 
56
- # ❌ empty response
57
  if not result or len(result) < 10:
58
  print(f"⚠️ Empty/short response ({duration}s)")
59
  time.sleep(1)
@@ -70,7 +68,6 @@ def generate_local(prompt: str, retries: int = 2) -> str:
70
  print("❌ Local error:", e)
71
  time.sleep(1)
72
 
73
- # ❌ failed completely
74
  print("🚨 Local LLM failed → fallback")
75
  return ""
76
-
 
 
1
  import requests
2
  import time
3
 
4
  OLLAMA_URL = "http://localhost:11434/api/generate"
5
  MODEL_NAME = "mistral"
6
 
 
7
  def generate_local(prompt: str, retries: int = 2) -> str:
8
  """
9
  Stable + fast local LLM call (Ollama)
10
  """
11
 
12
+
13
  prompt = prompt[:1200]
14
 
15
  payload = {
 
18
  "stream": False,
19
  "options": {
20
  "temperature": 0.7,
21
+ "num_predict": 150,
22
  "top_p": 0.9
23
  }
24
  }
 
35
 
36
  duration = round(time.time() - start_time, 2)
37
 
38
+
39
  if response.status_code != 200:
40
  print(f"❌ Local status {response.status_code}")
41
  time.sleep(1)
42
  continue
43
 
44
+
45
  try:
46
  data = response.json()
47
  except Exception:
 
51
 
52
  result = data.get("response", "").strip()
53
 
54
+
55
  if not result or len(result) < 10:
56
  print(f"⚠️ Empty/short response ({duration}s)")
57
  time.sleep(1)
 
68
  print("❌ Local error:", e)
69
  time.sleep(1)
70
 
71
+
72
  print("🚨 Local LLM failed → fallback")
73
  return ""
 
src/recommendation_engine/memory_store.py CHANGED
@@ -1,19 +1,13 @@
1
- # src/recommendation_engine/memory_store.py
2
-
3
  import time
4
  import uuid
5
  from copy import deepcopy
6
 
7
  MEMORY = {}
8
 
9
- TTL = 86400 # 24 hours
10
  MAX_HISTORY = 20
11
  MAX_CHATS = 10
12
 
13
-
14
- # =====================================================
15
- # Default State
16
- # =====================================================
17
  def default_state():
18
  return {
19
  "project_title": "",
@@ -31,10 +25,6 @@ def default_state():
31
  "category": ""
32
  }
33
 
34
-
35
- # =====================================================
36
- # Create New Chat
37
- # =====================================================
38
  def create_chat(user_id: str):
39
 
40
  user = MEMORY.setdefault(user_id, {
@@ -62,10 +52,6 @@ def create_chat(user_id: str):
62
 
63
  return chat_id
64
 
65
-
66
- # =====================================================
67
- # Get Active Chat
68
- # =====================================================
69
  def get_user_memory(user_id: str):
70
 
71
  user = MEMORY.get(user_id)
@@ -74,7 +60,7 @@ def get_user_memory(user_id: str):
74
  create_chat(user_id)
75
  user = MEMORY[user_id]
76
 
77
- # TTL reset
78
  if time.time() - user["timestamp"] > TTL:
79
  MEMORY[user_id] = {}
80
  create_chat(user_id)
@@ -87,10 +73,6 @@ def get_user_memory(user_id: str):
87
 
88
  return user["chats"][chat_id]
89
 
90
-
91
- # =====================================================
92
- # Merge State (FIXED 🔥)
93
- # =====================================================
94
  def merge_state(old: dict, new: dict):
95
 
96
  merged = deepcopy(old)
@@ -100,12 +82,12 @@ def merge_state(old: dict, new: dict):
100
  if value is None:
101
  continue
102
 
103
- # 🔥 overwrite critical fields
104
  if key in ["project_title", "description"]:
105
  merged[key] = value
106
  continue
107
 
108
- # 🔥 lists → replace if empty OR extend uniquely
109
  if isinstance(value, list):
110
 
111
  if not value:
@@ -123,7 +105,7 @@ def merge_state(old: dict, new: dict):
123
  seen.add(norm)
124
  combined.append(item)
125
 
126
- # limit growth
127
  merged[key] = combined[:20]
128
 
129
  else:
@@ -131,10 +113,6 @@ def merge_state(old: dict, new: dict):
131
 
132
  return merged
133
 
134
-
135
- # =====================================================
136
- # Save Chat
137
- # =====================================================
138
  def save_user_memory(user_id: str, data: dict):
139
 
140
  user = MEMORY.get(user_id)
@@ -157,10 +135,6 @@ def save_user_memory(user_id: str, data: dict):
157
 
158
  user["timestamp"] = time.time()
159
 
160
-
161
- # =====================================================
162
- # Get All Chats
163
- # =====================================================
164
  def get_all_chats(user_id: str):
165
 
166
  user = MEMORY.get(user_id, {})
@@ -175,10 +149,6 @@ def get_all_chats(user_id: str):
175
  for chat_id, chat in chats.items()
176
  ]
177
 
178
-
179
- # =====================================================
180
- # Switch Chat
181
- # =====================================================
182
  def switch_chat(user_id: str, chat_id: str):
183
 
184
  user = MEMORY.get(user_id)
@@ -189,11 +159,7 @@ def switch_chat(user_id: str, chat_id: str):
189
  if chat_id in user.get("chats", {}):
190
  user["active_chat_id"] = chat_id
191
 
192
-
193
- # =====================================================
194
- # Clear User
195
- # =====================================================
196
  def clear_user_memory(user_id: str):
197
 
198
  if user_id in MEMORY:
199
- del MEMORY[user_id]
 
 
 
1
  import time
2
  import uuid
3
  from copy import deepcopy
4
 
5
  MEMORY = {}
6
 
7
+ TTL = 86400
8
  MAX_HISTORY = 20
9
  MAX_CHATS = 10
10
 
 
 
 
 
11
  def default_state():
12
  return {
13
  "project_title": "",
 
25
  "category": ""
26
  }
27
 
 
 
 
 
28
  def create_chat(user_id: str):
29
 
30
  user = MEMORY.setdefault(user_id, {
 
52
 
53
  return chat_id
54
 
 
 
 
 
55
  def get_user_memory(user_id: str):
56
 
57
  user = MEMORY.get(user_id)
 
60
  create_chat(user_id)
61
  user = MEMORY[user_id]
62
 
63
+
64
  if time.time() - user["timestamp"] > TTL:
65
  MEMORY[user_id] = {}
66
  create_chat(user_id)
 
73
 
74
  return user["chats"][chat_id]
75
 
 
 
 
 
76
  def merge_state(old: dict, new: dict):
77
 
78
  merged = deepcopy(old)
 
82
  if value is None:
83
  continue
84
 
85
+
86
  if key in ["project_title", "description"]:
87
  merged[key] = value
88
  continue
89
 
90
+
91
  if isinstance(value, list):
92
 
93
  if not value:
 
105
  seen.add(norm)
106
  combined.append(item)
107
 
108
+
109
  merged[key] = combined[:20]
110
 
111
  else:
 
113
 
114
  return merged
115
 
 
 
 
 
116
  def save_user_memory(user_id: str, data: dict):
117
 
118
  user = MEMORY.get(user_id)
 
135
 
136
  user["timestamp"] = time.time()
137
 
 
 
 
 
138
  def get_all_chats(user_id: str):
139
 
140
  user = MEMORY.get(user_id, {})
 
149
  for chat_id, chat in chats.items()
150
  ]
151
 
 
 
 
 
152
  def switch_chat(user_id: str, chat_id: str):
153
 
154
  user = MEMORY.get(user_id)
 
159
  if chat_id in user.get("chats", {}):
160
  user["active_chat_id"] = chat_id
161
 
 
 
 
 
162
  def clear_user_memory(user_id: str):
163
 
164
  if user_id in MEMORY:
165
+ del MEMORY[user_id]
src/recommendation_engine/novelty_checker.py CHANGED
@@ -1,5 +1,3 @@
1
- # src/recommendation_engine/novelty_checker.py
2
-
3
  import logging
4
  import re
5
 
@@ -18,10 +16,6 @@ from src.recommendation_engine.config import (
18
 
19
  logger = logging.getLogger(__name__)
20
 
21
-
22
- # =====================================================
23
- # Generic weak patterns
24
- # =====================================================
25
  GENERIC_PATTERNS = [
26
  "dashboard",
27
  "platform",
@@ -34,10 +28,6 @@ GENERIC_PATTERNS = [
34
  "management system"
35
  ]
36
 
37
-
38
- # =====================================================
39
- # Text normalization
40
- # =====================================================
41
  def normalize(text: str) -> str:
42
 
43
  text = str(text).lower().strip()
@@ -48,10 +38,6 @@ def normalize(text: str) -> str:
48
 
49
  return text
50
 
51
-
52
- # =====================================================
53
- # Generic detector
54
- # =====================================================
55
  def is_generic(text: str) -> bool:
56
 
57
  low = normalize(text)
@@ -63,10 +49,6 @@ def is_generic(text: str) -> bool:
63
 
64
  return False
65
 
66
-
67
- # =====================================================
68
- # Token overlap similarity
69
- # =====================================================
70
  def token_overlap_score(a: str, b: str) -> float:
71
 
72
  a_tokens = set(normalize(a).split())
@@ -82,10 +64,6 @@ def token_overlap_score(a: str, b: str) -> float:
82
 
83
  return overlap / union
84
 
85
-
86
- # =====================================================
87
- # Feature Novelty
88
- # =====================================================
89
  def is_feature_novel(
90
  feature: str,
91
  existing_features: List[str]
@@ -96,18 +74,18 @@ def is_feature_novel(
96
  if not feature:
97
  return False
98
 
99
- # =========================================
100
- # Reject generic weak features
101
- # =========================================
102
  if is_generic(feature):
103
 
104
  logger.debug(f"[GENERIC FEATURE] {feature}")
105
 
106
  return False
107
 
108
- # =========================================
109
- # Exact duplicate
110
- # =========================================
111
  existing_norm = [
112
  normalize(f)
113
  for f in existing_features
@@ -119,27 +97,27 @@ def is_feature_novel(
119
 
120
  return False
121
 
122
- # =========================================
123
- # Semantic comparison
124
- # =========================================
125
  for old in existing_norm:
126
 
127
  if not old:
128
  continue
129
 
130
- # embedding similarity
131
  semantic_score = compare_two_ideas(
132
  feature,
133
  old
134
  )
135
 
136
- # token overlap
137
  overlap_score = token_overlap_score(
138
  feature,
139
  old
140
  )
141
 
142
- # combined reasoning
143
  final_score = max(
144
  semantic_score,
145
  overlap_score
@@ -151,7 +129,7 @@ def is_feature_novel(
151
  f"overlap={overlap_score:.2f})"
152
  )
153
 
154
- # stricter threshold
155
  if final_score >= (
156
  FEATURE_DUPLICATE_THRESHOLD + 0.08
157
  ):
@@ -165,10 +143,6 @@ def is_feature_novel(
165
 
166
  return True
167
 
168
-
169
- # =====================================================
170
- # Duplicate Feature Filter
171
- # =====================================================
172
  def filter_duplicate_features(
173
  generated_features: List[str],
174
  existing_features: List[str]
@@ -202,10 +176,6 @@ def filter_duplicate_features(
202
 
203
  return final
204
 
205
-
206
- # =====================================================
207
- # Cached DB originality check
208
- # =====================================================
209
  @lru_cache(maxsize=256)
210
  def _cached_db_check(idea: str) -> float:
211
 
@@ -234,20 +204,16 @@ def _cached_db_check(idea: str) -> float:
234
 
235
  if scores:
236
 
237
- # strongest similarity
238
  return max(scores)
239
 
240
  except Exception as e:
241
 
242
  logger.warning(f"[DB ERROR] {e}")
243
 
244
- # safe fallback
245
  return 0.0
246
 
247
-
248
- # =====================================================
249
- # Idea Novelty
250
- # =====================================================
251
  def is_idea_novel(idea_title: str) -> bool:
252
 
253
  idea_title = normalize(idea_title)
@@ -271,10 +237,6 @@ def is_idea_novel(idea_title: str) -> bool:
271
 
272
  return score < IDEA_DUPLICATE_THRESHOLD
273
 
274
-
275
- # =====================================================
276
- # Optional novelty score
277
- # =====================================================
278
  def score_feature_novelty(
279
  feature: str,
280
  existing_features: List[str]
@@ -311,4 +273,4 @@ def score_feature_novelty(
311
  return round(
312
  1.0 - max(scores),
313
  4
314
- )
 
 
 
1
  import logging
2
  import re
3
 
 
16
 
17
  logger = logging.getLogger(__name__)
18
 
 
 
 
 
19
  GENERIC_PATTERNS = [
20
  "dashboard",
21
  "platform",
 
28
  "management system"
29
  ]
30
 
 
 
 
 
31
  def normalize(text: str) -> str:
32
 
33
  text = str(text).lower().strip()
 
38
 
39
  return text
40
 
 
 
 
 
41
  def is_generic(text: str) -> bool:
42
 
43
  low = normalize(text)
 
49
 
50
  return False
51
 
 
 
 
 
52
  def token_overlap_score(a: str, b: str) -> float:
53
 
54
  a_tokens = set(normalize(a).split())
 
64
 
65
  return overlap / union
66
 
 
 
 
 
67
  def is_feature_novel(
68
  feature: str,
69
  existing_features: List[str]
 
74
  if not feature:
75
  return False
76
 
77
+
78
+
79
+
80
  if is_generic(feature):
81
 
82
  logger.debug(f"[GENERIC FEATURE] {feature}")
83
 
84
  return False
85
 
86
+
87
+
88
+
89
  existing_norm = [
90
  normalize(f)
91
  for f in existing_features
 
97
 
98
  return False
99
 
100
+
101
+
102
+
103
  for old in existing_norm:
104
 
105
  if not old:
106
  continue
107
 
108
+
109
  semantic_score = compare_two_ideas(
110
  feature,
111
  old
112
  )
113
 
114
+
115
  overlap_score = token_overlap_score(
116
  feature,
117
  old
118
  )
119
 
120
+
121
  final_score = max(
122
  semantic_score,
123
  overlap_score
 
129
  f"overlap={overlap_score:.2f})"
130
  )
131
 
132
+
133
  if final_score >= (
134
  FEATURE_DUPLICATE_THRESHOLD + 0.08
135
  ):
 
143
 
144
  return True
145
 
 
 
 
 
146
  def filter_duplicate_features(
147
  generated_features: List[str],
148
  existing_features: List[str]
 
176
 
177
  return final
178
 
 
 
 
 
179
  @lru_cache(maxsize=256)
180
  def _cached_db_check(idea: str) -> float:
181
 
 
204
 
205
  if scores:
206
 
207
+
208
  return max(scores)
209
 
210
  except Exception as e:
211
 
212
  logger.warning(f"[DB ERROR] {e}")
213
 
214
+
215
  return 0.0
216
 
 
 
 
 
217
  def is_idea_novel(idea_title: str) -> bool:
218
 
219
  idea_title = normalize(idea_title)
 
237
 
238
  return score < IDEA_DUPLICATE_THRESHOLD
239
 
 
 
 
 
240
  def score_feature_novelty(
241
  feature: str,
242
  existing_features: List[str]
 
273
  return round(
274
  1.0 - max(scores),
275
  4
276
+ )
src/recommendation_engine/prompt_builder.py CHANGED
@@ -1,11 +1,5 @@
1
- # src/recommendation_engine/prompt_builder.py
2
-
3
  from typing import Dict, Any, List
4
 
5
-
6
- # =====================================================
7
- # Helpers
8
- # =====================================================
9
  def list_to_text(items: List[str], max_items: int = 10) -> str:
10
 
11
  if not items:
@@ -29,10 +23,6 @@ def list_to_text(items: List[str], max_items: int = 10) -> str:
29
 
30
  return ", ".join(cleaned)
31
 
32
-
33
- # =====================================================
34
- # Feature Prompt
35
- # =====================================================
36
  def build_feature_prompt(
37
  context: Dict[str, Any],
38
  count: int = 10,
@@ -108,10 +98,6 @@ BAD FEATURE EXAMPLES:
108
  OUTPUT:
109
  """.strip()
110
 
111
-
112
- # =====================================================
113
- # Idea Prompt
114
- # =====================================================
115
  def build_idea_prompt(
116
  context: Dict[str, Any],
117
  count: int = 10,
@@ -177,10 +163,6 @@ BAD IDEA EXAMPLES:
177
  OUTPUT:
178
  """.strip()
179
 
180
-
181
- # =====================================================
182
- # Description Prompt
183
- # =====================================================
184
  def build_description_prompt(context: Dict[str, Any]) -> str:
185
 
186
  return f"""
@@ -214,10 +196,6 @@ OUTPUT:
214
  Professional structured paragraph
215
  """.strip()
216
 
217
-
218
- # =====================================================
219
- # Chat Prompt
220
- # =====================================================
221
  def build_chat_prompt(context: Dict[str, Any]) -> str:
222
 
223
  return f"""
@@ -250,10 +228,6 @@ RULES:
250
  - Keep all answers related to the current project
251
  """.strip()
252
 
253
-
254
- # =====================================================
255
- # Full Project Prompt
256
- # =====================================================
257
  def build_full_project_prompt(context):
258
 
259
  title = context.get("project_title", "")
@@ -349,4 +323,4 @@ FUTURE_WORK:
349
 
350
  METHODOLOGY:
351
  (step-by-step implementation process)
352
- """.strip()
 
 
 
1
  from typing import Dict, Any, List
2
 
 
 
 
 
3
  def list_to_text(items: List[str], max_items: int = 10) -> str:
4
 
5
  if not items:
 
23
 
24
  return ", ".join(cleaned)
25
 
 
 
 
 
26
  def build_feature_prompt(
27
  context: Dict[str, Any],
28
  count: int = 10,
 
98
  OUTPUT:
99
  """.strip()
100
 
 
 
 
 
101
  def build_idea_prompt(
102
  context: Dict[str, Any],
103
  count: int = 10,
 
163
  OUTPUT:
164
  """.strip()
165
 
 
 
 
 
166
  def build_description_prompt(context: Dict[str, Any]) -> str:
167
 
168
  return f"""
 
196
  Professional structured paragraph
197
  """.strip()
198
 
 
 
 
 
199
  def build_chat_prompt(context: Dict[str, Any]) -> str:
200
 
201
  return f"""
 
228
  - Keep all answers related to the current project
229
  """.strip()
230
 
 
 
 
 
231
  def build_full_project_prompt(context):
232
 
233
  title = context.get("project_title", "")
 
323
 
324
  METHODOLOGY:
325
  (step-by-step implementation process)
326
+ """.strip()
src/recommendation_engine/response_formatter.py CHANGED
@@ -1,11 +1,5 @@
1
- # src/recommendation_engine/response_formatter.py
2
-
3
  from typing import Dict, Any, List
4
 
5
-
6
- # =====================================================
7
- # Helpers
8
- # =====================================================
9
  def unique_items(items: List[str]) -> List[str]:
10
 
11
  seen = set()
@@ -25,7 +19,6 @@ def unique_items(items: List[str]) -> List[str]:
25
 
26
  return final
27
 
28
-
29
  def format_list(
30
  items: List[str],
31
  prefix: str = "- "
@@ -41,7 +34,6 @@ def format_list(
41
  for item in items
42
  )
43
 
44
-
45
  def clean_text(text: str) -> str:
46
 
47
  if not text:
@@ -49,7 +41,7 @@ def clean_text(text: str) -> str:
49
 
50
  text = str(text).strip()
51
 
52
- # remove duplicated assistant prefixes
53
  bad_prefixes = [
54
  "assistant:",
55
  "🤖 assistant:",
@@ -66,19 +58,14 @@ def clean_text(text: str) -> str:
66
 
67
  return text
68
 
69
-
70
  def divider() -> str:
71
  return "━━━━━━━━━━━━━━━━━━━━━━"
72
 
73
-
74
- # =====================================================
75
- # Dynamic Next Step
76
- # =====================================================
77
  def next_step(state: Dict[str, Any]) -> str:
78
 
79
- # =========================================
80
- # No project yet
81
- # =========================================
82
  if not state.get("project_title"):
83
 
84
  return (
@@ -86,9 +73,9 @@ def next_step(state: Dict[str, Any]) -> str:
86
  "or specify a domain."
87
  )
88
 
89
- # =========================================
90
- # No features yet
91
- # =========================================
92
  if not state.get("features"):
93
 
94
  return (
@@ -97,19 +84,15 @@ def next_step(state: Dict[str, Any]) -> str:
97
  "2️⃣ Generate another idea\n"
98
  )
99
 
100
- # =========================================
101
- # No description yet
102
- # =========================================
103
 
104
- # =========================================
105
- # Full project exists
106
- # =========================================
 
 
 
107
 
108
 
109
-
110
- # =====================================================
111
- # Full Project Overview
112
- # =====================================================
113
  def format_full_project(state: Dict[str, Any]) -> str:
114
 
115
  title = state.get("project_title", "Not defined")
@@ -156,10 +139,6 @@ def format_full_project(state: Dict[str, Any]) -> str:
156
  {context_strength}
157
  """.strip()
158
 
159
-
160
- # =====================================================
161
- # Main Formatter
162
- # =====================================================
163
  def format_response(
164
  intent: str,
165
  raw_response: str,
@@ -180,9 +159,9 @@ def format_response(
180
  state.get("description", "")
181
  )
182
 
183
- # =================================================
184
- # IDEA RESPONSE
185
- # =================================================
186
  if intent == "idea":
187
 
188
  if not ideas:
@@ -197,9 +176,9 @@ def format_response(
197
  {next_step(state)}
198
  """.strip()
199
 
200
- # =================================================
201
- # FEATURE RESPONSE
202
- # =================================================
203
  if intent == "feature":
204
 
205
  if not features:
@@ -215,9 +194,9 @@ def format_response(
215
  {next_step(state)}
216
  """.strip()
217
 
218
- # =================================================
219
- # DESCRIPTION RESPONSE
220
- # =================================================
221
  if intent == "description":
222
 
223
  return f"""
@@ -231,9 +210,9 @@ def format_response(
231
  {next_step(state)}
232
  """.strip()
233
 
234
- # =================================================
235
- # FULL PROJECT
236
- # =================================================
237
  if intent == "full_project":
238
 
239
  return f"""
@@ -243,9 +222,9 @@ def format_response(
243
  👉 You can continue improving the project.
244
  """.strip()
245
 
246
- # =================================================
247
- # CHAT RESPONSE
248
- # =================================================
249
  cleaned = clean_text(raw_response)
250
 
251
  return f"""
@@ -259,4 +238,4 @@ def format_response(
259
 
260
  {divider()}
261
  {next_step(state)}
262
- """.strip()
 
 
 
1
  from typing import Dict, Any, List
2
 
 
 
 
 
3
  def unique_items(items: List[str]) -> List[str]:
4
 
5
  seen = set()
 
19
 
20
  return final
21
 
 
22
  def format_list(
23
  items: List[str],
24
  prefix: str = "- "
 
34
  for item in items
35
  )
36
 
 
37
  def clean_text(text: str) -> str:
38
 
39
  if not text:
 
41
 
42
  text = str(text).strip()
43
 
44
+
45
  bad_prefixes = [
46
  "assistant:",
47
  "🤖 assistant:",
 
58
 
59
  return text
60
 
 
61
  def divider() -> str:
62
  return "━━━━━━━━━━━━━━━━━━━━━━"
63
 
 
 
 
 
64
  def next_step(state: Dict[str, Any]) -> str:
65
 
66
+
67
+
68
+
69
  if not state.get("project_title"):
70
 
71
  return (
 
73
  "or specify a domain."
74
  )
75
 
76
+
77
+
78
+
79
  if not state.get("features"):
80
 
81
  return (
 
84
  "2️⃣ Generate another idea\n"
85
  )
86
 
 
 
 
87
 
88
+
89
+
90
+
91
+
92
+
93
+
94
 
95
 
 
 
 
 
96
  def format_full_project(state: Dict[str, Any]) -> str:
97
 
98
  title = state.get("project_title", "Not defined")
 
139
  {context_strength}
140
  """.strip()
141
 
 
 
 
 
142
  def format_response(
143
  intent: str,
144
  raw_response: str,
 
159
  state.get("description", "")
160
  )
161
 
162
+
163
+
164
+
165
  if intent == "idea":
166
 
167
  if not ideas:
 
176
  {next_step(state)}
177
  """.strip()
178
 
179
+
180
+
181
+
182
  if intent == "feature":
183
 
184
  if not features:
 
194
  {next_step(state)}
195
  """.strip()
196
 
197
+
198
+
199
+
200
  if intent == "description":
201
 
202
  return f"""
 
210
  {next_step(state)}
211
  """.strip()
212
 
213
+
214
+
215
+
216
  if intent == "full_project":
217
 
218
  return f"""
 
222
  👉 You can continue improving the project.
223
  """.strip()
224
 
225
+
226
+
227
+
228
  cleaned = clean_text(raw_response)
229
 
230
  return f"""
 
238
 
239
  {divider()}
240
  {next_step(state)}
241
+ """.strip()
src/recommendation_engine/semantic_intent_classifier.py CHANGED
@@ -1,11 +1,5 @@
1
- # src/recommendation_engine/semantic_intent_classifier.py
2
-
3
  from src.recommendation_engine.llm_client import generate_text
4
 
5
-
6
- # =====================================================
7
- # Allowed intents
8
- # =====================================================
9
  VALID_INTENTS = {
10
  "idea",
11
  "feature",
@@ -13,10 +7,6 @@ VALID_INTENTS = {
13
  "chat"
14
  }
15
 
16
-
17
- # =====================================================
18
- # Semantic Intent Detection (🔥 upgraded)
19
- # =====================================================
20
  def detect_intent_semantic(user_input: str, state: dict) -> str:
21
 
22
  prompt = f"""
@@ -56,17 +46,17 @@ Return ONLY ONE WORD from the list.
56
 
57
  result = generate_text(prompt, task="intent").lower().strip()
58
 
59
- # =========================================
60
- # strict cleanup (🔥 important)
61
- # =========================================
62
  result = result.split()[0].strip()
63
 
64
  if result in VALID_INTENTS:
65
  return result
66
 
67
- # =========================================
68
- # fallback logic (🔥 critical safety)
69
- # =========================================
70
  text = user_input.lower()
71
 
72
  if any(w in text for w in ["idea", "project", "suggest"]):
@@ -78,4 +68,4 @@ Return ONLY ONE WORD from the list.
78
  if any(w in text for w in ["full", "all details", "complete"]):
79
  return "full_project"
80
 
81
- return "chat"
 
 
 
1
  from src.recommendation_engine.llm_client import generate_text
2
 
 
 
 
 
3
  VALID_INTENTS = {
4
  "idea",
5
  "feature",
 
7
  "chat"
8
  }
9
 
 
 
 
 
10
  def detect_intent_semantic(user_input: str, state: dict) -> str:
11
 
12
  prompt = f"""
 
46
 
47
  result = generate_text(prompt, task="intent").lower().strip()
48
 
49
+
50
+
51
+
52
  result = result.split()[0].strip()
53
 
54
  if result in VALID_INTENTS:
55
  return result
56
 
57
+
58
+
59
+
60
  text = user_input.lower()
61
 
62
  if any(w in text for w in ["idea", "project", "suggest"]):
 
68
  if any(w in text for w in ["full", "all details", "complete"]):
69
  return "full_project"
70
 
71
+ return "chat"
src/recommendation_engine/state_manager.py CHANGED
@@ -1,13 +1,8 @@
1
  from typing import Dict, Any, List
2
 
3
-
4
- # =====================================================
5
- # HELPERS
6
- # =====================================================
7
  def normalize(text: str) -> str:
8
  return str(text).strip().lower()
9
 
10
-
11
  def merge_lists(old: List[str], new: List[str]) -> List[str]:
12
 
13
  seen = set()
@@ -29,10 +24,6 @@ def merge_lists(old: List[str], new: List[str]) -> List[str]:
29
 
30
  return result
31
 
32
-
33
- # =====================================================
34
- # SMART UPDATE STATE
35
- # =====================================================
36
  def update_state(
37
  state: Dict[str, Any],
38
  new_data: Dict[str, Any],
@@ -47,9 +38,9 @@ def update_state(
47
  if value is None or value == "":
48
  continue
49
 
50
- # =========================================
51
- # ALWAYS REPLACE
52
- # =========================================
53
  if key in [
54
  "ideas",
55
  "project_title",
@@ -65,9 +56,9 @@ def update_state(
65
  state[key] = value
66
  continue
67
 
68
- # =========================================
69
- # FEATURES
70
- # =========================================
71
  if key == "features":
72
 
73
  if mode == "replace":
@@ -80,9 +71,9 @@ def update_state(
80
 
81
  continue
82
 
83
- # =========================================
84
- # LIST FIELDS
85
- # =========================================
86
  if isinstance(value, list):
87
 
88
  if mode == "replace":
@@ -98,9 +89,9 @@ def update_state(
98
 
99
  continue
100
 
101
- # =========================================
102
- # STRING FIELDS
103
- # =========================================
104
  if isinstance(value, str):
105
 
106
  clean_val = value.strip()
@@ -119,17 +110,13 @@ def update_state(
119
 
120
  continue
121
 
122
- # =========================================
123
- # OTHER TYPES
124
- # =========================================
125
  state[key] = value
126
 
127
  return state
128
 
129
-
130
- # =====================================================
131
- # RESET HELPERS
132
- # =====================================================
133
  def reset_for_new_idea(
134
  state: Dict[str, Any]
135
  ) -> Dict[str, Any]:
@@ -171,7 +158,6 @@ def reset_for_new_idea(
171
  "context_strength": None
172
  }
173
 
174
-
175
  def reset_features_only(
176
  state: Dict[str, Any]
177
  ) -> Dict[str, Any]:
@@ -180,4 +166,4 @@ def reset_features_only(
180
 
181
  state["description"] = ""
182
 
183
- return state
 
1
  from typing import Dict, Any, List
2
 
 
 
 
 
3
  def normalize(text: str) -> str:
4
  return str(text).strip().lower()
5
 
 
6
  def merge_lists(old: List[str], new: List[str]) -> List[str]:
7
 
8
  seen = set()
 
24
 
25
  return result
26
 
 
 
 
 
27
  def update_state(
28
  state: Dict[str, Any],
29
  new_data: Dict[str, Any],
 
38
  if value is None or value == "":
39
  continue
40
 
41
+
42
+
43
+
44
  if key in [
45
  "ideas",
46
  "project_title",
 
56
  state[key] = value
57
  continue
58
 
59
+
60
+
61
+
62
  if key == "features":
63
 
64
  if mode == "replace":
 
71
 
72
  continue
73
 
74
+
75
+
76
+
77
  if isinstance(value, list):
78
 
79
  if mode == "replace":
 
89
 
90
  continue
91
 
92
+
93
+
94
+
95
  if isinstance(value, str):
96
 
97
  clean_val = value.strip()
 
110
 
111
  continue
112
 
113
+
114
+
115
+
116
  state[key] = value
117
 
118
  return state
119
 
 
 
 
 
120
  def reset_for_new_idea(
121
  state: Dict[str, Any]
122
  ) -> Dict[str, Any]:
 
158
  "context_strength": None
159
  }
160
 
 
161
  def reset_features_only(
162
  state: Dict[str, Any]
163
  ) -> Dict[str, Any]:
 
166
 
167
  state["description"] = ""
168
 
169
+ return state
src/recommendation_engine/test.py DELETED
@@ -1,123 +0,0 @@
1
-
2
- ### idea generator test
3
-
4
- # from src.recommendation_engine.idea_generator import generate_ideas
5
-
6
- # result = generate_ideas(
7
- # domain="healthcare",
8
- # top_k=5
9
- # )
10
-
11
- # print("\nFINAL IDEAS:")
12
- # for i, idea in enumerate(result["final_ideas"], 1):
13
- # print(f"{i}. {idea}")
14
-
15
- ###featuregeneratoretest
16
-
17
- # from src.recommendation_engine.feature_generator import generate_features
18
-
19
- # # =========================================
20
- # # TEST 1: First generation
21
- # # =========================================
22
- # print("\n========== FIRST RUN ==========\n")
23
-
24
- # first = generate_features(
25
- # title="Smart Hospital System",
26
- # description="""
27
- # AI-based hospital system with chatbot,
28
- # patient management, QR booking,
29
- # and smart recommendations.
30
- # """,
31
- # features=["chatbot", "search", "qr booking"],
32
- # top_k=2
33
- # )
34
-
35
- # for i, f in enumerate(first["recommended_features"], 1):
36
- # print(f"{i}. {f}")
37
-
38
-
39
- # # =========================================
40
- # # TEST 2: Second generation (NO duplicates)
41
- # # =========================================
42
- # print("\n========== SECOND RUN (NO DUPLICATES) ==========\n")
43
-
44
- # second = generate_features(
45
- # title="Smart Hospital System",
46
- # description="""
47
- # AI-based hospital system with chatbot,
48
- # patient management, QR booking,
49
- # and smart recommendations.
50
- # """,
51
- # features=["chatbot", "search", "qr booking"],
52
- # previous_generated_features=first["recommended_features"],
53
- # top_k=2
54
- # )
55
-
56
- # for i, f in enumerate(second["recommended_features"], 1):
57
- # print(f"{i}. {f}")
58
-
59
-
60
- # # =========================================
61
- # # TEST 3: Check overlap manually
62
- # # =========================================
63
- # print("\n========== DUPLICATE CHECK ==========\n")
64
-
65
- # duplicates = set(first["recommended_features"]) & set(second["recommended_features"])
66
-
67
- # if duplicates:
68
- # print(" DUPLICATES FOUND:")
69
- # for d in duplicates:
70
- # print("-", d)
71
- # else:
72
- # print(" NO DUPLICATES — SYSTEM WORKING PERFECT")
73
-
74
-
75
- ## chatbot generator test
76
-
77
-
78
-
79
- # from src.recommendation_engine.chatbot_engine import chatbot
80
-
81
- # user_id = "user_1"
82
-
83
- # print("\n=== IDEA ===")
84
- # print(chatbot(user_id, "I want healthcare project idea"))
85
-
86
- # print("\n=== FEATURES ===")
87
- # print(chatbot(user_id, "add smart features"))
88
-
89
- # print("\n=== DESCRIPTION ===")
90
- # print(chatbot(user_id, "describe it"))
91
-
92
- # print("\n=== MEMORY TEST ===")
93
- # print(chatbot(user_id, "improve it more"))
94
-
95
-
96
-
97
-
98
- # run_test.py
99
-
100
- from src.recommendation_engine.chatbot_engine import chatbot
101
-
102
- USER_ID = "test_user"
103
-
104
-
105
-
106
- def interactive():
107
- print("\n===== INTERACTIVE MODE =====\n")
108
-
109
- while True:
110
- user_input = input("YOU: ")
111
-
112
- if user_input.lower() in ["exit", "quit"]:
113
- break
114
-
115
- response = chatbot(USER_ID, user_input)
116
-
117
- print("\nBOT:")
118
- print(response)
119
- print("\n----------------------\n")
120
-
121
-
122
-
123
- interactive()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/recommendation_engine/validator.py CHANGED
@@ -1,12 +1,6 @@
1
- # src/recommendation_engine/validator.py
2
-
3
  import re
4
  from typing import List
5
 
6
-
7
- # =====================================================
8
- # Generic / weak patterns
9
- # =====================================================
10
  GENERIC_PATTERNS = [
11
  "dashboard",
12
  "login",
@@ -27,7 +21,6 @@ GENERIC_PATTERNS = [
27
  "user management"
28
  ]
29
 
30
-
31
  BAD_STARTS = [
32
  "here are",
33
  "below are",
@@ -37,7 +30,6 @@ BAD_STARTS = [
37
  "features include"
38
  ]
39
 
40
-
41
  LOW_VALUE_WORDS = [
42
  "system",
43
  "platform",
@@ -46,10 +38,6 @@ LOW_VALUE_WORDS = [
46
  "solution"
47
  ]
48
 
49
-
50
- # =====================================================
51
- # CLEAN TEXT
52
- # =====================================================
53
  def clean_text(text: str) -> str:
54
 
55
  if not text:
@@ -57,40 +45,36 @@ def clean_text(text: str) -> str:
57
 
58
  text = str(text).strip()
59
 
60
- # remove numbering
61
  text = re.sub(r"^\d+[\)\.\-\s]+", "", text)
62
 
63
- # remove bullets
64
  text = re.sub(r"^[\-\*\•\→\▪\s]+", "", text)
65
 
66
- # remove markdown
67
  text = text.replace("**", "")
68
 
69
- # remove quotes
70
  text = text.replace('"', "").replace("'", "")
71
 
72
- # remove brackets
73
  text = re.sub(r"\(.*?\)", "", text)
74
 
75
- # remove long explanations after colon
76
  if ":" in text and len(text.split()) > 6:
77
  text = text.split(":")[0]
78
 
79
- # remove assistant prefixes
80
  text = re.sub(r"^(assistant|bot)\s*[:\-]\s*", "", text, flags=re.I)
81
 
82
- # remove trailing punctuation
83
  text = re.sub(r"[.,\-:;]+$", "", text)
84
 
85
- # normalize spaces
86
  text = re.sub(r"\s+", " ", text).strip()
87
 
88
  return text
89
 
90
-
91
- # =====================================================
92
- # NORMALIZATION
93
- # =====================================================
94
  def normalize_key(text: str) -> str:
95
 
96
  text = text.lower()
@@ -101,10 +85,6 @@ def normalize_key(text: str) -> str:
101
 
102
  return text
103
 
104
-
105
- # =====================================================
106
- # Generic detector
107
- # =====================================================
108
  def is_generic(text: str) -> bool:
109
 
110
  low = normalize_key(text)
@@ -115,29 +95,25 @@ def is_generic(text: str) -> bool:
115
 
116
  return False
117
 
118
-
119
- # =====================================================
120
- # Weak phrase detector
121
- # =====================================================
122
  def is_low_quality(text: str) -> bool:
123
 
124
  low = normalize_key(text)
125
 
126
  words = low.split()
127
 
128
- # too short
129
  if len(words) < 3:
130
  return True
131
 
132
- # too long
133
  if len(words) > 12:
134
  return True
135
 
136
- # starts badly
137
  if any(low.startswith(x) for x in BAD_STARTS):
138
  return True
139
 
140
- # mostly weak words
141
  weak_count = sum(
142
  1 for w in words
143
  if w in LOW_VALUE_WORDS
@@ -148,29 +124,21 @@ def is_low_quality(text: str) -> bool:
148
 
149
  return False
150
 
151
-
152
- # =====================================================
153
- # VALIDATION RULES
154
- # =====================================================
155
  def is_valid_item(text: str) -> bool:
156
 
157
  if not text:
158
  return False
159
 
160
- # reject generic garbage
161
  if is_generic(text):
162
  return False
163
 
164
- # reject low-quality phrases
165
  if is_low_quality(text):
166
  return False
167
 
168
  return True
169
 
170
-
171
- # =====================================================
172
- # FILTER ITEMS
173
- # =====================================================
174
  def filter_items(items: List[str]) -> List[str]:
175
 
176
  final = []
@@ -189,16 +157,16 @@ def filter_items(items: List[str]) -> List[str]:
189
 
190
  key = normalize_key(text)
191
 
192
- # exact duplicate
193
  if key in seen:
194
  continue
195
 
196
- # semantic-ish duplicate
197
  duplicate = False
198
 
199
  for old in seen:
200
 
201
- # overlap similarity
202
  overlap = set(key.split()) & set(old.split())
203
 
204
  if len(overlap) >= max(2, min(len(key.split()), len(old.split())) - 1):
@@ -214,10 +182,6 @@ def filter_items(items: List[str]) -> List[str]:
214
 
215
  return final
216
 
217
-
218
- # =====================================================
219
- # SMART SPLITTER
220
- # =====================================================
221
  def smart_split(text: str) -> List[str]:
222
 
223
  if not text:
@@ -234,7 +198,7 @@ def smart_split(text: str) -> List[str]:
234
  if not line:
235
  continue
236
 
237
- # split inline numbering
238
  parts = re.split(r"\d+[\.\)]\s*", line)
239
 
240
  for p in parts:
@@ -244,7 +208,7 @@ def smart_split(text: str) -> List[str]:
244
  if not p:
245
  continue
246
 
247
- # split bullets inline
248
  subparts = re.split(r"\s*[-•▪]\s*", p)
249
 
250
  for sp in subparts:
@@ -256,10 +220,6 @@ def smart_split(text: str) -> List[str]:
256
 
257
  return lines
258
 
259
-
260
- # =====================================================
261
- # MAIN VALIDATOR
262
- # =====================================================
263
  def validate_generated_list(
264
  text: str,
265
  top_k: int = 10
@@ -272,4 +232,4 @@ def validate_generated_list(
272
 
273
  cleaned = filter_items(raw_items)
274
 
275
- return cleaned[:top_k]
 
 
 
1
  import re
2
  from typing import List
3
 
 
 
 
 
4
  GENERIC_PATTERNS = [
5
  "dashboard",
6
  "login",
 
21
  "user management"
22
  ]
23
 
 
24
  BAD_STARTS = [
25
  "here are",
26
  "below are",
 
30
  "features include"
31
  ]
32
 
 
33
  LOW_VALUE_WORDS = [
34
  "system",
35
  "platform",
 
38
  "solution"
39
  ]
40
 
 
 
 
 
41
  def clean_text(text: str) -> str:
42
 
43
  if not text:
 
45
 
46
  text = str(text).strip()
47
 
48
+
49
  text = re.sub(r"^\d+[\)\.\-\s]+", "", text)
50
 
51
+
52
  text = re.sub(r"^[\-\*\•\→\▪\s]+", "", text)
53
 
54
+
55
  text = text.replace("**", "")
56
 
57
+
58
  text = text.replace('"', "").replace("'", "")
59
 
60
+
61
  text = re.sub(r"\(.*?\)", "", text)
62
 
63
+
64
  if ":" in text and len(text.split()) > 6:
65
  text = text.split(":")[0]
66
 
67
+
68
  text = re.sub(r"^(assistant|bot)\s*[:\-]\s*", "", text, flags=re.I)
69
 
70
+
71
  text = re.sub(r"[.,\-:;]+$", "", text)
72
 
73
+
74
  text = re.sub(r"\s+", " ", text).strip()
75
 
76
  return text
77
 
 
 
 
 
78
  def normalize_key(text: str) -> str:
79
 
80
  text = text.lower()
 
85
 
86
  return text
87
 
 
 
 
 
88
  def is_generic(text: str) -> bool:
89
 
90
  low = normalize_key(text)
 
95
 
96
  return False
97
 
 
 
 
 
98
  def is_low_quality(text: str) -> bool:
99
 
100
  low = normalize_key(text)
101
 
102
  words = low.split()
103
 
104
+
105
  if len(words) < 3:
106
  return True
107
 
108
+
109
  if len(words) > 12:
110
  return True
111
 
112
+
113
  if any(low.startswith(x) for x in BAD_STARTS):
114
  return True
115
 
116
+
117
  weak_count = sum(
118
  1 for w in words
119
  if w in LOW_VALUE_WORDS
 
124
 
125
  return False
126
 
 
 
 
 
127
  def is_valid_item(text: str) -> bool:
128
 
129
  if not text:
130
  return False
131
 
132
+
133
  if is_generic(text):
134
  return False
135
 
136
+
137
  if is_low_quality(text):
138
  return False
139
 
140
  return True
141
 
 
 
 
 
142
  def filter_items(items: List[str]) -> List[str]:
143
 
144
  final = []
 
157
 
158
  key = normalize_key(text)
159
 
160
+
161
  if key in seen:
162
  continue
163
 
164
+
165
  duplicate = False
166
 
167
  for old in seen:
168
 
169
+
170
  overlap = set(key.split()) & set(old.split())
171
 
172
  if len(overlap) >= max(2, min(len(key.split()), len(old.split())) - 1):
 
182
 
183
  return final
184
 
 
 
 
 
185
  def smart_split(text: str) -> List[str]:
186
 
187
  if not text:
 
198
  if not line:
199
  continue
200
 
201
+
202
  parts = re.split(r"\d+[\.\)]\s*", line)
203
 
204
  for p in parts:
 
208
  if not p:
209
  continue
210
 
211
+
212
  subparts = re.split(r"\s*[-•▪]\s*", p)
213
 
214
  for sp in subparts:
 
220
 
221
  return lines
222
 
 
 
 
 
223
  def validate_generated_list(
224
  text: str,
225
  top_k: int = 10
 
232
 
233
  cleaned = filter_items(raw_items)
234
 
235
+ return cleaned[:top_k]
src/similarity_model/__init__.py CHANGED
@@ -40,4 +40,4 @@ from .similarity_engine import (
40
 
41
  from .sync_projects import (
42
  sync_projects
43
- )
 
40
 
41
  from .sync_projects import (
42
  sync_projects
43
+ )
src/similarity_model/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/src/similarity_model/__pycache__/__init__.cpython-313.pyc and b/src/similarity_model/__pycache__/__init__.cpython-313.pyc differ
 
src/similarity_model/__pycache__/embedding_engine.cpython-313.pyc CHANGED
Binary files a/src/similarity_model/__pycache__/embedding_engine.cpython-313.pyc and b/src/similarity_model/__pycache__/embedding_engine.cpython-313.pyc differ
 
src/similarity_model/__pycache__/feature_similarity.cpython-313.pyc CHANGED
Binary files a/src/similarity_model/__pycache__/feature_similarity.cpython-313.pyc and b/src/similarity_model/__pycache__/feature_similarity.cpython-313.pyc differ
 
src/similarity_model/__pycache__/hybrid_ranker.cpython-313.pyc CHANGED
Binary files a/src/similarity_model/__pycache__/hybrid_ranker.cpython-313.pyc and b/src/similarity_model/__pycache__/hybrid_ranker.cpython-313.pyc differ
 
src/similarity_model/__pycache__/preprocessing.cpython-313.pyc CHANGED
Binary files a/src/similarity_model/__pycache__/preprocessing.cpython-313.pyc and b/src/similarity_model/__pycache__/preprocessing.cpython-313.pyc differ
 
src/similarity_model/__pycache__/semantic_search.cpython-313.pyc CHANGED
Binary files a/src/similarity_model/__pycache__/semantic_search.cpython-313.pyc and b/src/similarity_model/__pycache__/semantic_search.cpython-313.pyc differ
 
src/similarity_model/__pycache__/similarity_engine.cpython-313.pyc CHANGED
Binary files a/src/similarity_model/__pycache__/similarity_engine.cpython-313.pyc and b/src/similarity_model/__pycache__/similarity_engine.cpython-313.pyc differ
 
src/similarity_model/embedding_engine.py CHANGED
@@ -16,7 +16,6 @@ TEXT_COL = "clean_text"
16
  TITLE_COL = "project_title"
17
  TECH_COL = "technologies"
18
 
19
- # Resolve paths relative to the project root
20
  _PROJECT_ROOT = Path(__file__).resolve().parents[2]
21
  MODEL_DIR = _PROJECT_ROOT / "models"
22
  INDEX_PATH = MODEL_DIR / "faiss_index.bin"
@@ -97,4 +96,4 @@ def train_embedding_engine():
97
  engine.save_artifacts()
98
 
99
  logger.info("Embedding engine completed successfully.")
100
- return engine
 
16
  TITLE_COL = "project_title"
17
  TECH_COL = "technologies"
18
 
 
19
  _PROJECT_ROOT = Path(__file__).resolve().parents[2]
20
  MODEL_DIR = _PROJECT_ROOT / "models"
21
  INDEX_PATH = MODEL_DIR / "faiss_index.bin"
 
96
  engine.save_artifacts()
97
 
98
  logger.info("Embedding engine completed successfully.")
99
+ return engine
src/similarity_model/evaluation.py CHANGED
@@ -1,17 +1,9 @@
1
- # src/evaluation.py
2
- # FINAL PROFESSIONAL EVALUATION VERSION
3
- # Self Test + Real Query Test + Overall Judgment
4
-
5
  import logging
6
  import pandas as pd
7
 
8
  from src.similarity_model import find_similar_projects
9
  from src.similarity_model import load_metadata
10
 
11
-
12
- # =====================================================
13
- # Logging
14
- # =====================================================
15
  logging.basicConfig(
16
  level=logging.INFO,
17
  format="%(asctime)s | %(levelname)s | %(message)s"
@@ -21,9 +13,6 @@ logger = logging.getLogger(__name__)
21
  TOP_K = 5
22
  SELF_TEST_SAMPLES = 20
23
 
24
- # =====================================================
25
- # SELF RETRIEVAL TEST
26
- # =====================================================
27
  def run_self_test():
28
 
29
  df = load_metadata()
@@ -62,10 +51,6 @@ def run_self_test():
62
 
63
  return score
64
 
65
-
66
- # =====================================================
67
- # REAL QUERY TEST
68
- # =====================================================
69
  def run_real_queries():
70
 
71
  queries = [
@@ -165,10 +150,6 @@ def run_real_queries():
165
 
166
  return avg
167
 
168
-
169
- # =====================================================
170
- # FINAL JUDGMENT
171
- # =====================================================
172
  def final_status(
173
  self_score,
174
  query_score
@@ -197,18 +178,3 @@ def final_status(
197
 
198
  print("Overall Score:", round(final_score, 4))
199
  print("==============================\n")
200
-
201
-
202
- # =====================================================
203
- # RUN
204
- # =====================================================
205
- if __name__ == "__main__":
206
-
207
- self_score = run_self_test()
208
-
209
- query_score = run_real_queries()
210
-
211
- final_status(
212
- self_score,
213
- query_score
214
- )
 
 
 
 
 
1
  import logging
2
  import pandas as pd
3
 
4
  from src.similarity_model import find_similar_projects
5
  from src.similarity_model import load_metadata
6
 
 
 
 
 
7
  logging.basicConfig(
8
  level=logging.INFO,
9
  format="%(asctime)s | %(levelname)s | %(message)s"
 
13
  TOP_K = 5
14
  SELF_TEST_SAMPLES = 20
15
 
 
 
 
16
  def run_self_test():
17
 
18
  df = load_metadata()
 
51
 
52
  return score
53
 
 
 
 
 
54
  def run_real_queries():
55
 
56
  queries = [
 
150
 
151
  return avg
152
 
 
 
 
 
153
  def final_status(
154
  self_score,
155
  query_score
 
178
 
179
  print("Overall Score:", round(final_score, 4))
180
  print("==============================\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/similarity_model/feature_similarity.py CHANGED
@@ -125,7 +125,7 @@ def compute_feature_similarity(
125
 
126
  sim_matrix = cosine_similarity(emb_a, emb_b)
127
 
128
- # Hungarian match
129
  row_idx, col_idx = linear_sum_assignment(-sim_matrix)
130
 
131
  matches = []
 
125
 
126
  sim_matrix = cosine_similarity(emb_a, emb_b)
127
 
128
+
129
  row_idx, col_idx = linear_sum_assignment(-sim_matrix)
130
 
131
  matches = []
src/similarity_model/hybrid_ranker.py CHANGED
@@ -1,5 +1,3 @@
1
- # src/hybrid_ranker.py
2
-
3
  import logging
4
  from typing import List, Dict, Any
5
 
@@ -11,24 +9,15 @@ from src.similarity_model import (
11
  safe_feature_list
12
  )
13
 
14
- # =====================================================
15
- # Logging
16
- # =====================================================
17
  logging.basicConfig(
18
  level=logging.INFO,
19
  format="%(asctime)s | %(levelname)s | %(message)s"
20
  )
21
  logger = logging.getLogger(__name__)
22
 
23
- # =====================================================
24
- # Columns
25
- # =====================================================
26
  TITLE_COL = "project_title"
27
  FEATURE_COL = "features"
28
 
29
- # =====================================================
30
- # Weights / Thresholds
31
- # =====================================================
32
  DEFAULT_SEMANTIC_WEIGHT = 0.65
33
  DEFAULT_FEATURE_WEIGHT = 0.35
34
 
@@ -38,16 +27,12 @@ LOW_FEATURE_WEIGHT = 0.20
38
  BONUS_WEIGHT = 0.05
39
  MIN_HYBRID_SCORE = 0.05
40
 
41
- # =====================================================
42
- # Helpers
43
- # =====================================================
44
  def clamp(value: float) -> float:
45
  """
46
  Keep score inside [0,1]
47
  """
48
  return max(0.0, min(1.0, float(value)))
49
 
50
-
51
  def get_dynamic_weights(
52
  feature_count: int,
53
  coverage: float
@@ -57,21 +42,17 @@ def get_dynamic_weights(
57
  Returns (semantic_w, feature_w, coverage_w) — always sum to 1.0
58
  """
59
 
60
- # Rich features → trust feature matching more
61
  if feature_count >= 5 and coverage >= 0.60:
62
  return 0.40, 0.45, 0.15
63
 
64
- # Sparse features → trust semantic more
65
  if feature_count <= 2:
66
  return 0.70, 0.20, 0.10
67
 
68
- # Default balance
69
  return 0.55, 0.35, 0.10
70
 
71
-
72
- # =====================================================
73
- # Score Engines
74
- # =====================================================
75
  def compute_hybrid_score(
76
  semantic_score,
77
  feature_score,
@@ -86,15 +67,15 @@ def compute_hybrid_score(
86
 
87
  shared_count = feature_count - unique_query_count
88
 
89
- # ==========================================
90
- # Near-duplicate fast path
91
- # ==========================================
92
  if shared_count >= 6 and unique_query_count <= 1:
93
  return 0.95
94
 
95
- # ==========================================
96
- # Strong feature overlap fast path
97
- # ==========================================
98
  if coverage >= 0.90 and feature_score >= 0.65:
99
  return round(
100
  clamp(
@@ -105,15 +86,15 @@ def compute_hybrid_score(
105
  4
106
  )
107
 
108
- # ==========================================
109
- # No feature overlap → rely on semantics only
110
- # ==========================================
111
  if feature_score == 0 or coverage == 0:
112
  return round(clamp(0.60 * semantic_score), 4)
113
 
114
- # ==========================================
115
- # Normal scoring with dynamic weights
116
- # ==========================================
117
  semantic_w, feature_w, coverage_w = get_dynamic_weights(
118
  feature_count, coverage
119
  )
@@ -124,17 +105,16 @@ def compute_hybrid_score(
124
  coverage_w * coverage
125
  )
126
 
127
- # ==========================================
128
- # Penalty for weak feature overlap
129
- # (prevents generic keyword matches from scoring too high)
130
- # ==========================================
131
  if feature_score < 0.40:
132
  penalty = 0.50 + (feature_score / 0.40) * 0.50
133
  score *= penalty
134
 
135
  return round(clamp(score), 4)
136
 
137
-
138
  def compute_originality(
139
  hybrid_score: float,
140
  unique_query_features: int,
@@ -147,21 +127,20 @@ def compute_originality(
147
  hybrid_score = clamp(hybrid_score)
148
 
149
  if hybrid_score >= 0.39:
150
- # > 0.39 is a strong semantic match -> Originality 0-25%
151
  originality = 25.0 - ((hybrid_score - 0.39) / 0.11) * 25.0
152
  elif hybrid_score >= 0.35:
153
- # 0.35 to 0.39 is a moderate match -> Originality 25-75%
154
  originality = 75.0 - ((hybrid_score - 0.35) / 0.04) * 50.0
155
  elif hybrid_score >= 0.25:
156
- # 0.25 to 0.35 is a weak match -> Originality 75-95%
157
  originality = 95.0 - ((hybrid_score - 0.25) / 0.10) * 20.0
158
  else:
159
- # < 0.25 is effectively unrelated -> Originality 95-100%
160
  originality = 100.0 - (hybrid_score / 0.25) * 5.0
161
 
162
  return round(max(0.0, min(100.0, originality)), 2)
163
 
164
-
165
  def compute_confidence(
166
  semantic_score: float,
167
  feature_score: float,
@@ -179,7 +158,6 @@ def compute_confidence(
179
 
180
  return round(clamp(confidence), 4)
181
 
182
-
183
  def risk_label(score: float) -> str:
184
  """
185
  Duplicate risk label.
@@ -199,10 +177,6 @@ def risk_label(score: float) -> str:
199
 
200
  return "Very Low"
201
 
202
-
203
- # =====================================================
204
- # Core Comparison
205
- # =====================================================
206
  def compare_single_candidate(
207
  query_row: Dict[str, Any],
208
  candidate_row: Dict[str, Any],
@@ -301,10 +275,6 @@ def compare_single_candidate(
301
  candidate_features
302
  }
303
 
304
-
305
- # =====================================================
306
- # Rank Candidates
307
- # =====================================================
308
  def rank_candidates(
309
  query_row,
310
  candidate_rows,
@@ -354,52 +324,3 @@ def rank_candidates(
354
  ).reset_index(drop=True)
355
 
356
  return results
357
-
358
-
359
- # =====================================================
360
- # Example Run
361
- # =====================================================
362
- if __name__ == "__main__":
363
-
364
- query_project = {
365
- "project_title": "Clinic AI System",
366
- "features": [
367
- "appointment booking",
368
- "ai chatbot",
369
- "patient records",
370
- "doctor dashboard"
371
- ]
372
- }
373
-
374
- candidate_projects = [
375
- {
376
- "project_title":
377
- "Hospital Reservation Platform",
378
-
379
- "features": [
380
- "online booking",
381
- "chatbot assistant",
382
- "medical records",
383
- "analytics dashboard"
384
- ]
385
- },
386
- {
387
- "project_title":
388
- "Smart Farming Monitor",
389
-
390
- "features": [
391
- "crop analysis",
392
- "soil sensors"
393
- ]
394
- }
395
- ]
396
-
397
- semantic_scores = [0.84, 0.33]
398
-
399
- ranked = rank_candidates(
400
- query_row=query_project,
401
- candidate_rows=candidate_projects,
402
- semantic_scores=semantic_scores
403
- )
404
-
405
- print(ranked)
 
 
 
1
  import logging
2
  from typing import List, Dict, Any
3
 
 
9
  safe_feature_list
10
  )
11
 
 
 
 
12
  logging.basicConfig(
13
  level=logging.INFO,
14
  format="%(asctime)s | %(levelname)s | %(message)s"
15
  )
16
  logger = logging.getLogger(__name__)
17
 
 
 
 
18
  TITLE_COL = "project_title"
19
  FEATURE_COL = "features"
20
 
 
 
 
21
  DEFAULT_SEMANTIC_WEIGHT = 0.65
22
  DEFAULT_FEATURE_WEIGHT = 0.35
23
 
 
27
  BONUS_WEIGHT = 0.05
28
  MIN_HYBRID_SCORE = 0.05
29
 
 
 
 
30
  def clamp(value: float) -> float:
31
  """
32
  Keep score inside [0,1]
33
  """
34
  return max(0.0, min(1.0, float(value)))
35
 
 
36
  def get_dynamic_weights(
37
  feature_count: int,
38
  coverage: float
 
42
  Returns (semantic_w, feature_w, coverage_w) — always sum to 1.0
43
  """
44
 
45
+
46
  if feature_count >= 5 and coverage >= 0.60:
47
  return 0.40, 0.45, 0.15
48
 
49
+
50
  if feature_count <= 2:
51
  return 0.70, 0.20, 0.10
52
 
53
+
54
  return 0.55, 0.35, 0.10
55
 
 
 
 
 
56
  def compute_hybrid_score(
57
  semantic_score,
58
  feature_score,
 
67
 
68
  shared_count = feature_count - unique_query_count
69
 
70
+
71
+
72
+
73
  if shared_count >= 6 and unique_query_count <= 1:
74
  return 0.95
75
 
76
+
77
+
78
+
79
  if coverage >= 0.90 and feature_score >= 0.65:
80
  return round(
81
  clamp(
 
86
  4
87
  )
88
 
89
+
90
+
91
+
92
  if feature_score == 0 or coverage == 0:
93
  return round(clamp(0.60 * semantic_score), 4)
94
 
95
+
96
+
97
+
98
  semantic_w, feature_w, coverage_w = get_dynamic_weights(
99
  feature_count, coverage
100
  )
 
105
  coverage_w * coverage
106
  )
107
 
108
+
109
+
110
+
111
+
112
  if feature_score < 0.40:
113
  penalty = 0.50 + (feature_score / 0.40) * 0.50
114
  score *= penalty
115
 
116
  return round(clamp(score), 4)
117
 
 
118
  def compute_originality(
119
  hybrid_score: float,
120
  unique_query_features: int,
 
127
  hybrid_score = clamp(hybrid_score)
128
 
129
  if hybrid_score >= 0.39:
130
+
131
  originality = 25.0 - ((hybrid_score - 0.39) / 0.11) * 25.0
132
  elif hybrid_score >= 0.35:
133
+
134
  originality = 75.0 - ((hybrid_score - 0.35) / 0.04) * 50.0
135
  elif hybrid_score >= 0.25:
136
+
137
  originality = 95.0 - ((hybrid_score - 0.25) / 0.10) * 20.0
138
  else:
139
+
140
  originality = 100.0 - (hybrid_score / 0.25) * 5.0
141
 
142
  return round(max(0.0, min(100.0, originality)), 2)
143
 
 
144
  def compute_confidence(
145
  semantic_score: float,
146
  feature_score: float,
 
158
 
159
  return round(clamp(confidence), 4)
160
 
 
161
  def risk_label(score: float) -> str:
162
  """
163
  Duplicate risk label.
 
177
 
178
  return "Very Low"
179
 
 
 
 
 
180
  def compare_single_candidate(
181
  query_row: Dict[str, Any],
182
  candidate_row: Dict[str, Any],
 
275
  candidate_features
276
  }
277
 
 
 
 
 
278
  def rank_candidates(
279
  query_row,
280
  candidate_rows,
 
324
  ).reset_index(drop=True)
325
 
326
  return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/similarity_model/semantic_search.py CHANGED
@@ -107,8 +107,8 @@ def build_results(
107
 
108
  final_score = float(score)
109
 
110
- # ---------------------------------------------
111
- # ---------------------------------------------
112
  if query_words:
113
 
114
  title_words = tokenize(row[TITLE_COL])
 
107
 
108
  final_score = float(score)
109
 
110
+
111
+
112
  if query_words:
113
 
114
  title_words = tokenize(row[TITLE_COL])