Spaces:
Runtime error
Runtime error
update
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .env +1 -1
- src/recommendation_engine/__init__.py +17 -2
- src/recommendation_engine/__pycache__/__init__.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/chatbot_engine.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/command_handler.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/config.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/context_builder.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/feature_generator.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/full_project_generator.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/idea_generator.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/llm_client.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/llm_router.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/memory_store.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/novelty_checker.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/prompt_builder.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/response_formatter.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/state_manager.cpython-313.pyc +0 -0
- src/recommendation_engine/__pycache__/validator.cpython-313.pyc +0 -0
- src/recommendation_engine/chatbot_engine.py +161 -185
- src/recommendation_engine/command_handler.py +24 -34
- src/recommendation_engine/config.py +1 -47
- src/recommendation_engine/context_builder.py +24 -67
- src/recommendation_engine/feature_generator.py +38 -66
- src/recommendation_engine/full_project_generator.py +13 -25
- src/recommendation_engine/idea_generator.py +19 -40
- src/recommendation_engine/intent_classifier.py +18 -37
- src/recommendation_engine/llm_client.py +7 -45
- src/recommendation_engine/llm_router.py +13 -15
- src/recommendation_engine/local_llm.py +6 -9
- src/recommendation_engine/memory_store.py +6 -40
- src/recommendation_engine/novelty_checker.py +16 -54
- src/recommendation_engine/prompt_builder.py +1 -27
- src/recommendation_engine/response_formatter.py +29 -50
- src/recommendation_engine/semantic_intent_classifier.py +7 -17
- src/recommendation_engine/state_manager.py +16 -30
- src/recommendation_engine/test.py +0 -123
- src/recommendation_engine/validator.py +21 -61
- src/similarity_model/__init__.py +1 -1
- src/similarity_model/__pycache__/__init__.cpython-313.pyc +0 -0
- src/similarity_model/__pycache__/embedding_engine.cpython-313.pyc +0 -0
- src/similarity_model/__pycache__/feature_similarity.cpython-313.pyc +0 -0
- src/similarity_model/__pycache__/hybrid_ranker.cpython-313.pyc +0 -0
- src/similarity_model/__pycache__/preprocessing.cpython-313.pyc +0 -0
- src/similarity_model/__pycache__/semantic_search.cpython-313.pyc +0 -0
- src/similarity_model/__pycache__/similarity_engine.cpython-313.pyc +0 -0
- src/similarity_model/embedding_engine.py +1 -2
- src/similarity_model/evaluation.py +0 -34
- src/similarity_model/feature_similarity.py +1 -1
- src/similarity_model/hybrid_ranker.py +23 -102
- src/similarity_model/semantic_search.py +2 -2
.env
CHANGED
|
@@ -1,2 +1,2 @@
|
|
| 1 |
-
GEMINI_API_KEY=
|
| 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 |
-
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 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 |
-
|
| 107 |
if len(words) < 2:
|
| 108 |
return False
|
| 109 |
|
| 110 |
-
|
| 111 |
unique_ratio = len(set(words)) / len(words)
|
| 112 |
|
| 113 |
if unique_ratio < 0.5:
|
| 114 |
return False
|
| 115 |
|
| 116 |
-
|
| 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 |
-
|
| 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 |
-
|
| 154 |
keywords = {
|
| 155 |
|
| 156 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 258 |
if text in {"1", "2", "3"}:
|
| 259 |
return False
|
| 260 |
|
| 261 |
-
|
| 262 |
if len(text) < 3:
|
| 263 |
|
| 264 |
-
|
| 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 |
-
|
| 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 |
-
|
| 317 |
"project",
|
| 318 |
"system",
|
| 319 |
"platform",
|
|
@@ -323,7 +309,7 @@ def is_project_related(text: str) -> bool:
|
|
| 323 |
"dashboard",
|
| 324 |
"management",
|
| 325 |
|
| 326 |
-
|
| 327 |
"ai",
|
| 328 |
"ml",
|
| 329 |
"machine learning",
|
|
@@ -338,7 +324,7 @@ def is_project_related(text: str) -> bool:
|
|
| 338 |
"database",
|
| 339 |
"api",
|
| 340 |
|
| 341 |
-
|
| 342 |
"generate",
|
| 343 |
"feature",
|
| 344 |
"features",
|
|
@@ -349,7 +335,7 @@ def is_project_related(text: str) -> bool:
|
|
| 349 |
"technologies",
|
| 350 |
"architecture",
|
| 351 |
|
| 352 |
-
|
| 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 |
-
|
| 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 |
-
|
| 412 |
-
|
| 413 |
if text in {"1", "2", "3"}:
|
| 414 |
pass
|
| 415 |
-
|
| 416 |
-
|
| 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 |
-
|
| 430 |
-
|
| 431 |
if is_command(user_input):
|
| 432 |
return handle_command(user_input)
|
| 433 |
|
| 434 |
-
|
| 435 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 496 |
-
|
| 497 |
if state.get("project_title"):
|
| 498 |
|
| 499 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 781 |
-
# =========================================
|
| 782 |
elif any(w in text for w in full_project_words):
|
| 783 |
|
| 784 |
state["waiting_for_project_action"] = False
|
| 785 |
|
| 786 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 977 |
-
|
| 978 |
feature_patterns = [
|
| 979 |
-
|
| 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 |
-
|
| 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 |
-
|
| 995 |
"add features to",
|
| 996 |
"improve features for",
|
| 997 |
"feature ideas for",
|
| 998 |
"smart features for",
|
| 999 |
|
| 1000 |
-
|
| 1001 |
"features:",
|
| 1002 |
"project:",
|
| 1003 |
"idea:",
|
| 1004 |
|
| 1005 |
-
|
| 1006 |
"gen features for",
|
| 1007 |
"generate faetures for",
|
| 1008 |
"fetures for",
|
| 1009 |
|
| 1010 |
-
|
| 1011 |
"make features for",
|
| 1012 |
"build features for",
|
| 1013 |
"what features for",
|
| 1014 |
"best features for",
|
| 1015 |
|
| 1016 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1091 |
"generate features",
|
| 1092 |
"generate feature",
|
| 1093 |
"more features",
|
| 1094 |
"features",
|
| 1095 |
|
| 1096 |
-
|
| 1097 |
"generate full project",
|
| 1098 |
"full project",
|
| 1099 |
"complete project",
|
| 1100 |
|
| 1101 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1230 |
-
|
| 1231 |
if any(w in text for w in FOLLOWUP_WORDS):
|
| 1232 |
|
| 1233 |
-
|
| 1234 |
if state.get("last_action") == "idea":
|
| 1235 |
|
| 1236 |
analysis["intent"] = "idea"
|
| 1237 |
analysis["domain"] = state.get("domain")
|
| 1238 |
|
| 1239 |
-
|
| 1240 |
elif state.get("last_action") == "feature":
|
| 1241 |
|
| 1242 |
analysis["intent"] = "feature"
|
| 1243 |
|
| 1244 |
-
|
| 1245 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1315 |
-
|
| 1316 |
if not domain:
|
| 1317 |
domain = extract_domain(text)
|
| 1318 |
|
| 1319 |
-
|
| 1320 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1368 |
blocked_starts = (
|
| 1369 |
"how ",
|
| 1370 |
"what ",
|
|
@@ -1411,16 +1390,16 @@ Choose what you want:
|
|
| 1411 |
)
|
| 1412 |
):
|
| 1413 |
|
| 1414 |
-
|
| 1415 |
user_project = user_input.strip()
|
| 1416 |
-
|
| 1417 |
-
|
| 1418 |
-
|
| 1419 |
if user_project:
|
| 1420 |
|
| 1421 |
-
|
| 1422 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 1546 |
-
|
| 1547 |
elif intent == "feature":
|
| 1548 |
|
| 1549 |
-
|
| 1550 |
-
|
| 1551 |
-
|
| 1552 |
if analysis.get("project_title"):
|
| 1553 |
|
| 1554 |
state["project_title"] = (
|
|
@@ -1579,8 +1558,8 @@ Choose what you want:
|
|
| 1579 |
[]
|
| 1580 |
)
|
| 1581 |
|
| 1582 |
-
|
| 1583 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 21 |
-
|
| 22 |
if text in {"exit", "quit", "clear"}:
|
| 23 |
return True
|
| 24 |
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
command_patterns = [
|
| 29 |
-
r"^python\s+\S+\.py",
|
| 30 |
-
r"^pip\s+install",
|
| 31 |
-
r"^npm\s+install",
|
| 32 |
-
r"^node\s+\S+",
|
| 33 |
-
r"^cd\s+.+",
|
| 34 |
-
r"^ls\b",
|
| 35 |
-
r"^dir\b",
|
| 36 |
-
r"^git\s+.+",
|
| 37 |
-
r"^sudo\s+.+",
|
| 38 |
-
r".+\.py\s*$",
|
| 39 |
]
|
| 40 |
|
| 41 |
for pattern in command_patterns:
|
| 42 |
if re.match(pattern, text):
|
| 43 |
return True
|
| 44 |
|
| 45 |
-
|
| 46 |
-
|
| 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 |
-
|
| 65 |
if text in {"exit", "quit"}:
|
| 66 |
return "👋 Session ended. Start a new chat anytime."
|
| 67 |
|
| 68 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 192 |
if text in ["ai", "ml"]:
|
| 193 |
return "artificial intelligence"
|
| 194 |
|
| 195 |
-
|
| 196 |
if text in DOMAIN_KEYWORDS:
|
| 197 |
return text
|
| 198 |
|
| 199 |
-
|
| 200 |
domains = detect_domains(text)
|
| 201 |
|
| 202 |
if domains:
|
| 203 |
|
| 204 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 408 |
-
|
| 409 |
try:
|
| 410 |
|
| 411 |
results = cached_similarity(
|
|
@@ -421,9 +382,9 @@ def build_project_context(
|
|
| 421 |
|
| 422 |
results = None
|
| 423 |
|
| 424 |
-
|
| 425 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 229 |
-
|
| 230 |
prompt = build_feature_prompt(
|
| 231 |
context=context,
|
| 232 |
count=generation_count,
|
| 233 |
previous_features=previous_generated_features
|
| 234 |
)
|
| 235 |
|
| 236 |
-
|
| 237 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 279 |
if not is_feature_novel(feat, existing_features):
|
| 280 |
continue
|
| 281 |
|
| 282 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 145 |
-
|
| 146 |
"problem_statement":
|
| 147 |
extract_section(
|
| 148 |
raw,
|
|
@@ -168,9 +156,9 @@ def generate_full_project(
|
|
| 168 |
)
|
| 169 |
}
|
| 170 |
|
| 171 |
-
|
| 172 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 166 |
-
|
| 167 |
if normalized in DATA_TITLES:
|
| 168 |
logger.info(f"[SKIP DATASET EXACT] {idea}")
|
| 169 |
continue
|
| 170 |
|
| 171 |
-
|
| 172 |
skip_dataset_similar = False
|
| 173 |
-
for data_title in list(DATA_TITLES)[:500]:
|
| 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 |
-
|
| 183 |
if normalized in final_set:
|
| 184 |
continue
|
| 185 |
|
| 186 |
-
|
| 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 |
-
|
| 204 |
if is_duplicate_local(idea, final_set):
|
| 205 |
logger.info(f"[SKIP SIMILAR] {idea}")
|
| 206 |
continue
|
| 207 |
|
| 208 |
-
|
| 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 |
-
|
| 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 |
-
|
| 95 |
-
|
| 96 |
|
| 97 |
-
|
| 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 |
-
|
| 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 |
-
|
| 116 |
-
|
| 117 |
-
|
| 118 |
if not has_project and len(text_clean.split()) >= 3:
|
| 119 |
return "feature"
|
| 120 |
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
intent = classify_with_llm(text, state)
|
| 125 |
|
| 126 |
logger.info(f"[INTENT] {intent}")
|
| 127 |
|
| 128 |
-
|
| 129 |
-
|
| 130 |
-
|
| 131 |
if intent == "feature" and not has_project:
|
| 132 |
-
return "feature"
|
| 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 |
-
|
| 90 |
text = getattr(response, "text", None)
|
| 91 |
if text:
|
| 92 |
return text.strip()
|
| 93 |
|
| 94 |
-
|
| 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 |
-
|
| 150 |
if len(text) < 3:
|
| 151 |
return True
|
| 152 |
|
| 153 |
-
|
| 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 |
-
|
| 206 |
if is_bad_response(text):
|
| 207 |
logger.warning("[LLM] Weak response, using anyway")
|
| 208 |
-
return text
|
| 209 |
|
| 210 |
return text
|
| 211 |
|
|
@@ -216,7 +182,7 @@ def generate_text(
|
|
| 216 |
if provider_error:
|
| 217 |
raise provider_error
|
| 218 |
|
| 219 |
-
|
| 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 |
-
|
| 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 |
-
|
| 45 |
-
|
| 46 |
domain = extract_domain(text)
|
| 47 |
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
number = word_to_number(text)
|
| 52 |
|
| 53 |
-
|
| 54 |
-
|
| 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 |
-
|
| 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 |
-
|
| 41 |
if response.status_code != 200:
|
| 42 |
print(f"❌ Local status {response.status_code}")
|
| 43 |
time.sleep(1)
|
| 44 |
continue
|
| 45 |
|
| 46 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
| 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 |
-
|
| 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 |
-
|
| 104 |
if key in ["project_title", "description"]:
|
| 105 |
merged[key] = value
|
| 106 |
continue
|
| 107 |
|
| 108 |
-
|
| 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 |
-
|
| 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 |
-
|
| 101 |
-
|
| 102 |
if is_generic(feature):
|
| 103 |
|
| 104 |
logger.debug(f"[GENERIC FEATURE] {feature}")
|
| 105 |
|
| 106 |
return False
|
| 107 |
|
| 108 |
-
|
| 109 |
-
|
| 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 |
-
|
| 124 |
-
|
| 125 |
for old in existing_norm:
|
| 126 |
|
| 127 |
if not old:
|
| 128 |
continue
|
| 129 |
|
| 130 |
-
|
| 131 |
semantic_score = compare_two_ideas(
|
| 132 |
feature,
|
| 133 |
old
|
| 134 |
)
|
| 135 |
|
| 136 |
-
|
| 137 |
overlap_score = token_overlap_score(
|
| 138 |
feature,
|
| 139 |
old
|
| 140 |
)
|
| 141 |
|
| 142 |
-
|
| 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 |
-
|
| 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 |
-
|
| 238 |
return max(scores)
|
| 239 |
|
| 240 |
except Exception as e:
|
| 241 |
|
| 242 |
logger.warning(f"[DB ERROR] {e}")
|
| 243 |
|
| 244 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 61 |
-
|
| 62 |
result = result.split()[0].strip()
|
| 63 |
|
| 64 |
if result in VALID_INTENTS:
|
| 65 |
return result
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 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 |
-
|
| 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 |
-
|
| 70 |
-
|
| 71 |
if key == "features":
|
| 72 |
|
| 73 |
if mode == "replace":
|
|
@@ -80,9 +71,9 @@ def update_state(
|
|
| 80 |
|
| 81 |
continue
|
| 82 |
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
if isinstance(value, list):
|
| 87 |
|
| 88 |
if mode == "replace":
|
|
@@ -98,9 +89,9 @@ def update_state(
|
|
| 98 |
|
| 99 |
continue
|
| 100 |
|
| 101 |
-
|
| 102 |
-
|
| 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 |
-
|
| 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 |
-
|
| 61 |
text = re.sub(r"^\d+[\)\.\-\s]+", "", text)
|
| 62 |
|
| 63 |
-
|
| 64 |
text = re.sub(r"^[\-\*\•\→\▪\s]+", "", text)
|
| 65 |
|
| 66 |
-
|
| 67 |
text = text.replace("**", "")
|
| 68 |
|
| 69 |
-
|
| 70 |
text = text.replace('"', "").replace("'", "")
|
| 71 |
|
| 72 |
-
|
| 73 |
text = re.sub(r"\(.*?\)", "", text)
|
| 74 |
|
| 75 |
-
|
| 76 |
if ":" in text and len(text.split()) > 6:
|
| 77 |
text = text.split(":")[0]
|
| 78 |
|
| 79 |
-
|
| 80 |
text = re.sub(r"^(assistant|bot)\s*[:\-]\s*", "", text, flags=re.I)
|
| 81 |
|
| 82 |
-
|
| 83 |
text = re.sub(r"[.,\-:;]+$", "", text)
|
| 84 |
|
| 85 |
-
|
| 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 |
-
|
| 129 |
if len(words) < 3:
|
| 130 |
return True
|
| 131 |
|
| 132 |
-
|
| 133 |
if len(words) > 12:
|
| 134 |
return True
|
| 135 |
|
| 136 |
-
|
| 137 |
if any(low.startswith(x) for x in BAD_STARTS):
|
| 138 |
return True
|
| 139 |
|
| 140 |
-
|
| 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 |
-
|
| 161 |
if is_generic(text):
|
| 162 |
return False
|
| 163 |
|
| 164 |
-
|
| 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 |
-
|
| 193 |
if key in seen:
|
| 194 |
continue
|
| 195 |
|
| 196 |
-
|
| 197 |
duplicate = False
|
| 198 |
|
| 199 |
for old in seen:
|
| 200 |
|
| 201 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 61 |
if feature_count >= 5 and coverage >= 0.60:
|
| 62 |
return 0.40, 0.45, 0.15
|
| 63 |
|
| 64 |
-
|
| 65 |
if feature_count <= 2:
|
| 66 |
return 0.70, 0.20, 0.10
|
| 67 |
|
| 68 |
-
|
| 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 |
-
|
| 91 |
-
|
| 92 |
if shared_count >= 6 and unique_query_count <= 1:
|
| 93 |
return 0.95
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 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 |
-
|
| 110 |
-
|
| 111 |
if feature_score == 0 or coverage == 0:
|
| 112 |
return round(clamp(0.60 * semantic_score), 4)
|
| 113 |
|
| 114 |
-
|
| 115 |
-
|
| 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 |
-
|
| 129 |
-
|
| 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 |
-
|
| 151 |
originality = 25.0 - ((hybrid_score - 0.39) / 0.11) * 25.0
|
| 152 |
elif hybrid_score >= 0.35:
|
| 153 |
-
|
| 154 |
originality = 75.0 - ((hybrid_score - 0.35) / 0.04) * 50.0
|
| 155 |
elif hybrid_score >= 0.25:
|
| 156 |
-
|
| 157 |
originality = 95.0 - ((hybrid_score - 0.25) / 0.10) * 20.0
|
| 158 |
else:
|
| 159 |
-
|
| 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])
|