ibraheem007 commited on
Commit
c2b1f56
·
verified ·
1 Parent(s): d842754

Upload 5 files

Browse files
Files changed (5) hide show
  1. app.py +230 -0
  2. export_training_data_from_db.py +160 -0
  3. feedback.py +248 -0
  4. generator.py +1636 -0
  5. simulate_adapt.py +116 -0
app.py ADDED
@@ -0,0 +1,230 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import re
3
+ from components.session_manager import initialize_session_state, clear_session, save_current_to_history, get_or_create_user_id
4
+ from components.ui_components import render_header, render_sidebar
5
+ from components.student_flow import render_student_flow
6
+ from components.tutor_flow import render_tutor_flow
7
+ from components.output_renderer import render_output_section
8
+ from components.feedback_handler import render_feedback_section
9
+ from components.export_handler import render_export_section
10
+ from components.history_page import render_history_page
11
+
12
+ import base64
13
+
14
+ # Find where the validation error is coming from
15
+ original_b64decode = base64.b64decode
16
+
17
+ def debug_b64decode(data, *args, **kwargs):
18
+ try:
19
+ return original_b64decode(data, *args, **kwargs)
20
+ except Exception as e:
21
+ print(f"🚨 BASE64 DECODE ERROR: {e}")
22
+ print(f"🚨 Data type: {type(data)}")
23
+ print(f"🚨 Data length: {len(data) if data else 0}")
24
+ if data and isinstance(data, str):
25
+ print(f"🚨 Data preview: {data[:100]}...")
26
+ import traceback
27
+ traceback.print_stack()
28
+ raise
29
+
30
+ base64.b64decode = debug_b64decode
31
+
32
+ # Streamlit App Configuration
33
+ st.set_page_config(page_title="TailorED", layout="wide")
34
+
35
+ def scroll_to_top():
36
+ """Force scroll to top of page"""
37
+ st.components.v1.html("""
38
+ <script>
39
+ window.scrollTo(0, 0);
40
+ setTimeout(() => window.scrollTo(0, 0), 100);
41
+ setTimeout(() => window.scrollTo({top: 0, behavior: 'smooth'}), 200);
42
+ </script>
43
+ """, height=0)
44
+
45
+ def main():
46
+
47
+ try:
48
+ from db.connection import init_db
49
+ init_db()
50
+ except Exception as e:
51
+ st.error(f"❌ Database initialization failed: {e}")
52
+ st.stop()
53
+
54
+ # Initialize session state
55
+ initialize_session_state()
56
+
57
+ # Ensure user ID is stored in session
58
+ if "user_id" not in st.session_state:
59
+ st.session_state.user_id = get_or_create_user_id()
60
+
61
+ # Create a scroll anchor at the top
62
+ scroll_anchor = st.empty()
63
+
64
+ # Render header with navigation
65
+ render_header_with_nav()
66
+
67
+ # Render sidebar
68
+ render_sidebar()
69
+
70
+ # Handle model regeneration if needed
71
+ if st.session_state.get("regenerate_with_new_model"):
72
+ handle_regeneration()
73
+
74
+ # Main application logic based on current page
75
+ handle_page_navigation()
76
+
77
+ # Session management
78
+ handle_session_management()
79
+
80
+ # Force scroll to top after content generation
81
+ if st.session_state.get("generated_output") and not st.session_state.get("scrolled_to_top", False):
82
+ scroll_to_top()
83
+ st.session_state.scrolled_to_top = True
84
+
85
+ def render_header_with_nav():
86
+ st.title("🧠 TailorED - AI-Powered Educational Content Generator")
87
+
88
+ col1, col2, col3, col4 = st.columns([2, 1, 1, 1])
89
+
90
+ with col1:
91
+ st.caption("Create, manage, and access your educational content")
92
+
93
+ with col2:
94
+ if st.button("🔄 New Content", use_container_width=True, key="new_content_btn"):
95
+ st.session_state.current_page = "generator"
96
+ clear_session()
97
+ st.rerun()
98
+
99
+ with col3:
100
+ if st.button("📚 History", use_container_width=True, key="history_btn"):
101
+ st.session_state.current_page = "history"
102
+ # RELOAD HISTORY WHEN NAVIGATING TO HISTORY PAGE
103
+ from components.session_manager import load_user_history_from_db
104
+ load_user_history_from_db()
105
+ st.rerun()
106
+
107
+ with col4:
108
+ if st.button("🔬 Research", use_container_width=True, key="research_btn"):
109
+ st.session_state.current_page = "research"
110
+ st.rerun()
111
+
112
+ def handle_regeneration():
113
+ """Handle model regeneration when user switches models"""
114
+ if st.session_state.get("regenerate_with_new_model"):
115
+ # Clear the flag first to prevent loops
116
+ st.session_state.regenerate_with_new_model = False
117
+
118
+ # Show regeneration in progress
119
+ regeneration_status = st.empty()
120
+ regeneration_status.info("🔄 Regenerating content with new model...")
121
+
122
+ # Get the preserved context
123
+ user_type = st.session_state.user_type
124
+ student_level = st.session_state.student_level
125
+
126
+ # Trigger regeneration based on user type
127
+ if user_type == "student":
128
+ from components.student_flow import generate_student_content
129
+ content_text = st.session_state.get("original_content_text", "")
130
+ if content_text:
131
+ generate_student_content(content_text, student_level, "", "regenerated_content.pdf")
132
+ else:
133
+ from components.tutor_flow import generate_tutor_content
134
+ topic = st.session_state.get("original_topic", "")
135
+ objectives = st.session_state.get("original_objectives", "")
136
+ content_type = st.session_state.get("tutor_content_type", "Comprehensive Explanation")
137
+ if topic and objectives:
138
+ generate_tutor_content(topic, objectives, student_level, content_type, "")
139
+
140
+ regeneration_status.empty()
141
+
142
+ def handle_page_navigation():
143
+ current_page = st.session_state.get("current_page", "generator")
144
+
145
+ if current_page == "history":
146
+ # ENSURE HISTORY IS LOADED BEFORE RENDERING
147
+ from components.session_manager import load_user_history_from_db
148
+ load_user_history_from_db()
149
+ render_history_page()
150
+ elif current_page == "research":
151
+ try:
152
+ from components.research_dashboard import render_research_dashboard
153
+ render_research_dashboard()
154
+ except ImportError as e:
155
+ st.error("🔬 Research Dashboard - Import Error")
156
+ st.code(f"Error: {str(e)}")
157
+ st.info("""
158
+ **To fix this:**
159
+ 1. Make sure `components/research_dashboard.py` exists
160
+ 2. Check the file has no syntax errors
161
+ 3. Restart the Streamlit app
162
+ """)
163
+ except Exception as e:
164
+ st.error("🔬 Research Dashboard - Runtime Error")
165
+ st.code(f"Error: {str(e)}")
166
+ st.info("The research dashboard encountered an error while running.")
167
+ else:
168
+ handle_generator_flow()
169
+
170
+ def handle_generator_flow():
171
+ # DEBUG: Check what's in session state
172
+ print(f"🔍 DEBUG handle_generator_flow:")
173
+ print(f" - generated_output: {bool(st.session_state.get('generated_output'))}")
174
+ print(f" - regenerated: {st.session_state.get('regenerated', False)}")
175
+ print(f" - feedback_given: {st.session_state.get('feedback_given', False)}")
176
+ print(f" - pending_regeneration: {st.session_state.get('pending_regeneration', False)}")
177
+
178
+ # Handle pending regeneration FIRST in the generator flow
179
+ if st.session_state.get('pending_regeneration'):
180
+ print("🔄 DEBUG: Handling pending regeneration in generator flow")
181
+ from components.feedback_handler import handle_pending_regeneration
182
+ handle_pending_regeneration()
183
+
184
+ # Check if we have content to display - REGARDLESS of regeneration status
185
+ if st.session_state.get("generated_output"):
186
+ print("✅ DEBUG: Rendering content sections")
187
+ render_output_section()
188
+ render_export_section()
189
+ render_feedback_section()
190
+ return
191
+
192
+ # If no content, check if we have a user type selected
193
+ if not st.session_state.user_type:
194
+ render_user_selection()
195
+ return
196
+
197
+ # If user type is selected but no content, render the appropriate flow
198
+ if st.session_state.user_type == "student":
199
+ render_student_flow()
200
+ else:
201
+ render_tutor_flow()
202
+
203
+ def render_user_selection():
204
+ st.header("🎯 Welcome to TailorED!")
205
+ st.subheader("Are you a Student or Tutor?")
206
+
207
+ col1, col2 = st.columns(2)
208
+
209
+ with col1:
210
+ if st.button("🎓 I'm a Student", use_container_width=True, key="student_btn"):
211
+ st.session_state.user_type = "student"
212
+ st.session_state.scrolled_to_top = False
213
+ st.rerun()
214
+
215
+ with col2:
216
+ if st.button("👨‍🏫 I'm a Tutor", use_container_width=True, key="tutor_btn"):
217
+ st.session_state.user_type = "tutor"
218
+ st.session_state.scrolled_to_top = False
219
+ st.rerun()
220
+
221
+ def handle_session_management():
222
+ # Only show start over if we have content
223
+ if (st.session_state.get("current_page") == "generator" and
224
+ st.session_state.get("generated_output") and
225
+ st.button("🆕 Start Over", key="start_over_btn")):
226
+ clear_session()
227
+ st.rerun()
228
+
229
+ if __name__ == "__main__":
230
+ main()
export_training_data_from_db.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from db.connection import SessionLocal
2
+ from db.models import ContentHistory, Feedback
3
+ from sqlalchemy.orm import joinedload
4
+ import os
5
+ import json
6
+
7
+ MIN_CLARITY = 4
8
+ MIN_DEPTH = 4
9
+ MIN_COMMENT_LENGTH = 25
10
+
11
+ def is_high_quality(feedback, content_entry):
12
+ """Check if feedback meets high quality criteria for Groq content (fine-tuning data)"""
13
+ # Only use Groq content for fine-tuning (the established model)
14
+ if content_entry.generated_model != "groq":
15
+ print(f"❌ Skipping - not Groq content: {content_entry.generated_model}")
16
+ return False
17
+
18
+ # Quality criteria for fine-tuning data
19
+ if feedback.clarity < MIN_CLARITY:
20
+ print(f"❌ Clarity too low: {feedback.clarity} < {MIN_CLARITY}")
21
+ return False
22
+
23
+ if feedback.depth < MIN_DEPTH:
24
+ print(f"❌ Depth too low: {feedback.depth} < {MIN_DEPTH}")
25
+ return False
26
+
27
+ if feedback.complexity != "Just right":
28
+ print(f"❌ Complexity not 'Just right': {feedback.complexity}")
29
+ return False
30
+
31
+ comment_text = (feedback.comments or "").strip()
32
+ if len(comment_text) < MIN_COMMENT_LENGTH:
33
+ print(f"❌ Comment too short: {len(comment_text)} < {MIN_COMMENT_LENGTH}")
34
+ return False
35
+
36
+ print(f"✅ High-quality Groq feedback for fine-tuning: clarity={feedback.clarity}, depth={feedback.depth}")
37
+ return True
38
+
39
+ def format_training_example(entry, feedback):
40
+ """Format a training example from Groq content and feedback"""
41
+ if entry.user_type == "student":
42
+ return {
43
+ "instruction": f"Simplify the following content for a {entry.student_level} student: {entry.prompt.strip()}",
44
+ "input": f"Student Level: {entry.student_level}",
45
+ "output": entry.output.strip(),
46
+ "metadata": {
47
+ "user_type": "student",
48
+ "student_level": entry.student_level,
49
+ "clarity_score": feedback.clarity,
50
+ "depth_score": feedback.depth,
51
+ "complexity": feedback.complexity,
52
+ "comments": feedback.comments
53
+ }
54
+ }
55
+ elif entry.user_type == "tutor":
56
+ return {
57
+ "instruction": f"Create a {entry.content_type} about '{entry.topic}' for {entry.student_level} students.",
58
+ "input": f"Learning Objectives: {entry.prompt}",
59
+ "output": entry.output.strip(),
60
+ "metadata": {
61
+ "user_type": "tutor",
62
+ "content_type": entry.content_type,
63
+ "topic": entry.topic,
64
+ "student_level": entry.student_level,
65
+ "clarity_score": feedback.clarity,
66
+ "depth_score": feedback.depth,
67
+ "complexity": feedback.complexity,
68
+ "comments": feedback.comments
69
+ }
70
+ }
71
+ return None
72
+
73
+ def export_training_data_from_db(output_file="data/training/phi3_fine_tuning_data.jsonl"):
74
+ """Export Groq content with high-quality feedback for Phi-3 fine-tuning"""
75
+ print("🔧 Exporting Groq training data for Phi-3 fine-tuning...")
76
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
77
+
78
+ session = SessionLocal()
79
+ try:
80
+ # Get all content entries with their feedback
81
+ entries = session.query(ContentHistory).options(joinedload(ContentHistory.feedback)).all()
82
+ print(f"📊 Found {len(entries)} total content entries")
83
+
84
+ high_quality_groq = []
85
+ total_groq_feedback = 0
86
+ total_entries_checked = 0
87
+
88
+ for entry in entries:
89
+ total_entries_checked += 1
90
+ feedback_list = entry.feedback
91
+ print(f"🔍 Checking entry {total_entries_checked}/{len(entries)}: model={entry.generated_model}, user_type={entry.user_type}, feedback_count={len(feedback_list)}")
92
+
93
+ for feedback in feedback_list:
94
+ # Count all Groq feedback for statistics
95
+ if entry.generated_model == "groq":
96
+ total_groq_feedback += 1
97
+ print(f" 📝 Groq Feedback {total_groq_feedback}: clarity={feedback.clarity}, depth={feedback.depth}")
98
+
99
+ # Only export high-quality Groq feedback (for fine-tuning Phi-3)
100
+ if is_high_quality(feedback, entry):
101
+ example = format_training_example(entry, feedback)
102
+ if example:
103
+ high_quality_groq.append(example)
104
+ print(f" ✅ Added Groq training example")
105
+
106
+ print(f"📈 Export Summary:")
107
+ print(f" - Total entries checked: {total_entries_checked}")
108
+ print(f" - Total Groq feedback: {total_groq_feedback}")
109
+ print(f" - High-quality Groq examples: {len(high_quality_groq)}")
110
+
111
+ if not high_quality_groq:
112
+ print("❌ No high-quality Groq training data found.")
113
+ print("💡 Make sure you have Groq-generated content with high-quality feedback:")
114
+ print(f" - Generated by Groq model")
115
+ print(f" - Clarity >= {MIN_CLARITY}")
116
+ print(f" - Depth >= {MIN_DEPTH}")
117
+ print(f" - Complexity = 'Just right'")
118
+ print(f" - Comments length >= {MIN_COMMENT_LENGTH} characters")
119
+ return False
120
+
121
+ # Write to JSONL file (without metadata for training)
122
+ with open(output_file, "w", encoding="utf-8") as f:
123
+ for item in high_quality_groq:
124
+ # Remove metadata for actual training
125
+ training_item = {
126
+ "instruction": item["instruction"],
127
+ "input": item["input"],
128
+ "output": item["output"]
129
+ }
130
+ f.write(json.dumps(training_item, ensure_ascii=False) + "\n")
131
+
132
+ print(f"✅ Successfully exported {len(high_quality_groq)} Groq training examples to {output_file}")
133
+
134
+ # Show detailed breakdown
135
+ if high_quality_groq:
136
+ student_examples = len([e for e in high_quality_groq if "Simplify" in e["instruction"]])
137
+ tutor_examples = len([e for e in high_quality_groq if "Create a" in e["instruction"]])
138
+ print(f"📊 Breakdown: {student_examples} student examples, {tutor_examples} tutor examples")
139
+
140
+ print("📝 Sample training example:")
141
+ sample = high_quality_groq[0]
142
+ print(json.dumps({
143
+ "instruction": sample["instruction"][:100] + "...",
144
+ "input": sample["input"],
145
+ "output": sample["output"][:100] + "..."
146
+ }, indent=2, ensure_ascii=False))
147
+
148
+ return True
149
+
150
+ except Exception as e:
151
+ print(f"❌ Error exporting training data: {e}")
152
+ import traceback
153
+ traceback.print_exc()
154
+ return False
155
+
156
+ finally:
157
+ session.close()
158
+
159
+ if __name__ == "__main__":
160
+ export_training_data_from_db()
feedback.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import re
4
+ from datetime import datetime
5
+ from db.helpers import get_research_stats
6
+
7
+ def save_feedback(prompt, output, clarity, depth, complexity, comments, user_type=None, student_level=None):
8
+ """
9
+ Save user feedback to a JSONL file with additional metadata
10
+ """
11
+
12
+ # Create feedback directory if it doesn't exist
13
+ os.makedirs("data/feedback", exist_ok=True)
14
+
15
+ feedback_data = {
16
+ "timestamp": datetime.now().isoformat(),
17
+ "prompt": prompt,
18
+ "output": output,
19
+ "feedback": {
20
+ "clarity": clarity,
21
+ "depth": depth,
22
+ "complexity": complexity,
23
+ "comments": comments
24
+ },
25
+ "metadata": {
26
+ "user_type": user_type,
27
+ "student_level": student_level
28
+ }
29
+ }
30
+
31
+ # Save to JSONL file
32
+ feedback_file = "data/feedback/feedback.jsonl"
33
+
34
+ try:
35
+ with open(feedback_file, "a", encoding="utf-8") as f:
36
+ f.write(json.dumps(feedback_data, ensure_ascii=False) + "\n")
37
+
38
+ print(f"✅ Feedback saved to {feedback_file}")
39
+ return True
40
+
41
+ except Exception as e:
42
+ print(f"❌ Error saving feedback: {e}")
43
+ return False
44
+
45
+ def load_feedback_data():
46
+ """Load all feedback data for analysis"""
47
+ feedback_file = "data/feedback/feedback.jsonl"
48
+
49
+ if not os.path.exists(feedback_file):
50
+ return []
51
+
52
+ feedback_data = []
53
+ try:
54
+ with open(feedback_file, "r", encoding="utf-8") as f:
55
+ for line in f:
56
+ if line.strip():
57
+ feedback_data.append(json.loads(line.strip()))
58
+ return feedback_data
59
+ except Exception as e:
60
+ print(f"❌ Error loading feedback data: {e}")
61
+ return []
62
+
63
+ def get_feedback_stats():
64
+ """Get basic statistics about collected feedback"""
65
+ feedback_data = load_feedback_data()
66
+
67
+ if not feedback_data:
68
+ return {
69
+ "total_feedback": 0,
70
+ "average_clarity": 0,
71
+ "average_depth": 0,
72
+ "complexity_distribution": {},
73
+ "user_type_distribution": {}
74
+ }
75
+
76
+ total = len(feedback_data)
77
+ clarity_sum = 0
78
+ depth_sum = 0
79
+ complexity_counts = {}
80
+ user_type_counts = {}
81
+
82
+ for entry in feedback_data:
83
+ clarity_sum += entry["feedback"]["clarity"]
84
+ depth_sum += entry["feedback"]["depth"]
85
+
86
+ complexity = entry["feedback"]["complexity"]
87
+ complexity_counts[complexity] = complexity_counts.get(complexity, 0) + 1
88
+
89
+ user_type = entry["metadata"].get("user_type", "unknown")
90
+ user_type_counts[user_type] = user_type_counts.get(user_type, 0) + 1
91
+
92
+ return {
93
+ "total_feedback": total,
94
+ "average_clarity": round(clarity_sum / total, 2) if total > 0 else 0,
95
+ "average_depth": round(depth_sum / total, 2) if total > 0 else 0,
96
+ "complexity_distribution": complexity_counts,
97
+ "user_type_distribution": user_type_counts
98
+ }
99
+
100
+ def is_high_quality_feedback(feedback_entry):
101
+ """
102
+ SIMPLEST VERSION: Length-based filtering after removing emojis
103
+ Only uses high-quality, "just right" feedback for training
104
+ """
105
+ feedback = feedback_entry["feedback"]
106
+
107
+ # Quality thresholds
108
+ MIN_CLARITY = 4
109
+ MIN_DEPTH = 4
110
+ MIN_COMMENT_LENGTH = 25 # Substantive comments after emoji removal
111
+ MIN_WORD_COUNT = 4 # Minimum words for substance
112
+
113
+ # Check ratings (must be high quality)
114
+ if feedback["clarity"] < MIN_CLARITY or feedback["depth"] < MIN_DEPTH:
115
+ return False
116
+
117
+ # Check complexity (we want "Just right" examples to replicate)
118
+ if feedback["complexity"] != "Just right":
119
+ return False
120
+
121
+ # Check comments if provided
122
+ comments = feedback.get("comments", "").strip()
123
+
124
+ if comments:
125
+ # Remove emojis first, then check length
126
+ emoji_pattern = re.compile(
127
+ r'[\U0001F600-\U0001F64F\U0001F300-\U0001F5FF\U0001F680-\U0001F6FF\U0001F1E0-\U0001F1FF\U00002600-\U000027BF\U0001F900-\U0001F9FF\U0001F018-\U0001F270👍👎😊😐😕❤️🔥]',
128
+ flags=re.UNICODE
129
+ )
130
+ text_without_emojis = emoji_pattern.sub('', comments).strip()
131
+
132
+ # Now apply length check on the cleaned text
133
+ if len(text_without_emojis) < MIN_COMMENT_LENGTH:
134
+ return False
135
+
136
+ # Check word count for minimal substance
137
+ word_count = len(text_without_emojis.split())
138
+ if word_count < MIN_WORD_COUNT:
139
+ return False
140
+
141
+ return True
142
+
143
+ def prepare_training_data():
144
+ """
145
+ Prepare high-quality feedback for model fine-tuning
146
+ Returns structured training examples
147
+ """
148
+ all_feedback = load_feedback_data()
149
+
150
+ training_examples = []
151
+ skipped_count = 0
152
+
153
+ for feedback in all_feedback:
154
+ if is_high_quality_feedback(feedback):
155
+ # Create training example from high-quality feedback
156
+ training_example = {
157
+ "instruction": feedback["prompt"],
158
+ "input": f"Student Level: {feedback['metadata'].get('student_level', 'Unknown')}",
159
+ "output": feedback["output"],
160
+ "metadata": {
161
+ "user_type": feedback["metadata"].get("user_type"),
162
+ "clarity_score": feedback["feedback"]["clarity"],
163
+ "depth_score": feedback["feedback"]["depth"],
164
+ "comments": feedback["feedback"].get("comments", "")
165
+ }
166
+ }
167
+ training_examples.append(training_example)
168
+ else:
169
+ skipped_count += 1
170
+
171
+ print(f"✅ Prepared {len(training_examples)} training examples (skipped {skipped_count} low-quality)")
172
+ return training_examples
173
+
174
+ def get_training_data_stats():
175
+ """
176
+ Get statistics about prepared training data
177
+ """
178
+ training_data = prepare_training_data()
179
+
180
+ if not training_data:
181
+ return {
182
+ "total_training_examples": 0,
183
+ "user_type_breakdown": {},
184
+ "average_scores": {"clarity": 0, "depth": 0}
185
+ }
186
+
187
+ user_type_counts = {}
188
+ clarity_sum = 0
189
+ depth_sum = 0
190
+
191
+ for example in training_data:
192
+ user_type = example["metadata"].get("user_type", "unknown")
193
+ user_type_counts[user_type] = user_type_counts.get(user_type, 0) + 1
194
+
195
+ clarity_sum += example["metadata"]["clarity_score"]
196
+ depth_sum += example["metadata"]["depth_score"]
197
+
198
+ return {
199
+ "total_training_examples": len(training_data),
200
+ "user_type_breakdown": user_type_counts,
201
+ "average_scores": {
202
+ "clarity": round(clarity_sum / len(training_data), 2),
203
+ "depth": round(depth_sum / len(training_data), 2)
204
+ }
205
+ }
206
+
207
+ def export_training_data(output_file="data/training/training_data.jsonl"):
208
+ """
209
+ Export filtered training data to file for fine-tuning
210
+ """
211
+ training_data = prepare_training_data()
212
+
213
+ if not training_data:
214
+ print("❌ No high-quality training data available")
215
+ return False
216
+
217
+ # Create directory if it doesn't exist
218
+ os.makedirs(os.path.dirname(output_file), exist_ok=True)
219
+
220
+ try:
221
+ with open(output_file, "w", encoding="utf-8") as f:
222
+ for example in training_data:
223
+ # Remove metadata for actual training
224
+ training_example = {
225
+ "instruction": example["instruction"],
226
+ "input": example["input"],
227
+ "output": example["output"]
228
+ }
229
+ f.write(json.dumps(training_example, ensure_ascii=False) + "\n")
230
+
231
+ print(f"✅ Exported {len(training_data)} training examples to {output_file}")
232
+ return True
233
+
234
+ except Exception as e:
235
+ print(f"❌ Error exporting training data: {e}")
236
+ return False
237
+
238
+ def get_research_progress():
239
+ """Fetch research progress from PostgreSQL"""
240
+ stats = get_research_stats()
241
+
242
+ return {
243
+ "total_feedback": stats["total_feedback"],
244
+ "high_quality_examples": stats["high_quality_feedback"],
245
+ "conversion_rate": stats["conversion_rate"],
246
+ "average_quality": stats["average_scores"],
247
+ "user_breakdown": stats["user_type_breakdown"]
248
+ }
generator.py ADDED
@@ -0,0 +1,1636 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import time
3
+ import random
4
+ import requests
5
+ from openai import OpenAI
6
+ from typing import Dict, List, Optional
7
+ import logging
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ def get_api_keys(service_name: str, key_names: List[str]) -> List[str]:
14
+ """Get API keys from multiple sources with priority for HuggingFace Spaces"""
15
+ keys = []
16
+
17
+ # 1. HuggingFace Spaces Secrets (Primary)
18
+ for key_name in key_names:
19
+ # Try HF-specific naming first
20
+ hf_key_name = f"HF_{service_name.upper()}_{key_name}"
21
+ key = os.getenv(hf_key_name)
22
+ if key and key.strip():
23
+ keys.append(key.strip())
24
+ logger.info(f"✅ Found {service_name} key {key_name} in HuggingFace secrets")
25
+
26
+ # 2. Standard Environment Variables
27
+ for key_name in key_names:
28
+ key = os.getenv(key_name) or os.getenv(key_name.upper())
29
+ if key and key.strip() and key not in keys:
30
+ keys.append(key.strip())
31
+ logger.info(f"✅ Found {service_name} key {key_name} in environment")
32
+
33
+ # 3. Streamlit Secrets (Backward Compatibility)
34
+ try:
35
+ import streamlit as st
36
+ if hasattr(st, 'secrets') and service_name in st.secrets:
37
+ secrets = st.secrets[service_name]
38
+ for key_name in key_names:
39
+ key = secrets.get(key_name)
40
+ if key and key.strip() and key not in keys:
41
+ keys.append(key.strip())
42
+ logger.info(f"✅ Found {service_name} key {key_name} in Streamlit secrets")
43
+ except (ImportError, AttributeError):
44
+ pass
45
+
46
+ return keys
47
+
48
+ def get_groq_api_keys():
49
+ """Get Groq API keys for all environments"""
50
+ return get_api_keys("groq", ["api_key", "api_key_1", "api_key_2"])
51
+
52
+ def get_ollama_url():
53
+ """Get Ollama URL from multiple sources"""
54
+
55
+ # 1. HuggingFace Spaces
56
+ hf_url = os.getenv("HF_OLLAMA_URL")
57
+ if hf_url:
58
+ logger.info("✅ Found Ollama URL in HuggingFace secrets")
59
+ return hf_url
60
+
61
+ # 2. Environment Variables
62
+ env_url = os.getenv("OLLAMA_URL") or os.getenv("MODEL_URL")
63
+ if env_url:
64
+ logger.info("✅ Found Ollama URL in environment")
65
+ return env_url
66
+
67
+ # 3. Streamlit Secrets
68
+ try:
69
+ import streamlit as st
70
+ if hasattr(st, 'secrets') and 'ollama' in st.secrets:
71
+ url = st.secrets["ollama"].get("url")
72
+ if url:
73
+ logger.info("✅ Found Ollama URL in Streamlit secrets")
74
+ return url
75
+ except (ImportError, AttributeError):
76
+ pass
77
+
78
+ logger.warning("⚠️ No Ollama URL configured - local models will not be available")
79
+ return None
80
+
81
+ class MultiGroqGenerator:
82
+ def __init__(self):
83
+ self.providers = self._initialize_groq_providers()
84
+ self.models = self._get_best_models()
85
+ self.max_retries = 3
86
+ self.retry_delay = 2 # seconds
87
+
88
+ def _initialize_groq_providers(self):
89
+ """Initialize multiple Groq API providers with different keys"""
90
+ providers = []
91
+
92
+ # Get all Groq API keys
93
+ groq_keys = get_groq_api_keys()
94
+
95
+ # Filter out None values and create providers
96
+ for i, key in enumerate(groq_keys):
97
+ if key and key.strip():
98
+ providers.append({
99
+ 'name': f'Groq-{i+1}',
100
+ 'client': OpenAI(
101
+ api_key=key.strip(),
102
+ base_url="https://api.groq.com/openai/v1"
103
+ ),
104
+ 'weight': 10,
105
+ 'fail_count': 0,
106
+ 'last_used': 0
107
+ })
108
+
109
+ if not providers:
110
+ logger.warning("❌ No Groq API keys found")
111
+ return []
112
+
113
+ logger.info(f"✅ Initialized {len(providers)} Groq providers")
114
+ return providers
115
+
116
+ def _get_best_models(self):
117
+ """Select optimal models for educational content"""
118
+ return [
119
+ {
120
+ 'id': 'llama-3.3-70b-versatile',
121
+ 'name': 'Llama 3.3 70B',
122
+ 'weight': 10,
123
+ 'max_tokens': 32768,
124
+ 'description': 'Best for complex explanations'
125
+ },
126
+ {
127
+ 'id': 'meta-llama/llama-4-maverick-17b-128e-instruct',
128
+ 'name': 'Llama 4 Maverick 17B',
129
+ 'weight': 9,
130
+ 'max_tokens': 128000,
131
+ 'description': 'Large context for big documents'
132
+ },
133
+ {
134
+ 'id': 'llama-3.1-8b-instant',
135
+ 'name': 'Llama 3.1 8B Instant',
136
+ 'weight': 8,
137
+ 'max_tokens': 32768,
138
+ 'description': 'Fast for most content'
139
+ },
140
+ ]
141
+
142
+ def _select_provider(self):
143
+ """Select provider based on weight and fail history"""
144
+ if not self.providers:
145
+ return None
146
+
147
+ available_providers = [
148
+ p for p in self.providers
149
+ if p['fail_count'] < 3 and (time.time() - p['last_used']) > 30
150
+ ]
151
+
152
+ if not available_providers:
153
+ available_providers = self.providers
154
+ for p in available_providers:
155
+ p['fail_count'] = max(0, p['fail_count'] - 1)
156
+
157
+ weights = [p['weight'] for p in available_providers]
158
+ selected = random.choices(available_providers, weights=weights, k=1)[0]
159
+ selected['last_used'] = time.time()
160
+ return selected
161
+
162
+ def _select_model(self, prompt_length: int):
163
+ """Select optimal model based on prompt size"""
164
+ approx_tokens = prompt_length // 4
165
+
166
+ if approx_tokens > 20000:
167
+ return self.models[1] # Maverick for huge docs
168
+ elif approx_tokens > 10000:
169
+ return self.models[1] # Maverick for large docs
170
+ elif approx_tokens > 6000:
171
+ return self.models[0] # 70B for medium-large
172
+ elif approx_tokens > 3000:
173
+ return self.models[0] # 70B for quality
174
+ else:
175
+ return self.models[2] # 8B for speed
176
+
177
+ def generate(self, prompt: str) -> str:
178
+ """Generate content with automatic failover"""
179
+ if not self.providers:
180
+ return "❌ Groq Error: No API keys configured. Please set GROQ_API_KEY in HuggingFace secrets or environment variables."
181
+
182
+ last_error = None
183
+ prompt_length = len(prompt)
184
+
185
+ for attempt in range(self.max_retries + 1):
186
+ provider = self._select_provider()
187
+ model = self._select_model(prompt_length)
188
+
189
+ if not provider:
190
+ return "❌ Groq Error: No available providers"
191
+
192
+ try:
193
+ logger.info(f"🔄 Attempt {attempt + 1} with {provider['name']} using {model['name']}...")
194
+
195
+ result = self._call_groq(provider, model, prompt)
196
+
197
+ if result and not result.startswith(("[Error", "[RateLimit]", "[Quota]", "[Auth]", "[Empty]", "[ModelNotFound]")):
198
+ logger.info(f"✅ Success with {provider['name']} + {model['name']}")
199
+ provider['weight'] = min(20, provider['weight'] + 1)
200
+ provider['fail_count'] = max(0, provider['fail_count'] - 1)
201
+ return result
202
+ else:
203
+ logger.warning(f"❌ Provider returned: {result}")
204
+ if "[ModelNotFound]" in result:
205
+ continue
206
+
207
+ except Exception as e:
208
+ last_error = str(e)
209
+ logger.error(f"❌ {provider['name']} + {model['name']} failed: {last_error}")
210
+ provider['weight'] = max(1, provider['weight'] - 2)
211
+ provider['fail_count'] += 1
212
+
213
+ if attempt < self.max_retries:
214
+ delay = self.retry_delay * (2 ** attempt)
215
+ logger.info(f"⏰ Waiting {delay}s before retry...")
216
+ time.sleep(delay)
217
+
218
+ return self._fallback_generate(prompt)
219
+
220
+ def generate_large_content(self, prompt: str) -> str:
221
+ """Handle large content generation for Groq - compatibility method"""
222
+ logger.info("🔷 Using Groq for large content generation...")
223
+
224
+ # For Groq, we can handle large content directly due to large context windows
225
+ # Just use the normal generate method with optimized model selection
226
+ prompt_length = len(prompt)
227
+
228
+ if prompt_length > 20000: # Very large prompt
229
+ logger.info("📝 Large prompt detected, optimizing for Groq Maverick...")
230
+ # Temporarily prioritize Maverick for large contexts
231
+ original_models = self.models.copy()
232
+ self.models = [self.models[1]] # Maverick has 128K context
233
+ try:
234
+ result = self.generate(prompt)
235
+ return result
236
+ finally:
237
+ self.models = original_models # Restore original models
238
+ else:
239
+ # Use normal generation
240
+ return self.generate(prompt)
241
+
242
+ def _fallback_generate(self, prompt: str) -> str:
243
+ """Fallback generation with simpler model selection"""
244
+ logger.info("🔄 Trying fallback generation...")
245
+
246
+ fallback_models = [self.models[2], self.models[0]]
247
+
248
+ for model in fallback_models:
249
+ for provider in self.providers:
250
+ try:
251
+ logger.info(f"🔄 Fallback with {provider['name']} using {model['name']}...")
252
+ result = self._call_groq(provider, model, prompt)
253
+
254
+ if result and not result.startswith(("[Error", "[RateLimit]", "[Quota]", "[Auth]", "[Empty]", "[ModelNotFound]")):
255
+ logger.info(f"✅ Fallback success with {provider['name']} + {model['name']}")
256
+ return result
257
+ except Exception as e:
258
+ logger.error(f"❌ Fallback failed: {e}")
259
+ continue
260
+
261
+ return self._get_user_friendly_error("All models failed")
262
+
263
+ def _call_groq(self, provider, model, prompt: str) -> str:
264
+ """Call Groq API with specific provider and model"""
265
+ try:
266
+ prompt_tokens_approx = len(prompt) // 4
267
+ available_tokens = model['max_tokens'] - prompt_tokens_approx - 500
268
+ max_response_tokens = max(1000, min(8000, available_tokens))
269
+
270
+ response = provider['client'].chat.completions.create(
271
+ model=model['id'],
272
+ messages=[{"role": "user", "content": prompt}],
273
+ temperature=0.7,
274
+ max_tokens=max_response_tokens,
275
+ top_p=0.9
276
+ )
277
+
278
+ if (response and response.choices and len(response.choices) > 0 and
279
+ response.choices[0].message and response.choices[0].message.content):
280
+
281
+ content = response.choices[0].message.content.strip()
282
+ return content if content else "[Empty] No content generated"
283
+ else:
284
+ return "[Empty] Invalid response structure"
285
+
286
+ except Exception as e:
287
+ error_msg = str(e).lower()
288
+
289
+ if "rate limit" in error_msg or "429" in error_msg:
290
+ return f"[RateLimit] {provider['name']} rate limit exceeded"
291
+ elif "quota" in error_msg:
292
+ return f"[Quota] {provider['name']} quota exceeded"
293
+ elif "authentication" in error_msg:
294
+ return f"[Auth] {provider['name']} authentication failed"
295
+ elif "context length" in error_msg:
296
+ return f"[Length] {provider['name']} content too long"
297
+ elif "model not found" in error_msg:
298
+ return f"[ModelNotFound] {provider['name']}: {str(e)}"
299
+ else:
300
+ return f"[Error] {provider['name']}: {str(e)}"
301
+
302
+ def _get_user_friendly_error(self, technical_error: str) -> str:
303
+ """Convert technical errors to user-friendly messages"""
304
+ error_lower = technical_error.lower()
305
+
306
+ if "rate limit" in error_lower:
307
+ return "🚫 **Service Busy** - Please wait a few minutes and try again"
308
+ elif "quota" in error_lower:
309
+ return "📊 **Daily Limit Reached** - Try again tomorrow"
310
+ elif "length" in error_lower:
311
+ return "📝 **Content Too Large** - Please break into smaller sections"
312
+ else:
313
+ return "❌ **Temporary Issue** - Please try again shortly"
314
+
315
+ def get_service_status(self) -> dict:
316
+ """Get current status of all providers"""
317
+ status = {
318
+ 'total_providers': len(self.providers),
319
+ 'healthy_providers': len([p for p in self.providers if p['fail_count'] < 2]),
320
+ 'providers': [],
321
+ 'models': [m['name'] for m in self.models]
322
+ }
323
+
324
+ for provider in self.providers:
325
+ if provider['fail_count'] >= 3:
326
+ status_text = "🔴 Limited"
327
+ elif provider['fail_count'] >= 1:
328
+ status_text = "🟡 Slow"
329
+ else:
330
+ status_text = "🟢 Good"
331
+
332
+ status['providers'].append({
333
+ 'name': provider['name'],
334
+ 'status': status_text,
335
+ 'failures': provider['fail_count']
336
+ })
337
+
338
+ return status
339
+
340
+
341
+ class HFGenerator:
342
+ """Phi-3 Generator with Auto-Pull, Smart Chunking, and Context Preservation"""
343
+
344
+ def __init__(self, base_url: str = None):
345
+ # Use environment variable or Streamlit secret as default
346
+ self.base_url = base_url or get_ollama_url()
347
+ self.model = "phi3:mini"
348
+ self.current_requests = 0
349
+ self.max_concurrent = 2
350
+ self.model_available = False
351
+
352
+ # Only try to connect if base_url is provided
353
+ if self.base_url:
354
+ self._ensure_model_available()
355
+ else:
356
+ logger.warning("⚠️ Ollama URL not configured - Phi-3 will not be available")
357
+
358
+ def _ensure_model_available(self):
359
+ """Check if model is available and pull if needed"""
360
+ try:
361
+ response = requests.get(f"{self.base_url}/api/tags", timeout=10)
362
+ if response.status_code == 200:
363
+ models = response.json().get('models', [])
364
+ self.model_available = any(model['name'] == self.model for model in models)
365
+
366
+ if not self.model_available:
367
+ logger.info(f"🔄 Model {self.model} not found, pulling...")
368
+ self._pull_model()
369
+ else:
370
+ logger.info(f"✅ Model {self.model} is available")
371
+ else:
372
+ logger.warning(f"❌ Could not check models: {response.status_code}")
373
+ except Exception as e:
374
+ logger.error(f"❌ Error checking models: {e}")
375
+
376
+ def _pull_model(self):
377
+ """Pull the Phi-3 model if not available"""
378
+ try:
379
+ logger.info(f"📥 Pulling {self.model}... This may take a few minutes.")
380
+
381
+ payload = {"name": self.model}
382
+ response = requests.post(
383
+ f"{self.base_url}/api/pull",
384
+ json=payload,
385
+ timeout=300 # 5 minute timeout for pull
386
+ )
387
+
388
+ if response.status_code == 200:
389
+ logger.info(f"✅ Successfully pulled {self.model}")
390
+ self.model_available = True
391
+ return True
392
+ else:
393
+ logger.error(f"❌ Failed to pull model: {response.text}")
394
+ return False
395
+
396
+ except Exception as e:
397
+ logger.error(f"❌ Error pulling model: {e}")
398
+ return False
399
+
400
+ def _estimate_tokens(self, text: str) -> int:
401
+ """Rough token estimation"""
402
+ return len(text) // 4
403
+
404
+ def _chunk_content(self, content: str, max_tokens: int = 2500) -> list:
405
+ """Split large content into manageable chunks"""
406
+ paragraphs = content.split('\n\n')
407
+ chunks = []
408
+ current_chunk = ""
409
+ current_tokens = 0
410
+
411
+ for paragraph in paragraphs:
412
+ para_tokens = self._estimate_tokens(paragraph)
413
+
414
+ if para_tokens > max_tokens:
415
+ sentences = paragraph.split('. ')
416
+ for sentence in sentences:
417
+ sent_tokens = self._estimate_tokens(sentence)
418
+ if current_tokens + sent_tokens > max_tokens:
419
+ if current_chunk:
420
+ chunks.append(current_chunk.strip())
421
+ current_chunk = sentence
422
+ current_tokens = sent_tokens
423
+ else:
424
+ current_chunk += " " + sentence
425
+ current_tokens += sent_tokens
426
+ else:
427
+ if current_tokens + para_tokens > max_tokens:
428
+ if current_chunk:
429
+ chunks.append(current_chunk.strip())
430
+ current_chunk = paragraph
431
+ current_tokens = para_tokens
432
+ else:
433
+ current_chunk += "\n\n" + paragraph
434
+ current_tokens += para_tokens
435
+
436
+ if current_chunk:
437
+ chunks.append(current_chunk.strip())
438
+
439
+ return chunks
440
+
441
+ def _create_context_summary(self, previous_chunks: list) -> str:
442
+ """Create a context summary from previous chunks"""
443
+ if not previous_chunks:
444
+ return ""
445
+
446
+ context_prompt = f"""
447
+ Here's a summary of previous sections:
448
+ {chr(10).join(previous_chunks)}
449
+
450
+ Provide a brief summary (2-3 sentences) of key points to help understand the next section.
451
+ """
452
+
453
+ try:
454
+ payload = {
455
+ "model": self.model,
456
+ "messages": [{"role": "user", "content": context_prompt}],
457
+ "stream": False,
458
+ "options": {
459
+ "temperature": 0.3,
460
+ "top_p": 0.8,
461
+ "num_predict": 200
462
+ }
463
+ }
464
+
465
+ response = requests.post(f"{self.base_url}/api/chat", json=payload, timeout=30)
466
+ if response.status_code == 200:
467
+ return response.json()['message']['content'].strip()
468
+ return f"Previous sections covered: {', '.join(previous_chunks[:2])}..."
469
+ except Exception:
470
+ return f"Context from {len(previous_chunks)} previous sections"
471
+
472
+ def _create_chunk_summary(self, content: str) -> str:
473
+ """Create a very brief summary of a chunk's content"""
474
+ try:
475
+ payload = {
476
+ "model": self.model,
477
+ "messages": [{"role": "user", "content": f"Summarize key points in 1-2 sentences: {content}"}],
478
+ "stream": False,
479
+ "options": {
480
+ "temperature": 0.3,
481
+ "top_p": 0.8,
482
+ "num_predict": 100
483
+ }
484
+ }
485
+
486
+ response = requests.post(f"{self.base_url}/api/chat", json=payload, timeout=20)
487
+ if response.status_code == 200:
488
+ return response.json()['message']['content'].strip()
489
+ return content[:100] + "..."
490
+ except:
491
+ return content[:100] + "..."
492
+
493
+ def _call_ollama_with_retry(self, payload: dict, max_retries: int = 2) -> Dict:
494
+ """Call Ollama API with auto-pull retry"""
495
+ for attempt in range(max_retries + 1):
496
+ try:
497
+ response = requests.post(
498
+ f"{self.base_url}/api/chat",
499
+ json=payload,
500
+ timeout=60
501
+ )
502
+
503
+ if response.status_code == 200:
504
+ return {"success": True, "data": response.json()}
505
+ elif response.status_code == 404 and "not found" in response.text.lower():
506
+ logger.info(f"🔄 Model not found, attempting to pull... (attempt {attempt + 1})")
507
+ if self._pull_model():
508
+ continue # Retry after successful pull
509
+ else:
510
+ return {"success": False, "error": "Failed to pull model"}
511
+ else:
512
+ return {"success": False, "error": f"API error {response.status_code}: {response.text}"}
513
+
514
+ except requests.exceptions.Timeout:
515
+ if attempt < max_retries:
516
+ logger.info(f"⏰ Timeout, retrying... (attempt {attempt + 1})")
517
+ time.sleep(2)
518
+ else:
519
+ return {"success": False, "error": "Request timeout"}
520
+ except Exception as e:
521
+ return {"success": False, "error": f"Connection failed: {str(e)}"}
522
+
523
+ return {"success": False, "error": "All retries failed"}
524
+
525
+ def generate(self, prompt: str, user_type: str = "student",
526
+ academic_level: str = "undergraduate",
527
+ content_type: str = "simplified_explanation") -> str:
528
+ """Generate educational content with auto-pull and smart features"""
529
+
530
+ # Check if Ollama is configured
531
+ if not self.base_url:
532
+ return "❌ Phi-3 Error: Ollama URL not configured. Please set MODEL_URL environment variable or add to HuggingFace secrets."
533
+
534
+ # Check if we need to pull model first
535
+ if not self.model_available:
536
+ logger.info("🔄 Model not available, pulling before generation...")
537
+ if not self._pull_model():
538
+ return f"❌ Phi-3 Error: Phi-3 model is not available and failed to pull. Please check the Ollama server."
539
+
540
+ estimated_tokens = self._estimate_tokens(prompt)
541
+
542
+ # Auto-detect large documents and use chunking
543
+ if estimated_tokens > 3000:
544
+ result = self.generate_large_content_with_context(prompt, user_type, academic_level, content_type)
545
+ if isinstance(result, dict):
546
+ return result.get("content", f"❌ Phi-3 Error: {result.get('error', 'Unknown error')}")
547
+ return result
548
+
549
+ # Queue management
550
+ if self.current_requests >= self.max_concurrent:
551
+ queue_position = self.current_requests - self.max_concurrent + 1
552
+ estimated_wait = queue_position * 7
553
+ return f"❌ Phi-3 Error: Service busy. You're #{queue_position} in queue (~{estimated_wait}s)"
554
+
555
+ self.current_requests += 1
556
+ try:
557
+ # FIXED: Increased token allocation for complete responses
558
+ if estimated_tokens > 2000:
559
+ max_output_tokens = 2000 # Increased from 500
560
+ elif estimated_tokens > 1000:
561
+ max_output_tokens = 2500 # Increased from 800
562
+ else:
563
+ max_output_tokens = 3000 # Increased from 1000
564
+
565
+ payload = {
566
+ "model": self.model,
567
+ "messages": [{"role": "user", "content": prompt}],
568
+ "stream": False,
569
+ "options": {
570
+ "temperature": 0.7,
571
+ "top_p": 0.9,
572
+ "num_predict": max_output_tokens
573
+ }
574
+ }
575
+
576
+ start_time = time.time()
577
+ result = self._call_ollama_with_retry(payload)
578
+ inference_time = time.time() - start_time
579
+
580
+ if result["success"]:
581
+ data = result["data"]
582
+ content = data['message']['content'].strip()
583
+
584
+ # Check if content was cut off and retry with more tokens if needed
585
+ if self._is_content_cut_off(content):
586
+ logger.info("⚠️ Content appears cut off, retrying with more tokens...")
587
+ payload["options"]["num_predict"] = 4000 # Max tokens for Phi-3
588
+ retry_result = self._call_ollama_with_retry(payload)
589
+
590
+ if retry_result["success"]:
591
+ data = retry_result["data"]
592
+ content = data['message']['content'].strip()
593
+
594
+ return content
595
+ else:
596
+ return f"❌ Phi-3 Error: {result['error']}"
597
+
598
+ except Exception as e:
599
+ return f"❌ Phi-3 Error: {str(e)}"
600
+ finally:
601
+ self.current_requests -= 1
602
+
603
+ def _is_content_cut_off(self, content: str) -> bool:
604
+ """Check if content appears to be cut off mid-sentence"""
605
+ if not content or len(content.strip()) < 100:
606
+ return True
607
+
608
+ # Check if it ends with proper punctuation
609
+ if content.strip().endswith(('.', '!', '?', '."', '!"', '?"')):
610
+ return False
611
+
612
+ # Check if it ends with incomplete sentence markers
613
+ if any(content.strip().endswith(marker) for marker in [',', ';', ':', '-', '–', '—']):
614
+ return True
615
+
616
+ # Check if it ends with an incomplete word or thought
617
+ last_paragraph = content.strip().split('\n')[-1]
618
+ if len(last_paragraph.split()) < 5: # Very short last paragraph
619
+ return True
620
+
621
+ return False
622
+
623
+ def generate_large_content_with_context(self, prompt: str, user_type: str = "student",
624
+ academic_level: str = "undergraduate",
625
+ content_type: str = "simplified_explanation") -> str:
626
+ """Handle large documents with context preservation"""
627
+
628
+ estimated_tokens = self._estimate_tokens(prompt)
629
+
630
+ if estimated_tokens <= 3000:
631
+ return self.generate(prompt, user_type, academic_level, content_type)
632
+
633
+ chunks = self._chunk_content(prompt, max_tokens=2500)
634
+
635
+ if len(chunks) > 6:
636
+ return f"❌ Phi-3 Error: Document too large ({estimated_tokens} tokens, {len(chunks)} chunks). Please use Groq or break into smaller sections."
637
+
638
+ all_results = []
639
+ previous_summaries = []
640
+
641
+ for i, chunk in enumerate(chunks):
642
+ logger.info(f"🔄 Processing chunk {i+1}/{len(chunks)} with context...")
643
+
644
+ context_summary = self._create_context_summary(previous_summaries)
645
+
646
+ if context_summary:
647
+ chunk_prompt = f"""Part {i+1} of {len(chunks)} - Building on previous context:
648
+
649
+ **PREVIOUS CONTEXT:**
650
+ {context_summary}
651
+
652
+ **CURRENT SECTION:**
653
+ {chunk}
654
+
655
+ Analyze this section while connecting to the overall context."""
656
+ else:
657
+ chunk_prompt = f"""Part {i+1} of {len(chunks)}:
658
+
659
+ **CONTENT:**
660
+ {chunk}
661
+
662
+ Please analyze this section."""
663
+
664
+ chunk_result = self.generate(chunk_prompt, user_type, academic_level, content_type)
665
+
666
+ if "❌ Phi-3 Error:" not in chunk_result:
667
+ chunk_summary = self._create_chunk_summary(chunk_result)
668
+ previous_summaries.append(chunk_summary)
669
+
670
+ all_results.append({
671
+ "chunk_number": i+1,
672
+ "content": chunk_result,
673
+ "context_used": bool(context_summary)
674
+ })
675
+ else:
676
+ return f"❌ Phi-3 Error: Failed to process chunk {i+1}: {chunk_result}"
677
+
678
+ if i < len(chunks) - 1:
679
+ time.sleep(1)
680
+
681
+ # Combine results
682
+ combined_content = "\n\n".join([f"## Part {r['chunk_number']}\n{r['content']}" for r in all_results])
683
+
684
+ return combined_content
685
+
686
+ def health_check(self) -> Dict:
687
+ """Comprehensive health check"""
688
+ if not self.base_url:
689
+ return {
690
+ "server_healthy": False,
691
+ "model_available": False,
692
+ "error": "Ollama URL not configured"
693
+ }
694
+
695
+ try:
696
+ response = requests.get(f"{self.base_url}/api/tags", timeout=10)
697
+ if response.status_code == 200:
698
+ models = response.json().get('models', [])
699
+ model_available = any(model['name'] == self.model for model in models)
700
+
701
+ return {
702
+ "server_healthy": True,
703
+ "model_available": model_available,
704
+ "available_models": [model['name'] for model in models],
705
+ "model_required": self.model
706
+ }
707
+ else:
708
+ return {
709
+ "server_healthy": False,
710
+ "model_available": False,
711
+ "error": f"Server returned {response.status_code}"
712
+ }
713
+ except Exception as e:
714
+ return {
715
+ "server_healthy": False,
716
+ "model_available": False,
717
+ "error": str(e)
718
+ }
719
+
720
+ def get_available_models(self):
721
+ """Get list of available models"""
722
+ try:
723
+ response = requests.get(f"{self.base_url}/api/tags", timeout=10)
724
+ if response.status_code == 200:
725
+ return [model['name'] for model in response.json().get('models', [])]
726
+ return []
727
+ except:
728
+ return []
729
+
730
+ def get_queue_status(self):
731
+ """Get current queue status"""
732
+ return {
733
+ "current_requests": self.current_requests,
734
+ "max_concurrent": self.max_concurrent,
735
+ "available_slots": max(0, self.max_concurrent - self.current_requests)
736
+ }
737
+
738
+
739
+ # Backward compatibility
740
+ class GroqGenerator(MultiGroqGenerator):
741
+ def __init__(self, model="llama-3.3-70b-versatile"):
742
+ super().__init__()
743
+
744
+
745
+ class ModelManager:
746
+ """Unified model manager that handles both Groq and Phi-3 models"""
747
+
748
+ def __init__(self):
749
+ self.groq_generator = MultiGroqGenerator()
750
+ self.phi3_generator = HFGenerator()
751
+
752
+ def generate(self, prompt: str, model_choice: str = "phi3", **kwargs) -> str:
753
+ """Generate content using selected model"""
754
+ logger.info(f"🎯 Using model: {model_choice}")
755
+
756
+ if model_choice == "phi3":
757
+ # Handle Phi-3 generation
758
+ user_type = kwargs.get('user_type', 'student')
759
+ academic_level = kwargs.get('student_level', 'undergraduate')
760
+ content_type = kwargs.get('content_type', 'simplified_explanation')
761
+
762
+ result = self.phi3_generator.generate(prompt, user_type, academic_level, content_type)
763
+ return result
764
+ else:
765
+ # Use Groq for comparison - check if this is a large content request
766
+ is_large_content = len(prompt) > 8000
767
+
768
+ if is_large_content:
769
+ return self.groq_generator.generate_large_content(prompt)
770
+ else:
771
+ return self.groq_generator.generate(prompt)
772
+
773
+ def get_service_status(self) -> dict:
774
+ """Get clean research-focused status"""
775
+ groq_status = self.groq_generator.get_service_status()
776
+ phi3_health = self.phi3_generator.health_check()
777
+
778
+ # Clean Groq status
779
+ clean_groq_status = {
780
+ 'healthy_providers': groq_status['healthy_providers'],
781
+ 'total_providers': groq_status['total_providers'],
782
+ 'providers': [
783
+ {
784
+ 'name': provider['name'],
785
+ 'failures': provider['failures']
786
+ }
787
+ for provider in groq_status['providers']
788
+ ]
789
+ }
790
+
791
+ # Enhanced Phi-3 status
792
+ enhanced_phi3_status = {
793
+ 'server_healthy': phi3_health['server_healthy'],
794
+ 'model_available': phi3_health['model_available'],
795
+ 'available_models': phi3_health['available_models'],
796
+ 'model_required': phi3_health['model_required']
797
+ }
798
+
799
+ return {
800
+ "groq": clean_groq_status,
801
+ "phi3": enhanced_phi3_status
802
+ }
803
+
804
+
805
+ # Global model manager instance
806
+ model_manager = ModelManager()
807
+
808
+
809
+ # Setup function for your Streamlit app
810
+ def setup_generators():
811
+ """Setup both generators with health checks"""
812
+ logger.info("🔧 Setting up generators...")
813
+
814
+ groq_generator = MultiGroqGenerator()
815
+
816
+ phi3_generator = HFGenerator()
817
+ phi3_health = phi3_generator.health_check()
818
+
819
+ logger.info(f"🏥 Phi-3 Health: {phi3_health}")
820
+
821
+ if not phi3_health["server_healthy"]:
822
+ logger.error("❌ Phi-3 server is not accessible")
823
+ elif not phi3_health["model_available"]:
824
+ logger.info("🔄 Phi-3 model needs to be pulled on first use")
825
+
826
+ return {
827
+ "groq": groq_generator,
828
+ "phi3": phi3_generator
829
+ }
830
+
831
+
832
+ # Test function
833
+ def test_generators():
834
+ """Test both generators"""
835
+ logger.info("🧪 Testing Generators...")
836
+
837
+ generators = setup_generators()
838
+
839
+ # Test Groq
840
+ logger.info("🔷 Testing Groq...")
841
+ groq_result = generators["groq"].generate("Explain photosynthesis briefly")
842
+ if not groq_result.startswith("["):
843
+ logger.info("✅ Groq working")
844
+ else:
845
+ logger.error(f"❌ Groq failed: {groq_result}")
846
+
847
+ # Test Phi-3
848
+ logger.info("🔶 Testing Phi-3...")
849
+ phi3_result = generators["phi3"].generate("Explain photosynthesis briefly")
850
+ if "❌ Phi-3 Error:" not in phi3_result:
851
+ logger.info("✅ Phi-3 working")
852
+ else:
853
+ logger.error(f"❌ Phi-3 failed: {phi3_result}")
854
+
855
+ # Test health
856
+ logger.info("🏥 Health Check:")
857
+ logger.info(f"Groq providers: {len(generators['groq'].providers)}")
858
+ logger.info(f"Phi-3 healthy: {generators['phi3'].health_check()}")
859
+
860
+
861
+ if __name__ == "__main__":
862
+ test_generators()
863
+
864
+ # import os
865
+ # import time
866
+ # import random
867
+ # import requests
868
+ # from openai import OpenAI
869
+ # from dotenv import load_dotenv
870
+ # from typing import Dict, List
871
+
872
+ # # Load environment variables once at module level
873
+ # load_dotenv()
874
+
875
+ # class MultiGroqGenerator:
876
+ # def __init__(self):
877
+ # self.providers = self._initialize_groq_providers()
878
+ # self.models = self._get_best_models()
879
+ # self.max_retries = 3
880
+ # self.retry_delay = 2 # seconds
881
+
882
+ # def _initialize_groq_providers(self):
883
+ # """Initialize multiple Groq API providers with different keys"""
884
+ # providers = []
885
+
886
+ # # Get all Groq API keys from environment
887
+ # groq_keys = [
888
+ # os.getenv("GROQ_API_KEY_1"),
889
+ # os.getenv("GROQ_API_KEY_2"),
890
+ # ]
891
+
892
+ # # Filter out None values and create providers
893
+ # for i, key in enumerate(groq_keys):
894
+ # if key and key.strip():
895
+ # providers.append({
896
+ # 'name': f'Groq-{i+1}',
897
+ # 'client': OpenAI(
898
+ # api_key=key.strip(),
899
+ # base_url="https://api.groq.com/openai/v1"
900
+ # ),
901
+ # 'weight': 10,
902
+ # 'fail_count': 0,
903
+ # 'last_used': 0
904
+ # })
905
+
906
+ # if not providers:
907
+ # raise ValueError("No Groq API keys found. Please set GROQ_API_KEY_1, GROQ_API_KEY_2, etc.")
908
+
909
+ # print(f"✅ Initialized {len(providers)} Groq providers")
910
+ # return providers
911
+
912
+ # def _get_best_models(self):
913
+ # """Select optimal models for educational content"""
914
+ # return [
915
+ # {
916
+ # 'id': 'llama-3.3-70b-versatile',
917
+ # 'name': 'Llama 3.3 70B',
918
+ # 'weight': 10,
919
+ # 'max_tokens': 32768,
920
+ # 'description': 'Best for complex explanations'
921
+ # },
922
+ # {
923
+ # 'id': 'meta-llama/llama-4-maverick-17b-128e-instruct',
924
+ # 'name': 'Llama 4 Maverick 17B',
925
+ # 'weight': 9,
926
+ # 'max_tokens': 128000,
927
+ # 'description': 'Large context for big documents'
928
+ # },
929
+ # {
930
+ # 'id': 'llama-3.1-8b-instant',
931
+ # 'name': 'Llama 3.1 8B Instant',
932
+ # 'weight': 8,
933
+ # 'max_tokens': 32768,
934
+ # 'description': 'Fast for most content'
935
+ # },
936
+ # ]
937
+
938
+ # def _select_provider(self):
939
+ # """Select provider based on weight and fail history"""
940
+ # available_providers = [
941
+ # p for p in self.providers
942
+ # if p['fail_count'] < 3 and (time.time() - p['last_used']) > 30
943
+ # ]
944
+
945
+ # if not available_providers:
946
+ # available_providers = self.providers
947
+ # for p in available_providers:
948
+ # p['fail_count'] = max(0, p['fail_count'] - 1)
949
+
950
+ # weights = [p['weight'] for p in available_providers]
951
+ # selected = random.choices(available_providers, weights=weights, k=1)[0]
952
+ # selected['last_used'] = time.time()
953
+ # return selected
954
+
955
+ # def _select_model(self, prompt_length: int):
956
+ # """Select optimal model based on prompt size"""
957
+ # approx_tokens = prompt_length // 4
958
+
959
+ # if approx_tokens > 20000:
960
+ # return self.models[1] # Maverick for huge docs
961
+ # elif approx_tokens > 10000:
962
+ # return self.models[1] # Maverick for large docs
963
+ # elif approx_tokens > 6000:
964
+ # return self.models[0] # 70B for medium-large
965
+ # elif approx_tokens > 3000:
966
+ # return self.models[0] # 70B for quality
967
+ # else:
968
+ # return self.models[2] # 8B for speed
969
+
970
+ # def generate(self, prompt: str) -> str:
971
+ # """Generate content with automatic failover"""
972
+ # last_error = None
973
+ # prompt_length = len(prompt)
974
+
975
+ # for attempt in range(self.max_retries + 1):
976
+ # provider = self._select_provider()
977
+ # model = self._select_model(prompt_length)
978
+
979
+ # try:
980
+ # print(f"🔄 Attempt {attempt + 1} with {provider['name']} using {model['name']}...")
981
+
982
+ # result = self._call_groq(provider, model, prompt)
983
+
984
+ # if result and not result.startswith(("[Error", "[RateLimit]", "[Quota]", "[Auth]", "[Empty]", "[ModelNotFound]")):
985
+ # print(f"✅ Success with {provider['name']} + {model['name']}")
986
+ # provider['weight'] = min(20, provider['weight'] + 1)
987
+ # provider['fail_count'] = max(0, provider['fail_count'] - 1)
988
+ # return result
989
+ # else:
990
+ # print(f"❌ Provider returned: {result}")
991
+ # if "[ModelNotFound]" in result:
992
+ # continue
993
+
994
+ # except Exception as e:
995
+ # last_error = str(e)
996
+ # print(f"❌ {provider['name']} + {model['name']} failed: {last_error}")
997
+ # provider['weight'] = max(1, provider['weight'] - 2)
998
+ # provider['fail_count'] += 1
999
+
1000
+ # if attempt < self.max_retries:
1001
+ # delay = self.retry_delay * (2 ** attempt)
1002
+ # print(f"⏰ Waiting {delay}s before retry...")
1003
+ # time.sleep(delay)
1004
+
1005
+ # return self._fallback_generate(prompt)
1006
+
1007
+ # def generate_large_content(self, prompt: str) -> str:
1008
+ # """Handle large content generation for Groq - compatibility method"""
1009
+ # print("🔷 Using Groq for large content generation...")
1010
+
1011
+ # # For Groq, we can handle large content directly due to large context windows
1012
+ # # Just use the normal generate method with optimized model selection
1013
+ # prompt_length = len(prompt)
1014
+
1015
+ # if prompt_length > 20000: # Very large prompt
1016
+ # print("📝 Large prompt detected, optimizing for Groq Maverick...")
1017
+ # # Temporarily prioritize Maverick for large contexts
1018
+ # original_models = self.models.copy()
1019
+ # self.models = [self.models[1]] # Maverick has 128K context
1020
+ # try:
1021
+ # result = self.generate(prompt)
1022
+ # return result
1023
+ # finally:
1024
+ # self.models = original_models # Restore original models
1025
+ # else:
1026
+ # # Use normal generation
1027
+ # return self.generate(prompt)
1028
+
1029
+ # def _fallback_generate(self, prompt: str) -> str:
1030
+ # """Fallback generation with simpler model selection"""
1031
+ # print("🔄 Trying fallback generation...")
1032
+
1033
+ # fallback_models = [self.models[2], self.models[0]]
1034
+
1035
+ # for model in fallback_models:
1036
+ # for provider in self.providers:
1037
+ # try:
1038
+ # print(f"🔄 Fallback with {provider['name']} using {model['name']}...")
1039
+ # result = self._call_groq(provider, model, prompt)
1040
+
1041
+ # if result and not result.startswith(("[Error", "[RateLimit]", "[Quota]", "[Auth]", "[Empty]", "[ModelNotFound]")):
1042
+ # print(f"✅ Fallback success with {provider['name']} + {model['name']}")
1043
+ # return result
1044
+ # except Exception as e:
1045
+ # print(f"❌ Fallback failed: {e}")
1046
+ # continue
1047
+
1048
+ # return self._get_user_friendly_error("All models failed")
1049
+
1050
+ # def _call_groq(self, provider, model, prompt: str) -> str:
1051
+ # """Call Groq API with specific provider and model"""
1052
+ # try:
1053
+ # prompt_tokens_approx = len(prompt) // 4
1054
+ # available_tokens = model['max_tokens'] - prompt_tokens_approx - 500
1055
+ # max_response_tokens = max(1000, min(8000, available_tokens))
1056
+
1057
+ # response = provider['client'].chat.completions.create(
1058
+ # model=model['id'],
1059
+ # messages=[{"role": "user", "content": prompt}],
1060
+ # temperature=0.7,
1061
+ # max_tokens=max_response_tokens,
1062
+ # top_p=0.9
1063
+ # )
1064
+
1065
+ # if (response and response.choices and len(response.choices) > 0 and
1066
+ # response.choices[0].message and response.choices[0].message.content):
1067
+
1068
+ # content = response.choices[0].message.content.strip()
1069
+ # return content if content else "[Empty] No content generated"
1070
+ # else:
1071
+ # return "[Empty] Invalid response structure"
1072
+
1073
+ # except Exception as e:
1074
+ # error_msg = str(e).lower()
1075
+
1076
+ # if "rate limit" in error_msg or "429" in error_msg:
1077
+ # return f"[RateLimit] {provider['name']} rate limit exceeded"
1078
+ # elif "quota" in error_msg:
1079
+ # return f"[Quota] {provider['name']} quota exceeded"
1080
+ # elif "authentication" in error_msg:
1081
+ # return f"[Auth] {provider['name']} authentication failed"
1082
+ # elif "context length" in error_msg:
1083
+ # return f"[Length] {provider['name']} content too long"
1084
+ # elif "model not found" in error_msg:
1085
+ # return f"[ModelNotFound] {provider['name']}: {str(e)}"
1086
+ # else:
1087
+ # return f"[Error] {provider['name']}: {str(e)}"
1088
+
1089
+ # def _get_user_friendly_error(self, technical_error: str) -> str:
1090
+ # """Convert technical errors to user-friendly messages"""
1091
+ # error_lower = technical_error.lower()
1092
+
1093
+ # if "rate limit" in error_lower:
1094
+ # return "🚫 **Service Busy** - Please wait a few minutes and try again"
1095
+ # elif "quota" in error_lower:
1096
+ # return "📊 **Daily Limit Reached** - Try again tomorrow"
1097
+ # elif "length" in error_lower:
1098
+ # return "📝 **Content Too Large** - Please break into smaller sections"
1099
+ # else:
1100
+ # return "❌ **Temporary Issue** - Please try again shortly"
1101
+
1102
+ # def get_service_status(self) -> dict:
1103
+ # """Get current status of all providers"""
1104
+ # status = {
1105
+ # 'total_providers': len(self.providers),
1106
+ # 'healthy_providers': len([p for p in self.providers if p['fail_count'] < 2]),
1107
+ # 'providers': [],
1108
+ # 'models': [m['name'] for m in self.models]
1109
+ # }
1110
+
1111
+ # for provider in self.providers:
1112
+ # if provider['fail_count'] >= 3:
1113
+ # status_text = "🔴 Limited"
1114
+ # elif provider['fail_count'] >= 1:
1115
+ # status_text = "🟡 Slow"
1116
+ # else:
1117
+ # status_text = "🟢 Good"
1118
+
1119
+ # status['providers'].append({
1120
+ # 'name': provider['name'],
1121
+ # 'status': status_text,
1122
+ # 'failures': provider['fail_count']
1123
+ # })
1124
+
1125
+ # return status
1126
+
1127
+
1128
+ # class HFGenerator:
1129
+ # """Phi-3 Generator with Auto-Pull, Smart Chunking, and Context Preservation"""
1130
+
1131
+ # def __init__(self, base_url: str = None):
1132
+ # # Use environment variable as default if no base_url provided
1133
+ # self.base_url = base_url or os.getenv("MODEL_URL")
1134
+ # self.model = "phi3:mini"
1135
+ # self.current_requests = 0
1136
+ # self.max_concurrent = 2
1137
+ # self.model_available = False
1138
+ # self._ensure_model_available()
1139
+
1140
+ # def _ensure_model_available(self):
1141
+ # """Check if model is available and pull if needed"""
1142
+ # try:
1143
+ # response = requests.get(f"{self.base_url}/api/tags", timeout=10)
1144
+ # if response.status_code == 200:
1145
+ # models = response.json().get('models', [])
1146
+ # self.model_available = any(model['name'] == self.model for model in models)
1147
+
1148
+ # if not self.model_available:
1149
+ # print(f"🔄 Model {self.model} not found, pulling...")
1150
+ # self._pull_model()
1151
+ # else:
1152
+ # print(f"✅ Model {self.model} is available")
1153
+ # else:
1154
+ # print(f"❌ Could not check models: {response.status_code}")
1155
+ # except Exception as e:
1156
+ # print(f"❌ Error checking models: {e}")
1157
+
1158
+ # def _pull_model(self):
1159
+ # """Pull the Phi-3 model if not available"""
1160
+ # try:
1161
+ # print(f"📥 Pulling {self.model}... This may take a few minutes.")
1162
+
1163
+ # payload = {"name": self.model}
1164
+ # response = requests.post(
1165
+ # f"{self.base_url}/api/pull",
1166
+ # json=payload,
1167
+ # timeout=300 # 5 minute timeout for pull
1168
+ # )
1169
+
1170
+ # if response.status_code == 200:
1171
+ # print(f"✅ Successfully pulled {self.model}")
1172
+ # self.model_available = True
1173
+ # return True
1174
+ # else:
1175
+ # print(f"❌ Failed to pull model: {response.text}")
1176
+ # return False
1177
+
1178
+ # except Exception as e:
1179
+ # print(f"❌ Error pulling model: {e}")
1180
+ # return False
1181
+
1182
+ # def _estimate_tokens(self, text: str) -> int:
1183
+ # """Rough token estimation"""
1184
+ # return len(text) // 4
1185
+
1186
+ # def _chunk_content(self, content: str, max_tokens: int = 2500) -> list:
1187
+ # """Split large content into manageable chunks"""
1188
+ # paragraphs = content.split('\n\n')
1189
+ # chunks = []
1190
+ # current_chunk = ""
1191
+ # current_tokens = 0
1192
+
1193
+ # for paragraph in paragraphs:
1194
+ # para_tokens = self._estimate_tokens(paragraph)
1195
+
1196
+ # if para_tokens > max_tokens:
1197
+ # sentences = paragraph.split('. ')
1198
+ # for sentence in sentences:
1199
+ # sent_tokens = self._estimate_tokens(sentence)
1200
+ # if current_tokens + sent_tokens > max_tokens:
1201
+ # if current_chunk:
1202
+ # chunks.append(current_chunk.strip())
1203
+ # current_chunk = sentence
1204
+ # current_tokens = sent_tokens
1205
+ # else:
1206
+ # current_chunk += " " + sentence
1207
+ # current_tokens += sent_tokens
1208
+ # else:
1209
+ # if current_tokens + para_tokens > max_tokens:
1210
+ # if current_chunk:
1211
+ # chunks.append(current_chunk.strip())
1212
+ # current_chunk = paragraph
1213
+ # current_tokens = para_tokens
1214
+ # else:
1215
+ # current_chunk += "\n\n" + paragraph
1216
+ # current_tokens += para_tokens
1217
+
1218
+ # if current_chunk:
1219
+ # chunks.append(current_chunk.strip())
1220
+
1221
+ # return chunks
1222
+
1223
+ # def _create_context_summary(self, previous_chunks: list) -> str:
1224
+ # """Create a context summary from previous chunks"""
1225
+ # if not previous_chunks:
1226
+ # return ""
1227
+
1228
+ # context_prompt = f"""
1229
+ # Here's a summary of previous sections:
1230
+ # {chr(10).join(previous_chunks)}
1231
+
1232
+ # Provide a brief summary (2-3 sentences) of key points to help understand the next section.
1233
+ # """
1234
+
1235
+ # try:
1236
+ # payload = {
1237
+ # "model": self.model,
1238
+ # "messages": [{"role": "user", "content": context_prompt}],
1239
+ # "stream": False,
1240
+ # "options": {
1241
+ # "temperature": 0.3,
1242
+ # "top_p": 0.8,
1243
+ # "num_predict": 200
1244
+ # }
1245
+ # }
1246
+
1247
+ # response = requests.post(f"{self.base_url}/api/chat", json=payload, timeout=30)
1248
+ # if response.status_code == 200:
1249
+ # return response.json()['message']['content'].strip()
1250
+ # return f"Previous sections covered: {', '.join(previous_chunks[:2])}..."
1251
+ # except Exception:
1252
+ # return f"Context from {len(previous_chunks)} previous sections"
1253
+
1254
+ # def _create_chunk_summary(self, content: str) -> str:
1255
+ # """Create a very brief summary of a chunk's content"""
1256
+ # try:
1257
+ # payload = {
1258
+ # "model": self.model,
1259
+ # "messages": [{"role": "user", "content": f"Summarize key points in 1-2 sentences: {content}"}],
1260
+ # "stream": False,
1261
+ # "options": {
1262
+ # "temperature": 0.3,
1263
+ # "top_p": 0.8,
1264
+ # "num_predict": 100
1265
+ # }
1266
+ # }
1267
+
1268
+ # response = requests.post(f"{self.base_url}/api/chat", json=payload, timeout=20)
1269
+ # if response.status_code == 200:
1270
+ # return response.json()['message']['content'].strip()
1271
+ # return content[:100] + "..."
1272
+ # except:
1273
+ # return content[:100] + "..."
1274
+
1275
+ # def _call_ollama_with_retry(self, payload: dict, max_retries: int = 2) -> Dict:
1276
+ # """Call Ollama API with auto-pull retry"""
1277
+ # for attempt in range(max_retries + 1):
1278
+ # try:
1279
+ # response = requests.post(
1280
+ # f"{self.base_url}/api/chat",
1281
+ # json=payload,
1282
+ # timeout=60
1283
+ # )
1284
+
1285
+ # if response.status_code == 200:
1286
+ # return {"success": True, "data": response.json()}
1287
+ # elif response.status_code == 404 and "not found" in response.text.lower():
1288
+ # print(f"🔄 Model not found, attempting to pull... (attempt {attempt + 1})")
1289
+ # if self._pull_model():
1290
+ # continue # Retry after successful pull
1291
+ # else:
1292
+ # return {"success": False, "error": "Failed to pull model"}
1293
+ # else:
1294
+ # return {"success": False, "error": f"API error {response.status_code}: {response.text}"}
1295
+
1296
+ # except requests.exceptions.Timeout:
1297
+ # if attempt < max_retries:
1298
+ # print(f"⏰ Timeout, retrying... (attempt {attempt + 1})")
1299
+ # time.sleep(2)
1300
+ # else:
1301
+ # return {"success": False, "error": "Request timeout"}
1302
+ # except Exception as e:
1303
+ # return {"success": False, "error": f"Connection failed: {str(e)}"}
1304
+
1305
+ # return {"success": False, "error": "All retries failed"}
1306
+
1307
+ # def generate(self, prompt: str, user_type: str = "student",
1308
+ # academic_level: str = "undergraduate",
1309
+ # content_type: str = "simplified_explanation") -> str:
1310
+ # """Generate educational content with auto-pull and smart features - FIXED to return string"""
1311
+
1312
+ # # Check if we need to pull model first
1313
+ # if not self.model_available:
1314
+ # print("🔄 Model not available, pulling before generation...")
1315
+ # if not self._pull_model():
1316
+ # return f"❌ Phi-3 Error: Phi-3 model is not available and failed to pull. Please check the Ollama server."
1317
+
1318
+ # estimated_tokens = self._estimate_tokens(prompt)
1319
+
1320
+ # # Auto-detect large documents and use chunking
1321
+ # if estimated_tokens > 3000:
1322
+ # result = self.generate_large_content_with_context(prompt, user_type, academic_level, content_type)
1323
+ # if isinstance(result, dict):
1324
+ # return result.get("content", f"❌ Phi-3 Error: {result.get('error', 'Unknown error')}")
1325
+ # return result
1326
+
1327
+ # # Queue management
1328
+ # if self.current_requests >= self.max_concurrent:
1329
+ # queue_position = self.current_requests - self.max_concurrent + 1
1330
+ # estimated_wait = queue_position * 7
1331
+ # return f"❌ Phi-3 Error: Service busy. You're #{queue_position} in queue (~{estimated_wait}s)"
1332
+
1333
+ # self.current_requests += 1
1334
+ # try:
1335
+ # # Use the prompt directly without adding instructional wrapper
1336
+ # # The prompts from tutor_flow and student_flow now tell it to generate content directly
1337
+
1338
+ # # FIXED: Increased token allocation for complete responses
1339
+ # if estimated_tokens > 2000:
1340
+ # max_output_tokens = 2000 # Increased from 500
1341
+ # elif estimated_tokens > 1000:
1342
+ # max_output_tokens = 2500 # Increased from 800
1343
+ # else:
1344
+ # max_output_tokens = 3000 # Increased from 1000
1345
+
1346
+ # payload = {
1347
+ # "model": self.model,
1348
+ # "messages": [{"role": "user", "content": prompt}],
1349
+ # "stream": False,
1350
+ # "options": {
1351
+ # "temperature": 0.7,
1352
+ # "top_p": 0.9,
1353
+ # "num_predict": max_output_tokens
1354
+ # }
1355
+ # }
1356
+
1357
+ # start_time = time.time()
1358
+ # result = self._call_ollama_with_retry(payload)
1359
+ # inference_time = time.time() - start_time
1360
+
1361
+ # if result["success"]:
1362
+ # data = result["data"]
1363
+ # content = data['message']['content'].strip()
1364
+
1365
+ # # Check if content was cut off and retry with more tokens if needed
1366
+ # if self._is_content_cut_off(content):
1367
+ # print("⚠️ Content appears cut off, retrying with more tokens...")
1368
+ # payload["options"]["num_predict"] = 4000 # Max tokens for Phi-3
1369
+ # retry_result = self._call_ollama_with_retry(payload)
1370
+
1371
+ # if retry_result["success"]:
1372
+ # data = retry_result["data"]
1373
+ # content = data['message']['content'].strip()
1374
+
1375
+ # return content
1376
+ # else:
1377
+ # return f"❌ Phi-3 Error: {result['error']}"
1378
+
1379
+ # except Exception as e:
1380
+ # return f"❌ Phi-3 Error: {str(e)}"
1381
+ # finally:
1382
+ # self.current_requests -= 1
1383
+
1384
+ # def _is_content_cut_off(self, content: str) -> bool:
1385
+ # """Check if content appears to be cut off mid-sentence"""
1386
+ # if not content or len(content.strip()) < 100:
1387
+ # return True
1388
+
1389
+ # # Check if it ends with proper punctuation
1390
+ # if content.strip().endswith(('.', '!', '?', '."', '!"', '?"')):
1391
+ # return False
1392
+
1393
+ # # Check if it ends with incomplete sentence markers
1394
+ # if any(content.strip().endswith(marker) for marker in [',', ';', ':', '-', '–', '—']):
1395
+ # return True
1396
+
1397
+ # # Check if it ends with an incomplete word or thought
1398
+ # last_paragraph = content.strip().split('\n')[-1]
1399
+ # if len(last_paragraph.split()) < 5: # Very short last paragraph
1400
+ # return True
1401
+
1402
+ # return False
1403
+
1404
+ # def generate_large_content_with_context(self, prompt: str, user_type: str = "student",
1405
+ # academic_level: str = "undergraduate",
1406
+ # content_type: str = "simplified_explanation") -> str:
1407
+ # """Handle large documents with context preservation - FIXED to return string"""
1408
+
1409
+ # estimated_tokens = self._estimate_tokens(prompt)
1410
+
1411
+ # if estimated_tokens <= 3000:
1412
+ # return self.generate(prompt, user_type, academic_level, content_type)
1413
+
1414
+ # chunks = self._chunk_content(prompt, max_tokens=2500)
1415
+
1416
+ # if len(chunks) > 6:
1417
+ # return f"❌ Phi-3 Error: Document too large ({estimated_tokens} tokens, {len(chunks)} chunks). Please use Groq or break into smaller sections."
1418
+
1419
+ # all_results = []
1420
+ # previous_summaries = []
1421
+
1422
+ # for i, chunk in enumerate(chunks):
1423
+ # print(f"🔄 Processing chunk {i+1}/{len(chunks)} with context...")
1424
+
1425
+ # context_summary = self._create_context_summary(previous_summaries)
1426
+
1427
+ # if context_summary:
1428
+ # chunk_prompt = f"""Part {i+1} of {len(chunks)} - Building on previous context:
1429
+
1430
+ # **PREVIOUS CONTEXT:**
1431
+ # {context_summary}
1432
+
1433
+ # **CURRENT SECTION:**
1434
+ # {chunk}
1435
+
1436
+ # Analyze this section while connecting to the overall context."""
1437
+ # else:
1438
+ # chunk_prompt = f"""Part {i+1} of {len(chunks)}:
1439
+
1440
+ # **CONTENT:**
1441
+ # {chunk}
1442
+
1443
+ # Please analyze this section."""
1444
+
1445
+ # chunk_result = self.generate(chunk_prompt, user_type, academic_level, content_type)
1446
+
1447
+ # if "❌ Phi-3 Error:" not in chunk_result:
1448
+ # chunk_summary = self._create_chunk_summary(chunk_result)
1449
+ # previous_summaries.append(chunk_summary)
1450
+
1451
+ # all_results.append({
1452
+ # "chunk_number": i+1,
1453
+ # "content": chunk_result,
1454
+ # "context_used": bool(context_summary)
1455
+ # })
1456
+ # else:
1457
+ # return f"❌ Phi-3 Error: Failed to process chunk {i+1}: {chunk_result}"
1458
+
1459
+ # if i < len(chunks) - 1:
1460
+ # time.sleep(1)
1461
+
1462
+ # # Combine results
1463
+ # combined_content = "\n\n".join([f"## Part {r['chunk_number']}\n{r['content']}" for r in all_results])
1464
+
1465
+ # return combined_content
1466
+
1467
+ # def health_check(self) -> Dict:
1468
+ # """Comprehensive health check"""
1469
+ # try:
1470
+ # response = requests.get(f"{self.base_url}/api/tags", timeout=10)
1471
+ # if response.status_code == 200:
1472
+ # models = response.json().get('models', [])
1473
+ # model_available = any(model['name'] == self.model for model in models)
1474
+
1475
+ # return {
1476
+ # "server_healthy": True,
1477
+ # "model_available": model_available,
1478
+ # "available_models": [model['name'] for model in models],
1479
+ # "model_required": self.model
1480
+ # }
1481
+ # else:
1482
+ # return {
1483
+ # "server_healthy": False,
1484
+ # "model_available": False,
1485
+ # "error": f"Server returned {response.status_code}"
1486
+ # }
1487
+ # except Exception as e:
1488
+ # return {
1489
+ # "server_healthy": False,
1490
+ # "model_available": False,
1491
+ # "error": str(e)
1492
+ # }
1493
+
1494
+ # def get_available_models(self):
1495
+ # """Get list of available models"""
1496
+ # try:
1497
+ # response = requests.get(f"{self.base_url}/api/tags", timeout=10)
1498
+ # if response.status_code == 200:
1499
+ # return [model['name'] for model in response.json().get('models', [])]
1500
+ # return []
1501
+ # except:
1502
+ # return []
1503
+
1504
+ # def get_queue_status(self):
1505
+ # """Get current queue status"""
1506
+ # return {
1507
+ # "current_requests": self.current_requests,
1508
+ # "max_concurrent": self.max_concurrent,
1509
+ # "available_slots": max(0, self.max_concurrent - self.current_requests)
1510
+ # }
1511
+
1512
+
1513
+ # # Backward compatibility
1514
+ # class GroqGenerator(MultiGroqGenerator):
1515
+ # def __init__(self, model="llama-3.3-70b-versatile"):
1516
+ # super().__init__()
1517
+
1518
+
1519
+ # class ModelManager:
1520
+ # """Unified model manager that handles both Groq and Phi-3 models"""
1521
+
1522
+ # def __init__(self):
1523
+ # self.groq_generator = MultiGroqGenerator()
1524
+ # self.phi3_generator = HFGenerator()
1525
+
1526
+ # def generate(self, prompt: str, model_choice: str = "phi3", **kwargs) -> str:
1527
+ # """Generate content using selected model"""
1528
+ # print(f"🎯 Using model: {model_choice}")
1529
+
1530
+ # if model_choice == "phi3":
1531
+ # # Handle Phi-3 generation - FIXED: Now returns string directly
1532
+ # user_type = kwargs.get('user_type', 'student')
1533
+ # academic_level = kwargs.get('student_level', 'undergraduate')
1534
+ # content_type = kwargs.get('content_type', 'simplified_explanation')
1535
+
1536
+ # result = self.phi3_generator.generate(prompt, user_type, academic_level, content_type)
1537
+ # return result
1538
+ # else:
1539
+ # # Use Groq for comparison - check if this is a large content request
1540
+ # is_large_content = len(prompt) > 8000 # You can adjust this threshold
1541
+
1542
+ # if is_large_content:
1543
+ # return self.groq_generator.generate_large_content(prompt)
1544
+ # else:
1545
+ # return self.groq_generator.generate(prompt)
1546
+
1547
+ # def get_service_status(self) -> dict:
1548
+ # """Get clean research-focused status"""
1549
+ # groq_status = self.groq_generator.get_service_status()
1550
+ # phi3_health = self.phi3_generator.health_check()
1551
+
1552
+ # # Clean Groq status - remove model names, focus on providers
1553
+ # clean_groq_status = {
1554
+ # 'healthy_providers': groq_status['healthy_providers'],
1555
+ # 'total_providers': groq_status['total_providers'],
1556
+ # 'providers': [
1557
+ # {
1558
+ # 'name': provider['name'],
1559
+ # 'failures': provider['failures']
1560
+ # }
1561
+ # for provider in groq_status['providers']
1562
+ # ]
1563
+ # }
1564
+
1565
+ # # Enhanced Phi-3 status
1566
+ # enhanced_phi3_status = {
1567
+ # 'server_healthy': phi3_health['server_healthy'],
1568
+ # 'model_available': phi3_health['model_available'],
1569
+ # 'available_models': phi3_health['available_models'],
1570
+ # 'model_required': phi3_health['model_required']
1571
+ # }
1572
+
1573
+ # return {
1574
+ # "groq": clean_groq_status,
1575
+ # "phi3": enhanced_phi3_status
1576
+ # }
1577
+
1578
+
1579
+ # # Global model manager instance
1580
+ # model_manager = ModelManager()
1581
+
1582
+
1583
+ # # Setup function for your Streamlit app
1584
+ # def setup_generators():
1585
+ # """Setup both generators with health checks"""
1586
+ # print("🔧 Setting up generators...")
1587
+
1588
+ # groq_generator = MultiGroqGenerator()
1589
+
1590
+ # phi3_generator = HFGenerator()
1591
+ # phi3_health = phi3_generator.health_check()
1592
+
1593
+ # print(f"🏥 Phi-3 Health: {phi3_health}")
1594
+
1595
+ # if not phi3_health["server_healthy"]:
1596
+ # print("❌ Phi-3 server is not accessible")
1597
+ # elif not phi3_health["model_available"]:
1598
+ # print("🔄 Phi-3 model needs to be pulled on first use")
1599
+
1600
+ # return {
1601
+ # "groq": groq_generator,
1602
+ # "phi3": phi3_generator
1603
+ # }
1604
+
1605
+
1606
+ # # Test function
1607
+ # def test_generators():
1608
+ # """Test both generators"""
1609
+ # print("🧪 Testing Generators...")
1610
+
1611
+ # generators = setup_generators()
1612
+
1613
+ # # Test Groq
1614
+ # print("\n🔷 Testing Groq...")
1615
+ # groq_result = generators["groq"].generate("Explain photosynthesis briefly")
1616
+ # if not groq_result.startswith("["):
1617
+ # print("✅ Groq working")
1618
+ # else:
1619
+ # print("❌ Groq failed:", groq_result)
1620
+
1621
+ # # Test Phi-3
1622
+ # print("\n🔶 Testing Phi-3...")
1623
+ # phi3_result = generators["phi3"].generate("Explain photosynthesis briefly")
1624
+ # if "❌ Phi-3 Error:" not in phi3_result:
1625
+ # print("✅ Phi-3 working")
1626
+ # else:
1627
+ # print("❌ Phi-3 failed:", phi3_result)
1628
+
1629
+ # # Test health
1630
+ # print("\n🏥 Health Check:")
1631
+ # print(f"Groq providers: {len(generators['groq'].providers)}")
1632
+ # print(f"Phi-3 healthy: {generators['phi3'].health_check()}")
1633
+
1634
+
1635
+ # if __name__ == "__main__":
1636
+ # test_generators()
simulate_adapt.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def adjust_prompt(original_prompt, complexity=None, clarity=None, depth=None, user_type=None, student_level=None, comments=""):
2
+ """
3
+ Enhanced prompt adjustment based on user feedback
4
+ """
5
+
6
+ adjustments = []
7
+ new_prompt = original_prompt
8
+
9
+ # Priority 1: Complexity adjustments (most impactful)
10
+ if complexity == "Too complex":
11
+ adjustments.append("simplified language and added analogies")
12
+ if user_type == "student":
13
+ new_prompt = f"Explain this in simpler, more beginner-friendly terms with practical examples and everyday analogies: {original_prompt}"
14
+ else:
15
+ new_prompt = f"Create a more accessible version suitable for {student_level} students with clear examples: {original_prompt}"
16
+
17
+ elif complexity == "Too simple":
18
+ adjustments.append("added technical depth and advanced concepts")
19
+ new_prompt = f"Expand this with more technical details, deeper insights, and advanced applications while maintaining clarity: {original_prompt}"
20
+
21
+ # Priority 2: Use specific comments if available (most targeted)
22
+ elif comments and len(comments.strip()) > 10:
23
+ # Extract key requests from user comments
24
+ user_requests = extract_requests_from_comments(comments)
25
+ if user_requests:
26
+ adjustments.append(f"addressed: {', '.join(user_requests)}")
27
+ new_prompt = f"{original_prompt}. Specifically: {', '.join(user_requests)}"
28
+
29
+ # Priority 3: Clarity adjustments
30
+ elif clarity and clarity <= 2:
31
+ adjustments.append("improved structure and clarity")
32
+ new_prompt = f"Make this extremely clear and well-structured with step-by-step explanation and better organization: {original_prompt}"
33
+
34
+ # Priority 4: Depth adjustments
35
+ elif depth and depth <= 2:
36
+ adjustments.append("added foundational content")
37
+ new_prompt = f"Provide more basic foundation, introductory content, and build up gradually: {original_prompt}"
38
+
39
+ elif depth and depth >= 4:
40
+ adjustments.append("included advanced insights")
41
+ new_prompt = f"Include more advanced insights, real-world applications, case studies, and deeper analysis: {original_prompt}"
42
+
43
+ # Priority 5: General improvement fallback
44
+ elif complexity or clarity or depth:
45
+ adjustments.append("general improvements based on feedback")
46
+ new_prompt = f"Improve this content to be more effective for learning: {original_prompt}"
47
+
48
+ # Always add learning level context
49
+ if student_level and student_level != "Unknown":
50
+ if "suitable for" not in new_prompt and f"for {student_level}" not in new_prompt:
51
+ new_prompt = f"{new_prompt} - Tailor specifically for {student_level} level understanding"
52
+
53
+ print(f"🔄 Adaptation applied: {adjustments}")
54
+ return new_prompt
55
+
56
+ def extract_requests_from_comments(comments):
57
+ """Extract specific requests from user comments"""
58
+ requests = []
59
+ comment_lower = comments.lower()
60
+
61
+ # Look for specific requests in comments
62
+ if any(word in comment_lower for word in ['example', 'examples']):
63
+ requests.append("more practical examples")
64
+
65
+ if any(word in comment_lower for word in ['confusing', 'unclear', 'hard to understand']):
66
+ requests.append("clearer explanations")
67
+
68
+ if any(word in comment_lower for word in ['analogy', 'metaphor', 'comparison']):
69
+ requests.append("better analogies")
70
+
71
+ if any(word in comment_lower for word in ['step by step', 'step-by-step', 'break down']):
72
+ requests.append("step-by-step breakdown")
73
+
74
+ if any(word in comment_lower for word in ['real world', 'real-world', 'practical']):
75
+ requests.append("real-world applications")
76
+
77
+ if any(word in comment_lower for word in ['visual', 'diagram', 'chart']):
78
+ requests.append("visual explanations")
79
+
80
+ return requests
81
+
82
+ def get_adaptation_explanation(complexity, clarity, depth, comments=""):
83
+ """Generate a user-friendly explanation of what adaptations were made"""
84
+ explanations = []
85
+
86
+ # Use comments for most specific explanations
87
+ if comments and len(comments.strip()) > 10:
88
+ user_requests = extract_requests_from_comments(comments)
89
+ if user_requests:
90
+ explanations.append(f"• Addressed your specific requests: {', '.join(user_requests)}")
91
+
92
+ # Fall back to rating-based explanations
93
+ if not explanations:
94
+ if complexity == "Too complex":
95
+ explanations.append("• Simplified language and added everyday analogies")
96
+ elif complexity == "Too simple":
97
+ explanations.append("• Added more technical depth and advanced concepts")
98
+
99
+ if clarity and clarity <= 2:
100
+ explanations.append("• Improved structure with step-by-step explanations")
101
+ elif clarity and clarity >= 4:
102
+ explanations.append("• Maintained the clear structure you liked")
103
+
104
+ if depth and depth <= 2:
105
+ explanations.append("• Added more foundational content and basics")
106
+ elif depth and depth >= 4:
107
+ explanations.append("• Included advanced insights and applications")
108
+
109
+ # Final fallback
110
+ if not explanations:
111
+ if comments:
112
+ explanations.append("• Incorporated your detailed feedback")
113
+ else:
114
+ explanations.append("• Made general improvements based on your ratings")
115
+
116
+ return "\n".join(explanations)