Upload folder using huggingface_hub
Browse files- ankigen_core/agents/integration.py +132 -53
- ankigen_core/agents/schemas.py +6 -0
- ankigen_core/auto_config.py +56 -13
- ankigen_core/card_generator.py +3 -5
- ankigen_core/cli.py +29 -12
- ankigen_core/ui_logic.py +9 -165
- app.py +22 -146
ankigen_core/agents/integration.py
CHANGED
|
@@ -52,71 +52,58 @@ class AgentOrchestrator:
|
|
| 52 |
library_name: Optional[str] = None,
|
| 53 |
library_topic: Optional[str] = None,
|
| 54 |
generate_cloze: bool = False,
|
|
|
|
|
|
|
| 55 |
) -> Tuple[List[Card], Dict[str, Any]]:
|
| 56 |
-
"""Generate cards using the agent system
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
start_time = datetime.now()
|
| 58 |
|
| 59 |
try:
|
| 60 |
if not self.openai_client:
|
| 61 |
raise ValueError("Agent system not initialized")
|
| 62 |
|
| 63 |
-
logger.info(f"Starting agent-based card generation: {topic} ({subject})")
|
| 64 |
-
|
| 65 |
# Enhance context with library documentation if requested
|
| 66 |
enhanced_context = context or {}
|
| 67 |
library_docs = None
|
| 68 |
|
| 69 |
if library_name:
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
f"Added {len(library_docs)} chars of {library_name} documentation to context"
|
| 104 |
-
)
|
| 105 |
-
else:
|
| 106 |
-
logger.warning(
|
| 107 |
-
f"Could not fetch documentation for library: {library_name}"
|
| 108 |
-
)
|
| 109 |
-
except Exception as e:
|
| 110 |
-
logger.error(f"Error fetching library documentation: {e}")
|
| 111 |
-
|
| 112 |
-
cards = await self._generation_phase(
|
| 113 |
-
topic=topic,
|
| 114 |
-
subject=subject,
|
| 115 |
-
num_cards=num_cards,
|
| 116 |
-
difficulty=difficulty,
|
| 117 |
-
context=enhanced_context,
|
| 118 |
-
generate_cloze=generate_cloze,
|
| 119 |
-
)
|
| 120 |
|
| 121 |
# Collect metadata
|
| 122 |
metadata = {
|
|
@@ -128,6 +115,8 @@ class AgentOrchestrator:
|
|
| 128 |
"difficulty": difficulty,
|
| 129 |
"library_name": library_name if library_name else None,
|
| 130 |
"library_docs_used": bool(library_docs),
|
|
|
|
|
|
|
| 131 |
}
|
| 132 |
|
| 133 |
logger.info(
|
|
@@ -139,6 +128,96 @@ class AgentOrchestrator:
|
|
| 139 |
logger.error(f"Agent-based generation failed: {e}")
|
| 140 |
raise
|
| 141 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
async def _generation_phase(
|
| 143 |
self,
|
| 144 |
topic: str,
|
|
|
|
| 52 |
library_name: Optional[str] = None,
|
| 53 |
library_topic: Optional[str] = None,
|
| 54 |
generate_cloze: bool = False,
|
| 55 |
+
topics_list: Optional[List[str]] = None,
|
| 56 |
+
cards_per_topic: int = 8,
|
| 57 |
) -> Tuple[List[Card], Dict[str, Any]]:
|
| 58 |
+
"""Generate cards using the agent system.
|
| 59 |
+
|
| 60 |
+
If topics_list is provided, generates cards for each subtopic separately
|
| 61 |
+
to ensure comprehensive coverage. Otherwise falls back to single-topic mode.
|
| 62 |
+
"""
|
| 63 |
start_time = datetime.now()
|
| 64 |
|
| 65 |
try:
|
| 66 |
if not self.openai_client:
|
| 67 |
raise ValueError("Agent system not initialized")
|
| 68 |
|
|
|
|
|
|
|
| 69 |
# Enhance context with library documentation if requested
|
| 70 |
enhanced_context = context or {}
|
| 71 |
library_docs = None
|
| 72 |
|
| 73 |
if library_name:
|
| 74 |
+
library_docs = await self._fetch_library_docs(
|
| 75 |
+
library_name, library_topic, num_cards
|
| 76 |
+
)
|
| 77 |
+
if library_docs:
|
| 78 |
+
enhanced_context["library_documentation"] = library_docs
|
| 79 |
+
enhanced_context["library_name"] = library_name
|
| 80 |
+
|
| 81 |
+
# Generate cards - either per-topic or single-topic mode
|
| 82 |
+
if topics_list and len(topics_list) > 0:
|
| 83 |
+
logger.info(
|
| 84 |
+
f"Starting multi-topic generation: {len(topics_list)} topics, "
|
| 85 |
+
f"{cards_per_topic} cards each for '{topic}'"
|
| 86 |
+
)
|
| 87 |
+
cards = await self._generate_cards_per_topic(
|
| 88 |
+
main_subject=topic,
|
| 89 |
+
subject=subject,
|
| 90 |
+
topics_list=topics_list,
|
| 91 |
+
cards_per_topic=cards_per_topic,
|
| 92 |
+
difficulty=difficulty,
|
| 93 |
+
context=enhanced_context,
|
| 94 |
+
generate_cloze=generate_cloze,
|
| 95 |
+
)
|
| 96 |
+
else:
|
| 97 |
+
# Fallback to single-topic mode
|
| 98 |
+
logger.info(f"Starting single-topic generation: {topic} ({subject})")
|
| 99 |
+
cards = await self._generation_phase(
|
| 100 |
+
topic=topic,
|
| 101 |
+
subject=subject,
|
| 102 |
+
num_cards=num_cards,
|
| 103 |
+
difficulty=difficulty,
|
| 104 |
+
context=enhanced_context,
|
| 105 |
+
generate_cloze=generate_cloze,
|
| 106 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
|
| 108 |
# Collect metadata
|
| 109 |
metadata = {
|
|
|
|
| 115 |
"difficulty": difficulty,
|
| 116 |
"library_name": library_name if library_name else None,
|
| 117 |
"library_docs_used": bool(library_docs),
|
| 118 |
+
"topics_list": topics_list,
|
| 119 |
+
"multi_topic_mode": topics_list is not None and len(topics_list) > 0,
|
| 120 |
}
|
| 121 |
|
| 122 |
logger.info(
|
|
|
|
| 128 |
logger.error(f"Agent-based generation failed: {e}")
|
| 129 |
raise
|
| 130 |
|
| 131 |
+
async def _fetch_library_docs(
|
| 132 |
+
self, library_name: str, library_topic: Optional[str], num_cards: int
|
| 133 |
+
) -> Optional[str]:
|
| 134 |
+
"""Fetch library documentation from Context7."""
|
| 135 |
+
logger.info(f"Fetching library documentation for: {library_name}")
|
| 136 |
+
try:
|
| 137 |
+
context7_client = Context7Client()
|
| 138 |
+
|
| 139 |
+
# Dynamic token allocation based on card generation needs
|
| 140 |
+
base_tokens = 8000
|
| 141 |
+
if num_cards > 40:
|
| 142 |
+
token_limit = 12000
|
| 143 |
+
elif num_cards > 20:
|
| 144 |
+
token_limit = 10000
|
| 145 |
+
else:
|
| 146 |
+
token_limit = base_tokens
|
| 147 |
+
|
| 148 |
+
if library_topic:
|
| 149 |
+
token_limit = int(token_limit * 0.8)
|
| 150 |
+
|
| 151 |
+
logger.info(
|
| 152 |
+
f"Fetching {token_limit} tokens of documentation"
|
| 153 |
+
+ (f" for topic: {library_topic}" if library_topic else "")
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
library_docs = await context7_client.fetch_library_documentation(
|
| 157 |
+
library_name, topic=library_topic, tokens=token_limit
|
| 158 |
+
)
|
| 159 |
+
|
| 160 |
+
if library_docs:
|
| 161 |
+
logger.info(
|
| 162 |
+
f"Added {len(library_docs)} chars of {library_name} documentation to context"
|
| 163 |
+
)
|
| 164 |
+
return library_docs
|
| 165 |
+
else:
|
| 166 |
+
logger.warning(
|
| 167 |
+
f"Could not fetch documentation for library: {library_name}"
|
| 168 |
+
)
|
| 169 |
+
return None
|
| 170 |
+
except Exception as e:
|
| 171 |
+
logger.error(f"Error fetching library documentation: {e}")
|
| 172 |
+
return None
|
| 173 |
+
|
| 174 |
+
async def _generate_cards_per_topic(
|
| 175 |
+
self,
|
| 176 |
+
main_subject: str,
|
| 177 |
+
subject: str,
|
| 178 |
+
topics_list: List[str],
|
| 179 |
+
cards_per_topic: int,
|
| 180 |
+
difficulty: str,
|
| 181 |
+
context: Dict[str, Any],
|
| 182 |
+
generate_cloze: bool,
|
| 183 |
+
) -> List[Card]:
|
| 184 |
+
"""Generate cards for each topic in the topics_list."""
|
| 185 |
+
all_cards: List[Card] = []
|
| 186 |
+
total_topics = len(topics_list)
|
| 187 |
+
|
| 188 |
+
for i, subtopic in enumerate(topics_list):
|
| 189 |
+
topic_num = i + 1
|
| 190 |
+
logger.info(
|
| 191 |
+
f"Generating topic {topic_num}/{total_topics}: {subtopic} "
|
| 192 |
+
f"({cards_per_topic} cards)"
|
| 193 |
+
)
|
| 194 |
+
|
| 195 |
+
# Add topic context
|
| 196 |
+
topic_context = {
|
| 197 |
+
**context,
|
| 198 |
+
"main_subject": main_subject,
|
| 199 |
+
"topic_index": topic_num,
|
| 200 |
+
"total_topics": total_topics,
|
| 201 |
+
"current_subtopic": subtopic,
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
cards = await self._generation_phase(
|
| 205 |
+
topic=subtopic,
|
| 206 |
+
subject=subject,
|
| 207 |
+
num_cards=cards_per_topic,
|
| 208 |
+
difficulty=difficulty,
|
| 209 |
+
context=topic_context,
|
| 210 |
+
generate_cloze=generate_cloze,
|
| 211 |
+
)
|
| 212 |
+
|
| 213 |
+
all_cards.extend(cards)
|
| 214 |
+
logger.info(
|
| 215 |
+
f"Topic {topic_num}/{total_topics} complete: {len(cards)} cards. "
|
| 216 |
+
f"Total: {len(all_cards)}"
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
return all_cards
|
| 220 |
+
|
| 221 |
async def _generation_phase(
|
| 222 |
self,
|
| 223 |
topic: str,
|
ankigen_core/agents/schemas.py
CHANGED
|
@@ -155,6 +155,12 @@ class AutoConfigSchema(BaseModel):
|
|
| 155 |
topic_number: int = Field(
|
| 156 |
..., ge=2, le=20, description="Number of topics to generate (2-20)"
|
| 157 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
cards_per_topic: int = Field(
|
| 159 |
..., ge=2, le=30, description="Number of cards per topic (2-30)"
|
| 160 |
)
|
|
|
|
| 155 |
topic_number: int = Field(
|
| 156 |
..., ge=2, le=20, description="Number of topics to generate (2-20)"
|
| 157 |
)
|
| 158 |
+
topics_list: List[str] = Field(
|
| 159 |
+
...,
|
| 160 |
+
min_length=2,
|
| 161 |
+
max_length=20,
|
| 162 |
+
description="List of distinct subtopics to cover, ordered by learning progression",
|
| 163 |
+
)
|
| 164 |
cards_per_topic: int = Field(
|
| 165 |
..., ge=2, le=30, description="Number of cards per topic (2-30)"
|
| 166 |
)
|
ankigen_core/auto_config.py
CHANGED
|
@@ -16,11 +16,30 @@ class AutoConfigService:
|
|
| 16 |
self.context7_client = Context7Client()
|
| 17 |
|
| 18 |
async def analyze_subject(
|
| 19 |
-
self,
|
|
|
|
|
|
|
|
|
|
| 20 |
) -> AutoConfigSchema:
|
| 21 |
-
"""Analyze a subject string and return configuration settings
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
CRITICAL PRINCIPLE: Quality over quantity. Focus on fundamental concepts that unlock understanding, not trivial facts.
|
| 26 |
|
|
@@ -32,14 +51,24 @@ Consider:
|
|
| 32 |
- "Docker networking" → documentation_focus: "networking, network drivers, container communication"
|
| 33 |
3. Identify the scope: narrow (specific feature), medium (several related topics), broad (full overview)
|
| 34 |
4. Determine content type: concepts (theory/understanding), syntax (code/commands), api (library usage), practical (hands-on skills)
|
| 35 |
-
5.
|
| 36 |
6. Recommend cloze cards for syntax/code, basic cards for concepts
|
| 37 |
7. Choose model based on complexity: gpt-4.1 for complex topics, gpt-4.1-nano for basic/simple
|
| 38 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
IMPORTANT - Focus on HIGH-VALUE topics:
|
| 40 |
- GOOD topics: Core concepts, fundamental principles, mental models, design patterns, key abstractions
|
| 41 |
- AVOID topics: Trivial commands (like "docker ps"), basic syntax that's easily googled, minor API details
|
| 42 |
-
- Example: For Docker, focus on "container lifecycle", "image layers", "networking models" NOT "list of docker commands"
|
| 43 |
|
| 44 |
Guidelines for settings (MINIMUM 30 cards total):
|
| 45 |
- Narrow/specific scope: 4-5 essential topics with 8-10 cards each (32-50 cards)
|
|
@@ -53,12 +82,6 @@ Learning preference suggestions:
|
|
| 53 |
- For practical: "Emphasize core patterns and principles with real-world applications"
|
| 54 |
- For theory: "Build deep conceptual understanding with progressive complexity"
|
| 55 |
|
| 56 |
-
Documentation focus examples (be specific and thorough):
|
| 57 |
-
- "Basic Pandas Dataframe" → "dataframe creation, indexing, selection, basic operations, data types"
|
| 58 |
-
- "React hooks" → "useState, useEffect, custom hooks, hook rules, common patterns"
|
| 59 |
-
- "Docker basics" → "containers, images, Dockerfile, volumes, basic networking"
|
| 60 |
-
- "TypeScript types" → "generics, conditional types, mapped types, utility types, type inference"
|
| 61 |
-
|
| 62 |
Return a JSON object matching the AutoConfigSchema."""
|
| 63 |
|
| 64 |
user_prompt = f"""Analyze this subject for flashcard generation: "{subject}"
|
|
@@ -89,10 +112,19 @@ Provide a brief rationale for your choices."""
|
|
| 89 |
except Exception as e:
|
| 90 |
logger.error(f"Failed to analyze subject: {e}")
|
| 91 |
# Return sensible defaults on error (still aim for good card count)
|
|
|
|
| 92 |
return AutoConfigSchema(
|
| 93 |
library_search_term="",
|
| 94 |
documentation_focus=None,
|
| 95 |
topic_number=6,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
cards_per_topic=8,
|
| 97 |
learning_preferences="Focus on fundamental concepts and core principles with practical examples",
|
| 98 |
generate_cloze=False,
|
|
@@ -103,13 +135,21 @@ Provide a brief rationale for your choices."""
|
|
| 103 |
)
|
| 104 |
|
| 105 |
async def auto_configure(
|
| 106 |
-
self,
|
|
|
|
|
|
|
|
|
|
| 107 |
) -> Dict[str, Any]:
|
| 108 |
"""
|
| 109 |
Complete auto-configuration pipeline:
|
| 110 |
1. Analyze subject with AI
|
| 111 |
2. Search Context7 for library if detected
|
| 112 |
3. Return complete configuration for UI
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
"""
|
| 114 |
|
| 115 |
if not subject or not subject.strip():
|
|
@@ -119,7 +159,9 @@ Provide a brief rationale for your choices."""
|
|
| 119 |
logger.info(f"Starting auto-configuration for subject: '{subject}'")
|
| 120 |
|
| 121 |
# Step 1: Analyze the subject
|
| 122 |
-
config = await self.analyze_subject(
|
|
|
|
|
|
|
| 123 |
|
| 124 |
# Step 2: Search Context7 for library if one was detected
|
| 125 |
library_id = None
|
|
@@ -145,6 +187,7 @@ Provide a brief rationale for your choices."""
|
|
| 145 |
"library_name": config.library_search_term if library_id else "",
|
| 146 |
"library_topic": config.documentation_focus or "",
|
| 147 |
"topic_number": config.topic_number,
|
|
|
|
| 148 |
"cards_per_topic": config.cards_per_topic,
|
| 149 |
"preference_prompt": config.learning_preferences,
|
| 150 |
"generate_cloze_checkbox": config.generate_cloze,
|
|
|
|
| 16 |
self.context7_client = Context7Client()
|
| 17 |
|
| 18 |
async def analyze_subject(
|
| 19 |
+
self,
|
| 20 |
+
subject: str,
|
| 21 |
+
openai_client: AsyncOpenAI,
|
| 22 |
+
target_topic_count: int | None = None,
|
| 23 |
) -> AutoConfigSchema:
|
| 24 |
+
"""Analyze a subject string and return configuration settings.
|
| 25 |
|
| 26 |
+
Args:
|
| 27 |
+
subject: The subject to analyze
|
| 28 |
+
openai_client: OpenAI client for LLM calls
|
| 29 |
+
target_topic_count: If provided, forces exactly this many topics in decomposition
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
# Build topic count instruction if override provided
|
| 33 |
+
topic_count_instruction = ""
|
| 34 |
+
if target_topic_count is not None:
|
| 35 |
+
topic_count_instruction = f"""
|
| 36 |
+
IMPORTANT OVERRIDE: The user has requested exactly {target_topic_count} topics.
|
| 37 |
+
You MUST set topic_number to {target_topic_count} and provide exactly {target_topic_count} items in topics_list.
|
| 38 |
+
Choose the {target_topic_count} most important/foundational subtopics for this subject.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
system_prompt = f"""You are an educational content analyzer specializing in spaced repetition learning. Analyze the given subject and determine flashcard generation settings that focus on ESSENTIAL concepts.
|
| 42 |
+
{topic_count_instruction}
|
| 43 |
|
| 44 |
CRITICAL PRINCIPLE: Quality over quantity. Focus on fundamental concepts that unlock understanding, not trivial facts.
|
| 45 |
|
|
|
|
| 51 |
- "Docker networking" → documentation_focus: "networking, network drivers, container communication"
|
| 52 |
3. Identify the scope: narrow (specific feature), medium (several related topics), broad (full overview)
|
| 53 |
4. Determine content type: concepts (theory/understanding), syntax (code/commands), api (library usage), practical (hands-on skills)
|
| 54 |
+
5. TOPIC DECOMPOSITION: Break down the subject into distinct subtopics that together provide comprehensive coverage
|
| 55 |
6. Recommend cloze cards for syntax/code, basic cards for concepts
|
| 56 |
7. Choose model based on complexity: gpt-4.1 for complex topics, gpt-4.1-nano for basic/simple
|
| 57 |
|
| 58 |
+
TOPIC DECOMPOSITION (topics_list):
|
| 59 |
+
You MUST provide a topics_list - a list of distinct subtopics that together cover the subject comprehensively.
|
| 60 |
+
- Each topic should be specific and non-overlapping
|
| 61 |
+
- Order topics from foundational to advanced (learning progression)
|
| 62 |
+
- The number of topics should match topic_number
|
| 63 |
+
|
| 64 |
+
Examples:
|
| 65 |
+
- "React Hooks" → topics_list: ["useState fundamentals", "useEffect and lifecycle", "useRef and useContext", "custom hooks patterns", "performance with useMemo/useCallback", "testing hooks"]
|
| 66 |
+
- "Docker basics" → topics_list: ["containers vs VMs", "images and Dockerfile", "container lifecycle", "volumes and persistence", "networking fundamentals", "docker-compose basics"]
|
| 67 |
+
- "Machine Learning" → topics_list: ["supervised vs unsupervised", "regression models", "classification models", "model evaluation metrics", "overfitting and regularization", "feature engineering", "cross-validation"]
|
| 68 |
+
|
| 69 |
IMPORTANT - Focus on HIGH-VALUE topics:
|
| 70 |
- GOOD topics: Core concepts, fundamental principles, mental models, design patterns, key abstractions
|
| 71 |
- AVOID topics: Trivial commands (like "docker ps"), basic syntax that's easily googled, minor API details
|
|
|
|
| 72 |
|
| 73 |
Guidelines for settings (MINIMUM 30 cards total):
|
| 74 |
- Narrow/specific scope: 4-5 essential topics with 8-10 cards each (32-50 cards)
|
|
|
|
| 82 |
- For practical: "Emphasize core patterns and principles with real-world applications"
|
| 83 |
- For theory: "Build deep conceptual understanding with progressive complexity"
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
Return a JSON object matching the AutoConfigSchema."""
|
| 86 |
|
| 87 |
user_prompt = f"""Analyze this subject for flashcard generation: "{subject}"
|
|
|
|
| 112 |
except Exception as e:
|
| 113 |
logger.error(f"Failed to analyze subject: {e}")
|
| 114 |
# Return sensible defaults on error (still aim for good card count)
|
| 115 |
+
# Use the subject as a single topic as fallback
|
| 116 |
return AutoConfigSchema(
|
| 117 |
library_search_term="",
|
| 118 |
documentation_focus=None,
|
| 119 |
topic_number=6,
|
| 120 |
+
topics_list=[
|
| 121 |
+
f"{subject} - fundamentals",
|
| 122 |
+
f"{subject} - core concepts",
|
| 123 |
+
f"{subject} - practical applications",
|
| 124 |
+
f"{subject} - common patterns",
|
| 125 |
+
f"{subject} - best practices",
|
| 126 |
+
f"{subject} - advanced topics",
|
| 127 |
+
],
|
| 128 |
cards_per_topic=8,
|
| 129 |
learning_preferences="Focus on fundamental concepts and core principles with practical examples",
|
| 130 |
generate_cloze=False,
|
|
|
|
| 135 |
)
|
| 136 |
|
| 137 |
async def auto_configure(
|
| 138 |
+
self,
|
| 139 |
+
subject: str,
|
| 140 |
+
openai_client: AsyncOpenAI,
|
| 141 |
+
target_topic_count: int | None = None,
|
| 142 |
) -> Dict[str, Any]:
|
| 143 |
"""
|
| 144 |
Complete auto-configuration pipeline:
|
| 145 |
1. Analyze subject with AI
|
| 146 |
2. Search Context7 for library if detected
|
| 147 |
3. Return complete configuration for UI
|
| 148 |
+
|
| 149 |
+
Args:
|
| 150 |
+
subject: The subject to analyze
|
| 151 |
+
openai_client: OpenAI client for LLM calls
|
| 152 |
+
target_topic_count: If provided, forces exactly this many topics
|
| 153 |
"""
|
| 154 |
|
| 155 |
if not subject or not subject.strip():
|
|
|
|
| 159 |
logger.info(f"Starting auto-configuration for subject: '{subject}'")
|
| 160 |
|
| 161 |
# Step 1: Analyze the subject
|
| 162 |
+
config = await self.analyze_subject(
|
| 163 |
+
subject, openai_client, target_topic_count=target_topic_count
|
| 164 |
+
)
|
| 165 |
|
| 166 |
# Step 2: Search Context7 for library if one was detected
|
| 167 |
library_id = None
|
|
|
|
| 187 |
"library_name": config.library_search_term if library_id else "",
|
| 188 |
"library_topic": config.documentation_focus or "",
|
| 189 |
"topic_number": config.topic_number,
|
| 190 |
+
"topics_list": config.topics_list,
|
| 191 |
"cards_per_topic": config.cards_per_topic,
|
| 192 |
"preference_prompt": config.learning_preferences,
|
| 193 |
"generate_cloze_checkbox": config.generate_cloze,
|
ankigen_core/card_generator.py
CHANGED
|
@@ -52,11 +52,6 @@ GENERATION_MODES = [
|
|
| 52 |
"label": "Single Subject",
|
| 53 |
"description": "Generate cards for a specific topic",
|
| 54 |
},
|
| 55 |
-
{
|
| 56 |
-
"value": "path",
|
| 57 |
-
"label": "Learning Path",
|
| 58 |
-
"description": "Break down a job description or learning goal into subjects",
|
| 59 |
-
},
|
| 60 |
{
|
| 61 |
"value": "text",
|
| 62 |
"label": "From Text",
|
|
@@ -140,6 +135,7 @@ async def orchestrate_card_generation(
|
|
| 140 |
use_llm_judge: bool = False,
|
| 141 |
library_name: str = None,
|
| 142 |
library_topic: str = None,
|
|
|
|
| 143 |
):
|
| 144 |
"""Orchestrates the card generation process based on UI inputs."""
|
| 145 |
logger.info(f"Starting card generation orchestration in {generation_mode} mode")
|
|
@@ -175,6 +171,8 @@ async def orchestrate_card_generation(
|
|
| 175 |
library_name=library_name,
|
| 176 |
library_topic=library_topic,
|
| 177 |
generate_cloze=generate_cloze,
|
|
|
|
|
|
|
| 178 |
)
|
| 179 |
|
| 180 |
token_usage_html = _get_token_usage_html(token_tracker)
|
|
|
|
| 52 |
"label": "Single Subject",
|
| 53 |
"description": "Generate cards for a specific topic",
|
| 54 |
},
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
{
|
| 56 |
"value": "text",
|
| 57 |
"label": "From Text",
|
|
|
|
| 135 |
use_llm_judge: bool = False,
|
| 136 |
library_name: str = None,
|
| 137 |
library_topic: str = None,
|
| 138 |
+
topics_list: List[str] = None,
|
| 139 |
):
|
| 140 |
"""Orchestrates the card generation process based on UI inputs."""
|
| 141 |
logger.info(f"Starting card generation orchestration in {generation_mode} mode")
|
|
|
|
| 171 |
library_name=library_name,
|
| 172 |
library_topic=library_topic,
|
| 173 |
generate_cloze=generate_cloze,
|
| 174 |
+
topics_list=topics_list,
|
| 175 |
+
cards_per_topic=cards_per_topic,
|
| 176 |
)
|
| 177 |
|
| 178 |
token_usage_html = _get_token_usage_html(token_tracker)
|
ankigen_core/cli.py
CHANGED
|
@@ -13,6 +13,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn, BarColumn
|
|
| 13 |
from rich.table import Table
|
| 14 |
from rich.panel import Panel
|
| 15 |
|
|
|
|
| 16 |
from ankigen_core.auto_config import AutoConfigService
|
| 17 |
from ankigen_core.card_generator import orchestrate_card_generation
|
| 18 |
from ankigen_core.exporters import export_dataframe_to_apkg, export_dataframe_to_csv
|
|
@@ -55,13 +56,13 @@ async def auto_configure_from_prompt(
|
|
| 55 |
await client_manager.initialize_client(api_key)
|
| 56 |
openai_client = client_manager.get_client()
|
| 57 |
|
| 58 |
-
# Get auto-config
|
| 59 |
auto_config_service = AutoConfigService()
|
| 60 |
-
config = await auto_config_service.auto_configure(
|
|
|
|
|
|
|
| 61 |
|
| 62 |
-
# Apply overrides
|
| 63 |
-
if override_topics is not None:
|
| 64 |
-
config["topic_number"] = override_topics
|
| 65 |
if override_cards is not None:
|
| 66 |
config["cards_per_topic"] = override_cards
|
| 67 |
if override_model is not None:
|
|
@@ -86,6 +87,17 @@ async def auto_configure_from_prompt(
|
|
| 86 |
table.add_row("Library", config.get("library_name"))
|
| 87 |
if config.get("library_topic"):
|
| 88 |
table.add_row("Library Topic", config.get("library_topic"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
if config.get("preference_prompt"):
|
| 90 |
table.add_row(
|
| 91 |
"Learning Focus", config.get("preference_prompt", "")[:50] + "..."
|
|
@@ -142,6 +154,7 @@ async def generate_cards_from_config(
|
|
| 142 |
library_topic=config.get("library_topic")
|
| 143 |
if config.get("library_topic")
|
| 144 |
else None,
|
|
|
|
| 145 |
)
|
| 146 |
|
| 147 |
progress.update(task, completed=100)
|
|
@@ -327,13 +340,17 @@ def main(
|
|
| 327 |
summary.add_row("Output File:", f"[bold]{exported_path}[/bold]")
|
| 328 |
summary.add_row("File Size:", f"{file_size:.1f} KB")
|
| 329 |
|
| 330 |
-
#
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 337 |
|
| 338 |
console.print(
|
| 339 |
Panel(summary, border_style="green", title="Generation Complete")
|
|
|
|
| 13 |
from rich.table import Table
|
| 14 |
from rich.panel import Panel
|
| 15 |
|
| 16 |
+
from ankigen_core.agents.token_tracker import get_token_tracker
|
| 17 |
from ankigen_core.auto_config import AutoConfigService
|
| 18 |
from ankigen_core.card_generator import orchestrate_card_generation
|
| 19 |
from ankigen_core.exporters import export_dataframe_to_apkg, export_dataframe_to_csv
|
|
|
|
| 56 |
await client_manager.initialize_client(api_key)
|
| 57 |
openai_client = client_manager.get_client()
|
| 58 |
|
| 59 |
+
# Get auto-config (pass topic count override so LLM decomposes correctly)
|
| 60 |
auto_config_service = AutoConfigService()
|
| 61 |
+
config = await auto_config_service.auto_configure(
|
| 62 |
+
prompt, openai_client, target_topic_count=override_topics
|
| 63 |
+
)
|
| 64 |
|
| 65 |
+
# Apply remaining overrides (topics already handled in auto_configure)
|
|
|
|
|
|
|
| 66 |
if override_cards is not None:
|
| 67 |
config["cards_per_topic"] = override_cards
|
| 68 |
if override_model is not None:
|
|
|
|
| 87 |
table.add_row("Library", config.get("library_name"))
|
| 88 |
if config.get("library_topic"):
|
| 89 |
table.add_row("Library Topic", config.get("library_topic"))
|
| 90 |
+
|
| 91 |
+
# Display discovered topics
|
| 92 |
+
if config.get("topics_list"):
|
| 93 |
+
topics = config["topics_list"]
|
| 94 |
+
# Show first few topics, indicate if there are more
|
| 95 |
+
if len(topics) <= 4:
|
| 96 |
+
topics_str = ", ".join(topics)
|
| 97 |
+
else:
|
| 98 |
+
topics_str = ", ".join(topics[:3]) + f", ... (+{len(topics) - 3} more)"
|
| 99 |
+
table.add_row("Subtopics", topics_str)
|
| 100 |
+
|
| 101 |
if config.get("preference_prompt"):
|
| 102 |
table.add_row(
|
| 103 |
"Learning Focus", config.get("preference_prompt", "")[:50] + "..."
|
|
|
|
| 154 |
library_topic=config.get("library_topic")
|
| 155 |
if config.get("library_topic")
|
| 156 |
else None,
|
| 157 |
+
topics_list=config.get("topics_list"),
|
| 158 |
)
|
| 159 |
|
| 160 |
progress.update(task, completed=100)
|
|
|
|
| 340 |
summary.add_row("Output File:", f"[bold]{exported_path}[/bold]")
|
| 341 |
summary.add_row("File Size:", f"{file_size:.1f} KB")
|
| 342 |
|
| 343 |
+
# Get token usage from tracker
|
| 344 |
+
tracker = get_token_tracker()
|
| 345 |
+
session = tracker.get_session_summary()
|
| 346 |
+
if session["total_tokens"] > 0:
|
| 347 |
+
# Calculate totals across all models
|
| 348 |
+
total_input = sum(u.prompt_tokens for u in tracker.usage_history)
|
| 349 |
+
total_output = sum(u.completion_tokens for u in tracker.usage_history)
|
| 350 |
+
summary.add_row(
|
| 351 |
+
"Tokens:",
|
| 352 |
+
f"{total_input:,} in / {total_output:,} out ({session['total_tokens']:,} total)",
|
| 353 |
+
)
|
| 354 |
|
| 355 |
console.print(
|
| 356 |
Panel(summary, border_style="green", title="Generation Complete")
|
ankigen_core/ui_logic.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# Module for functions that build or manage UI sections/logic
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
-
import pandas as pd
|
| 5 |
from typing import (
|
| 6 |
Callable,
|
| 7 |
List,
|
|
@@ -51,24 +51,19 @@ crawler_ui_logger = get_logger() # Keep this definition
|
|
| 51 |
def update_mode_visibility(
|
| 52 |
mode: str,
|
| 53 |
current_subject: str,
|
| 54 |
-
current_description: str,
|
| 55 |
current_text: str,
|
| 56 |
current_url: str,
|
| 57 |
):
|
| 58 |
"""Updates visibility and values of UI elements based on generation mode."""
|
| 59 |
is_subject = mode == "subject"
|
| 60 |
-
is_path = mode == "path"
|
| 61 |
is_text = mode == "text"
|
| 62 |
is_web = mode == "web"
|
| 63 |
|
| 64 |
# Determine value persistence or clearing
|
| 65 |
subject_val = current_subject if is_subject else ""
|
| 66 |
-
description_val = current_description if is_path else ""
|
| 67 |
text_val = current_text if is_text else ""
|
| 68 |
url_val = current_url if is_web else ""
|
| 69 |
|
| 70 |
-
cards_output_visible = is_subject or is_text or is_web
|
| 71 |
-
|
| 72 |
# Define standard columns for empty DataFrames
|
| 73 |
main_output_df_columns = [
|
| 74 |
"Index",
|
|
@@ -82,173 +77,22 @@ def update_mode_visibility(
|
|
| 82 |
"Learning_Outcomes",
|
| 83 |
"Difficulty",
|
| 84 |
]
|
| 85 |
-
subjects_list_df_columns = ["Subject", "Prerequisites", "Time Estimate"]
|
| 86 |
|
| 87 |
return (
|
| 88 |
gr.update(visible=is_subject), # 1 subject_mode (Group)
|
| 89 |
-
gr.update(visible=
|
| 90 |
-
gr.update(visible=
|
| 91 |
-
gr.update(visible=
|
| 92 |
-
gr.update(
|
| 93 |
-
gr.update(
|
| 94 |
-
|
| 95 |
-
), # 6 cards_output (Group for main table)
|
| 96 |
-
gr.update(value=subject_val), # Now 7th item (was 8th)
|
| 97 |
-
gr.update(value=description_val), # Now 8th item (was 9th)
|
| 98 |
-
gr.update(value=text_val), # Now 9th item (was 10th)
|
| 99 |
-
gr.update(value=url_val), # Now 10th item (was 11th)
|
| 100 |
gr.update(
|
| 101 |
value=pd.DataFrame(columns=main_output_df_columns)
|
| 102 |
-
), #
|
| 103 |
-
gr.update(
|
| 104 |
-
value=pd.DataFrame(columns=subjects_list_df_columns)
|
| 105 |
-
), # Now 12th item (was 13th)
|
| 106 |
-
gr.update(value=""), # Now 13th item (was 14th)
|
| 107 |
-
gr.update(value=""), # Now 14th item (was 15th)
|
| 108 |
gr.update(
|
| 109 |
value="<div><b>Total Cards Generated:</b> <span id='total-cards-count'>0</span></div>",
|
| 110 |
visible=False,
|
| 111 |
-
), #
|
| 112 |
-
)
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
def use_selected_subjects(subjects_df: pd.DataFrame | None):
|
| 116 |
-
"""Updates UI to use subjects from learning path analysis."""
|
| 117 |
-
if subjects_df is None or subjects_df.empty:
|
| 118 |
-
gr.Warning("No subjects available to copy from Learning Path analysis.")
|
| 119 |
-
# Return updates that change nothing for all 18 outputs
|
| 120 |
-
return (
|
| 121 |
-
gr.update(), # 1 generation_mode
|
| 122 |
-
gr.update(), # 2 subject_mode
|
| 123 |
-
gr.update(), # 3 path_mode
|
| 124 |
-
gr.update(), # 4 text_mode
|
| 125 |
-
gr.update(), # 5 web_mode
|
| 126 |
-
gr.update(), # 6 path_results
|
| 127 |
-
gr.update(), # 7 cards_output
|
| 128 |
-
gr.update(), # 8 subject
|
| 129 |
-
gr.update(), # 9 description
|
| 130 |
-
gr.update(), # 10 source_text
|
| 131 |
-
gr.update(), # 11 web_crawl_url_input
|
| 132 |
-
gr.update(), # 12 topic_number
|
| 133 |
-
gr.update(), # 13 preference_prompt
|
| 134 |
-
gr.update(
|
| 135 |
-
value=pd.DataFrame(
|
| 136 |
-
columns=[
|
| 137 |
-
"Index",
|
| 138 |
-
"Topic",
|
| 139 |
-
"Card_Type",
|
| 140 |
-
"Question",
|
| 141 |
-
"Answer",
|
| 142 |
-
"Explanation",
|
| 143 |
-
"Example",
|
| 144 |
-
"Prerequisites",
|
| 145 |
-
"Learning_Outcomes",
|
| 146 |
-
"Difficulty",
|
| 147 |
-
]
|
| 148 |
-
)
|
| 149 |
-
), # 14 output (DataFrame)
|
| 150 |
-
gr.update(
|
| 151 |
-
value=pd.DataFrame(
|
| 152 |
-
columns=["Subject", "Prerequisites", "Time Estimate"]
|
| 153 |
-
)
|
| 154 |
-
), # 15 subjects_list (DataFrame)
|
| 155 |
-
gr.update(), # 16 learning_order
|
| 156 |
-
gr.update(), # 17 projects
|
| 157 |
-
gr.update(visible=False), # 18 total_cards_html
|
| 158 |
-
)
|
| 159 |
-
|
| 160 |
-
try:
|
| 161 |
-
subjects = subjects_df["Subject"].tolist()
|
| 162 |
-
combined_subject = ", ".join(subjects)
|
| 163 |
-
# Ensure suggested_topics is an int, Gradio sliders expect int/float for value
|
| 164 |
-
suggested_topics = int(min(len(subjects) + 1, 20))
|
| 165 |
-
except KeyError:
|
| 166 |
-
gr.Error("Learning path analysis result is missing the 'Subject' column.")
|
| 167 |
-
# Return no-change updates for all 18 outputs
|
| 168 |
-
return (
|
| 169 |
-
gr.update(), # 1 generation_mode
|
| 170 |
-
gr.update(), # 2 subject_mode
|
| 171 |
-
gr.update(), # 3 path_mode
|
| 172 |
-
gr.update(), # 4 text_mode
|
| 173 |
-
gr.update(), # 5 web_mode
|
| 174 |
-
gr.update(), # 6 path_results
|
| 175 |
-
gr.update(), # 7 cards_output
|
| 176 |
-
gr.update(), # 8 subject
|
| 177 |
-
gr.update(), # 9 description
|
| 178 |
-
gr.update(), # 10 source_text
|
| 179 |
-
gr.update(), # 11 web_crawl_url_input
|
| 180 |
-
gr.update(), # 12 topic_number
|
| 181 |
-
gr.update(), # 13 preference_prompt
|
| 182 |
-
gr.update(
|
| 183 |
-
value=pd.DataFrame(
|
| 184 |
-
columns=[
|
| 185 |
-
"Index",
|
| 186 |
-
"Topic",
|
| 187 |
-
"Card_Type",
|
| 188 |
-
"Question",
|
| 189 |
-
"Answer",
|
| 190 |
-
"Explanation",
|
| 191 |
-
"Example",
|
| 192 |
-
"Prerequisites",
|
| 193 |
-
"Learning_Outcomes",
|
| 194 |
-
"Difficulty",
|
| 195 |
-
]
|
| 196 |
-
)
|
| 197 |
-
), # 14 output (DataFrame)
|
| 198 |
-
gr.update(
|
| 199 |
-
value=pd.DataFrame(
|
| 200 |
-
columns=["Subject", "Prerequisites", "Time Estimate"]
|
| 201 |
-
)
|
| 202 |
-
), # 15 subjects_list (DataFrame)
|
| 203 |
-
gr.update(), # 16 learning_order
|
| 204 |
-
gr.update(), # 17 projects
|
| 205 |
-
gr.update(visible=False), # 18 total_cards_html
|
| 206 |
-
)
|
| 207 |
-
|
| 208 |
-
# Corresponds to outputs in app.py for use_subjects.click:
|
| 209 |
-
# [generation_mode, subject_mode, path_mode, text_mode, web_mode, path_results, cards_output,
|
| 210 |
-
# subject, description, source_text, web_crawl_url_input, topic_number, preference_prompt,
|
| 211 |
-
# output, subjects_list, learning_order, projects, total_cards_html]
|
| 212 |
-
return (
|
| 213 |
-
gr.update(value="subject"), # 1 generation_mode (Radio)
|
| 214 |
-
gr.update(visible=True), # 2 subject_mode (Group)
|
| 215 |
-
gr.update(visible=False), # 3 path_mode (Group)
|
| 216 |
-
gr.update(visible=False), # 4 text_mode (Group)
|
| 217 |
-
gr.update(visible=False), # 5 web_mode (Group)
|
| 218 |
-
gr.update(visible=False), # 6 path_results (Group)
|
| 219 |
-
gr.update(visible=True), # 7 cards_output (Group)
|
| 220 |
-
gr.update(value=combined_subject), # 8 subject (Textbox)
|
| 221 |
-
gr.update(value=""), # 9 description (Textbox)
|
| 222 |
-
gr.update(value=""), # 10 source_text (Textbox)
|
| 223 |
-
gr.update(value=""), # 11 web_crawl_url_input (Textbox)
|
| 224 |
-
gr.update(value=suggested_topics), # 12 topic_number (Slider)
|
| 225 |
-
gr.update(
|
| 226 |
-
value="Focus on connections between these subjects and their practical applications."
|
| 227 |
-
), # 13 preference_prompt (Textbox)
|
| 228 |
-
gr.update(
|
| 229 |
-
value=pd.DataFrame(
|
| 230 |
-
columns=[
|
| 231 |
-
"Index",
|
| 232 |
-
"Topic",
|
| 233 |
-
"Card_Type",
|
| 234 |
-
"Question",
|
| 235 |
-
"Answer",
|
| 236 |
-
"Explanation",
|
| 237 |
-
"Example",
|
| 238 |
-
"Prerequisites",
|
| 239 |
-
"Learning_Outcomes",
|
| 240 |
-
"Difficulty",
|
| 241 |
-
]
|
| 242 |
-
)
|
| 243 |
-
), # 14 output (DataFrame) - Clear it
|
| 244 |
-
gr.update(
|
| 245 |
-
value=subjects_df
|
| 246 |
-
), # 15 subjects_list (DataFrame) - Keep the value that triggered this
|
| 247 |
-
gr.update(
|
| 248 |
-
value=""
|
| 249 |
-
), # 16 learning_order (Markdown) - Clear it or decide to keep
|
| 250 |
-
gr.update(value=""), # 17 projects (Markdown) - Clear it or decide to keep
|
| 251 |
-
gr.update(visible=False), # 18 total_cards_html (HTML)
|
| 252 |
)
|
| 253 |
|
| 254 |
|
|
|
|
| 1 |
# Module for functions that build or manage UI sections/logic
|
| 2 |
|
| 3 |
import gradio as gr
|
| 4 |
+
import pandas as pd
|
| 5 |
from typing import (
|
| 6 |
Callable,
|
| 7 |
List,
|
|
|
|
| 51 |
def update_mode_visibility(
|
| 52 |
mode: str,
|
| 53 |
current_subject: str,
|
|
|
|
| 54 |
current_text: str,
|
| 55 |
current_url: str,
|
| 56 |
):
|
| 57 |
"""Updates visibility and values of UI elements based on generation mode."""
|
| 58 |
is_subject = mode == "subject"
|
|
|
|
| 59 |
is_text = mode == "text"
|
| 60 |
is_web = mode == "web"
|
| 61 |
|
| 62 |
# Determine value persistence or clearing
|
| 63 |
subject_val = current_subject if is_subject else ""
|
|
|
|
| 64 |
text_val = current_text if is_text else ""
|
| 65 |
url_val = current_url if is_web else ""
|
| 66 |
|
|
|
|
|
|
|
| 67 |
# Define standard columns for empty DataFrames
|
| 68 |
main_output_df_columns = [
|
| 69 |
"Index",
|
|
|
|
| 77 |
"Learning_Outcomes",
|
| 78 |
"Difficulty",
|
| 79 |
]
|
|
|
|
| 80 |
|
| 81 |
return (
|
| 82 |
gr.update(visible=is_subject), # 1 subject_mode (Group)
|
| 83 |
+
gr.update(visible=is_text), # 2 text_mode (Group)
|
| 84 |
+
gr.update(visible=is_web), # 3 web_mode (Group for crawler UI)
|
| 85 |
+
gr.update(visible=True), # 4 cards_output (always visible now)
|
| 86 |
+
gr.update(value=subject_val), # 5 subject
|
| 87 |
+
gr.update(value=text_val), # 6 source_text
|
| 88 |
+
gr.update(value=url_val), # 7 web_crawl_url_input
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
gr.update(
|
| 90 |
value=pd.DataFrame(columns=main_output_df_columns)
|
| 91 |
+
), # 8 output (DataFrame)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
gr.update(
|
| 93 |
value="<div><b>Total Cards Generated:</b> <span id='total-cards-count'>0</span></div>",
|
| 94 |
visible=False,
|
| 95 |
+
), # 9 total_cards_html
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 96 |
)
|
| 97 |
|
| 98 |
|
app.py
CHANGED
|
@@ -15,7 +15,6 @@ from ankigen_core.exporters import (
|
|
| 15 |
export_dataframe_to_apkg,
|
| 16 |
export_dataframe_to_csv,
|
| 17 |
) # Anki models (BASIC_MODEL, CLOZE_MODEL) are internal to exporters
|
| 18 |
-
from ankigen_core.learning_path import analyze_learning_path
|
| 19 |
from ankigen_core.llm_interface import (
|
| 20 |
OpenAIClientManager,
|
| 21 |
) # structured_output_completion is internal to core modules
|
|
@@ -23,7 +22,6 @@ from ankigen_core.ui_logic import (
|
|
| 23 |
crawl_and_generate,
|
| 24 |
create_crawler_main_mode_elements,
|
| 25 |
update_mode_visibility,
|
| 26 |
-
use_selected_subjects,
|
| 27 |
)
|
| 28 |
from ankigen_core.utils import (
|
| 29 |
ResponseCache,
|
|
@@ -68,6 +66,16 @@ except (AttributeError, ImportError):
|
|
| 68 |
# Fallback for older gradio versions or when themes are not available
|
| 69 |
custom_theme = None
|
| 70 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
# --- Example Data for Initialization ---
|
| 72 |
example_data = pd.DataFrame(
|
| 73 |
[
|
|
@@ -139,48 +147,8 @@ def get_recent_logs(logger_name="ankigen") -> str:
|
|
| 139 |
|
| 140 |
def create_ankigen_interface():
|
| 141 |
logger.info("Creating AnkiGen Gradio interface...")
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
title="AnkiGen",
|
| 145 |
-
css="""
|
| 146 |
-
#footer {display:none !important}
|
| 147 |
-
.tall-dataframe {min-height: 500px !important}
|
| 148 |
-
.contain {max-width: 100% !important; margin: auto;}
|
| 149 |
-
.output-cards {border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);}
|
| 150 |
-
.hint-text {font-size: 0.9em; color: #666; margin-top: 4px;}
|
| 151 |
-
.export-group > .gradio-group { margin-bottom: 0 !important; padding-bottom: 5px !important; }
|
| 152 |
-
|
| 153 |
-
/* REMOVING CSS previously intended for DataFrame readability to ensure plain text */
|
| 154 |
-
/*
|
| 155 |
-
.explanation-text {
|
| 156 |
-
background: #f0fdf4;
|
| 157 |
-
border-left: 3px solid #4ade80;
|
| 158 |
-
padding: 0.5em;
|
| 159 |
-
margin-bottom: 0.5em;
|
| 160 |
-
border-radius: 4px;
|
| 161 |
-
}
|
| 162 |
-
.example-text-plain {
|
| 163 |
-
background: #fff7ed;
|
| 164 |
-
border-left: 3px solid #f97316;
|
| 165 |
-
padding: 0.5em;
|
| 166 |
-
margin-bottom: 0.5em;
|
| 167 |
-
border-radius: 4px;
|
| 168 |
-
}
|
| 169 |
-
pre code {
|
| 170 |
-
display: block;
|
| 171 |
-
padding: 0.8em;
|
| 172 |
-
background: #1e293b;
|
| 173 |
-
color: #e2e8f0;
|
| 174 |
-
border-radius: 4px;
|
| 175 |
-
overflow-x: auto;
|
| 176 |
-
font-family: 'Fira Code', 'Consolas', monospace;
|
| 177 |
-
font-size: 0.9em;
|
| 178 |
-
margin-bottom: 0.5em;
|
| 179 |
-
}
|
| 180 |
-
*/
|
| 181 |
-
""",
|
| 182 |
-
js=js_storage,
|
| 183 |
-
) as ankigen:
|
| 184 |
with gr.Column(elem_classes="contain"):
|
| 185 |
gr.Markdown("# 📚 AnkiGen - Anki Card Generator")
|
| 186 |
gr.Markdown("#### Generate Anki flashcards using AI.")
|
|
@@ -208,16 +176,6 @@ def create_ankigen_interface():
|
|
| 208 |
"Auto-fill",
|
| 209 |
variant="secondary",
|
| 210 |
)
|
| 211 |
-
with gr.Group(visible=False) as path_mode:
|
| 212 |
-
description = gr.Textbox(
|
| 213 |
-
label="Learning Goal",
|
| 214 |
-
placeholder="Paste a job description...",
|
| 215 |
-
lines=5,
|
| 216 |
-
)
|
| 217 |
-
analyze_button = gr.Button(
|
| 218 |
-
"Analyze & Break Down",
|
| 219 |
-
variant="secondary",
|
| 220 |
-
)
|
| 221 |
with gr.Group(visible=False) as text_mode:
|
| 222 |
source_text = gr.Textbox(
|
| 223 |
label="Source Text",
|
|
@@ -329,21 +287,6 @@ def create_ankigen_interface():
|
|
| 329 |
|
| 330 |
generate_button = gr.Button("Generate Cards", variant="primary")
|
| 331 |
|
| 332 |
-
with gr.Group(visible=False) as path_results:
|
| 333 |
-
gr.Markdown("### Learning Path Analysis")
|
| 334 |
-
subjects_list = gr.Dataframe(
|
| 335 |
-
headers=["Subject", "Prerequisites", "Time Estimate"],
|
| 336 |
-
label="Recommended Subjects",
|
| 337 |
-
interactive=False,
|
| 338 |
-
)
|
| 339 |
-
learning_order = gr.Markdown("### Recommended Learning Order")
|
| 340 |
-
projects = gr.Markdown("### Suggested Projects")
|
| 341 |
-
use_subjects = gr.Button("Use These Subjects ℹ️", variant="primary")
|
| 342 |
-
gr.Markdown(
|
| 343 |
-
"*Click to copy subjects to main input*",
|
| 344 |
-
elem_classes="hint-text",
|
| 345 |
-
)
|
| 346 |
-
|
| 347 |
with gr.Group() as cards_output:
|
| 348 |
gr.Markdown("### Generated Cards")
|
| 349 |
with gr.Accordion("Output Format", open=False):
|
|
@@ -421,93 +364,18 @@ def create_ankigen_interface():
|
|
| 421 |
inputs=[
|
| 422 |
generation_mode,
|
| 423 |
subject,
|
| 424 |
-
description,
|
| 425 |
source_text,
|
| 426 |
web_crawl_url_input,
|
| 427 |
],
|
| 428 |
outputs=[
|
| 429 |
subject_mode,
|
| 430 |
-
path_mode,
|
| 431 |
-
text_mode,
|
| 432 |
-
web_mode,
|
| 433 |
-
path_results,
|
| 434 |
-
cards_output,
|
| 435 |
-
subject,
|
| 436 |
-
description,
|
| 437 |
-
source_text,
|
| 438 |
-
web_crawl_url_input,
|
| 439 |
-
output,
|
| 440 |
-
subjects_list,
|
| 441 |
-
learning_order,
|
| 442 |
-
projects,
|
| 443 |
-
total_cards_html,
|
| 444 |
-
],
|
| 445 |
-
)
|
| 446 |
-
|
| 447 |
-
# Define an async wrapper for the analyze_learning_path partial
|
| 448 |
-
async def handle_analyze_click(
|
| 449 |
-
api_key_val,
|
| 450 |
-
description_val,
|
| 451 |
-
model_choice_val,
|
| 452 |
-
progress=gr.Progress(track_tqdm=True), # Added progress tracker
|
| 453 |
-
):
|
| 454 |
-
try:
|
| 455 |
-
# Call analyze_learning_path directly, as client_manager and response_cache are in scope
|
| 456 |
-
return await analyze_learning_path(
|
| 457 |
-
client_manager, # from global scope
|
| 458 |
-
response_cache, # from global scope
|
| 459 |
-
api_key_val,
|
| 460 |
-
description_val,
|
| 461 |
-
model_choice_val,
|
| 462 |
-
)
|
| 463 |
-
except gr.Error as e: # Catch the specific Gradio error
|
| 464 |
-
logger.error(f"Learning path analysis failed: {e}", exc_info=True)
|
| 465 |
-
# Re-raise the error so Gradio displays it to the user
|
| 466 |
-
# And return appropriate empty updates for the outputs
|
| 467 |
-
# to prevent a subsequent Gradio error about mismatched return values.
|
| 468 |
-
gr.Error(str(e)) # This will be shown in the UI.
|
| 469 |
-
empty_subjects_df = pd.DataFrame(
|
| 470 |
-
columns=["Subject", "Prerequisites", "Time Estimate"],
|
| 471 |
-
)
|
| 472 |
-
return (
|
| 473 |
-
gr.update(
|
| 474 |
-
value=empty_subjects_df,
|
| 475 |
-
), # For subjects_list (DataFrame)
|
| 476 |
-
gr.update(value=""), # For learning_order (Markdown)
|
| 477 |
-
gr.update(value=""), # For projects (Markdown)
|
| 478 |
-
)
|
| 479 |
-
|
| 480 |
-
analyze_button.click(
|
| 481 |
-
fn=handle_analyze_click, # MODIFIED: Use the new async handler
|
| 482 |
-
inputs=[
|
| 483 |
-
api_key_input,
|
| 484 |
-
description,
|
| 485 |
-
model_choice,
|
| 486 |
-
],
|
| 487 |
-
outputs=[subjects_list, learning_order, projects],
|
| 488 |
-
)
|
| 489 |
-
|
| 490 |
-
use_subjects.click(
|
| 491 |
-
fn=use_selected_subjects,
|
| 492 |
-
inputs=[subjects_list],
|
| 493 |
-
outputs=[
|
| 494 |
-
generation_mode,
|
| 495 |
-
subject_mode,
|
| 496 |
-
path_mode,
|
| 497 |
text_mode,
|
| 498 |
web_mode,
|
| 499 |
-
path_results,
|
| 500 |
cards_output,
|
| 501 |
subject,
|
| 502 |
-
description,
|
| 503 |
source_text,
|
| 504 |
web_crawl_url_input,
|
| 505 |
-
topic_number,
|
| 506 |
-
preference_prompt,
|
| 507 |
output,
|
| 508 |
-
subjects_list,
|
| 509 |
-
learning_order,
|
| 510 |
-
projects,
|
| 511 |
total_cards_html,
|
| 512 |
],
|
| 513 |
)
|
|
@@ -920,13 +788,21 @@ if __name__ == "__main__":
|
|
| 920 |
logger.info("Launching AnkiGen Gradio interface...")
|
| 921 |
|
| 922 |
# Configure for HuggingFace Spaces vs local development
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 923 |
if os.environ.get("SPACE_ID"): # On HuggingFace Spaces
|
| 924 |
# Let HuggingFace handle all the configuration
|
| 925 |
-
ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch(
|
|
|
|
|
|
|
| 926 |
else: # Local development
|
| 927 |
# Use auto port finding for local dev
|
| 928 |
ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch(
|
| 929 |
-
server_name="127.0.0.1", share=False
|
| 930 |
)
|
| 931 |
except Exception as e:
|
| 932 |
logger.critical(f"Failed to launch Gradio interface: {e}", exc_info=True)
|
|
|
|
| 15 |
export_dataframe_to_apkg,
|
| 16 |
export_dataframe_to_csv,
|
| 17 |
) # Anki models (BASIC_MODEL, CLOZE_MODEL) are internal to exporters
|
|
|
|
| 18 |
from ankigen_core.llm_interface import (
|
| 19 |
OpenAIClientManager,
|
| 20 |
) # structured_output_completion is internal to core modules
|
|
|
|
| 22 |
crawl_and_generate,
|
| 23 |
create_crawler_main_mode_elements,
|
| 24 |
update_mode_visibility,
|
|
|
|
| 25 |
)
|
| 26 |
from ankigen_core.utils import (
|
| 27 |
ResponseCache,
|
|
|
|
| 66 |
# Fallback for older gradio versions or when themes are not available
|
| 67 |
custom_theme = None
|
| 68 |
|
| 69 |
+
# CSS for the interface (moved to module level for Gradio 6 compatibility)
|
| 70 |
+
custom_css = """
|
| 71 |
+
#footer {display:none !important}
|
| 72 |
+
.tall-dataframe {min-height: 500px !important}
|
| 73 |
+
.contain {max-width: 100% !important; margin: auto;}
|
| 74 |
+
.output-cards {border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);}
|
| 75 |
+
.hint-text {font-size: 0.9em; color: #666; margin-top: 4px;}
|
| 76 |
+
.export-group > .gradio-group { margin-bottom: 0 !important; padding-bottom: 5px !important; }
|
| 77 |
+
"""
|
| 78 |
+
|
| 79 |
# --- Example Data for Initialization ---
|
| 80 |
example_data = pd.DataFrame(
|
| 81 |
[
|
|
|
|
| 147 |
|
| 148 |
def create_ankigen_interface():
|
| 149 |
logger.info("Creating AnkiGen Gradio interface...")
|
| 150 |
+
# Note: theme, css, and js moved to .launch() for Gradio 6 compatibility
|
| 151 |
+
with gr.Blocks(title="AnkiGen") as ankigen:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
with gr.Column(elem_classes="contain"):
|
| 153 |
gr.Markdown("# 📚 AnkiGen - Anki Card Generator")
|
| 154 |
gr.Markdown("#### Generate Anki flashcards using AI.")
|
|
|
|
| 176 |
"Auto-fill",
|
| 177 |
variant="secondary",
|
| 178 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
with gr.Group(visible=False) as text_mode:
|
| 180 |
source_text = gr.Textbox(
|
| 181 |
label="Source Text",
|
|
|
|
| 287 |
|
| 288 |
generate_button = gr.Button("Generate Cards", variant="primary")
|
| 289 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
with gr.Group() as cards_output:
|
| 291 |
gr.Markdown("### Generated Cards")
|
| 292 |
with gr.Accordion("Output Format", open=False):
|
|
|
|
| 364 |
inputs=[
|
| 365 |
generation_mode,
|
| 366 |
subject,
|
|
|
|
| 367 |
source_text,
|
| 368 |
web_crawl_url_input,
|
| 369 |
],
|
| 370 |
outputs=[
|
| 371 |
subject_mode,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 372 |
text_mode,
|
| 373 |
web_mode,
|
|
|
|
| 374 |
cards_output,
|
| 375 |
subject,
|
|
|
|
| 376 |
source_text,
|
| 377 |
web_crawl_url_input,
|
|
|
|
|
|
|
| 378 |
output,
|
|
|
|
|
|
|
|
|
|
| 379 |
total_cards_html,
|
| 380 |
],
|
| 381 |
)
|
|
|
|
| 788 |
logger.info("Launching AnkiGen Gradio interface...")
|
| 789 |
|
| 790 |
# Configure for HuggingFace Spaces vs local development
|
| 791 |
+
# Note: theme, css, js moved to launch() for Gradio 6 compatibility
|
| 792 |
+
launch_kwargs = {
|
| 793 |
+
"theme": custom_theme,
|
| 794 |
+
"css": custom_css,
|
| 795 |
+
"js": js_storage,
|
| 796 |
+
}
|
| 797 |
if os.environ.get("SPACE_ID"): # On HuggingFace Spaces
|
| 798 |
# Let HuggingFace handle all the configuration
|
| 799 |
+
ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch(
|
| 800 |
+
**launch_kwargs
|
| 801 |
+
)
|
| 802 |
else: # Local development
|
| 803 |
# Use auto port finding for local dev
|
| 804 |
ankigen_interface.queue(default_concurrency_limit=2, max_size=10).launch(
|
| 805 |
+
server_name="127.0.0.1", share=False, **launch_kwargs
|
| 806 |
)
|
| 807 |
except Exception as e:
|
| 808 |
logger.critical(f"Failed to launch Gradio interface: {e}", exc_info=True)
|