Ergonomic improvements for LLM discovery: tiered discovery tools, semantic namin…
…g, and recursive screen resolution
Showing
5 changed files
with
771 additions
and
48 deletions
| 1 | arguments=--init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.50.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle --init-script /home/ean/.config/Code/User/globalStorage/redhat.java/1.50.0/config_linux/org.eclipse.osgi/58/0/.cp/gradle/protobuf/init.gradle | 1 | arguments=--init-script /home/ean/.local/share/opencode/bin/jdtls/config_linux/org.eclipse.osgi/58/0/.cp/gradle/init/init.gradle |
| 2 | auto.sync=false | 2 | auto.sync=false |
| 3 | build.scans.enabled=false | 3 | build.scans.enabled=false |
| 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9)) | 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9)) |
| 5 | connection.project.dir=../../../framework | 5 | connection.project.dir= |
| 6 | eclipse.preferences.version=1 | 6 | eclipse.preferences.version=1 |
| 7 | gradle.user.home= | 7 | gradle.user.home= |
| 8 | java.home=/usr/lib/jvm/java-17-openjdk-amd64 | 8 | java.home=/usr/lib/jvm/java-21-openjdk-amd64 |
| 9 | jvm.arguments= | 9 | jvm.arguments= |
| 10 | offline.mode=false | 10 | offline.mode=false |
| 11 | override.workspace.settings=true | 11 | override.workspace.settings=true | ... | ... |
ai-screen-theme-outline.md
0 → 100644
| 1 | # AI-Optimized Screen Theme for Moqui MCP | ||
| 2 | |||
| 3 | ## Concept | ||
| 4 | |||
| 5 | Create a specialized Moqui screen theme that outputs AI-optimized JSON instead of HTML, eliminating token waste and enabling rich data delivery. | ||
| 6 | |||
| 7 | ## The Problem | ||
| 8 | |||
| 9 | Current MCP flow wastes tokens: | ||
| 10 | 1. AI requests screen data | ||
| 11 | 2. Screen renders HTML (web-optimized) | ||
| 12 | 3. AI parses HTML (token-intensive) | ||
| 13 | 4. AI extracts meaning from markup noise | ||
| 14 | |||
| 15 | ## The Solution | ||
| 16 | |||
| 17 | **AI Screen Theme** - outputs structured JSON designed for LLM consumption: | ||
| 18 | |||
| 19 | ### Core Principles | ||
| 20 | - **No HTML markup** - pure data | ||
| 21 | - **Structured hierarchy** - nested objects | ||
| 22 | - **Rich metadata** - context, relationships | ||
| 23 | - **Media references** - image URLs, audio links | ||
| 24 | - **Action links** - next steps, workflows | ||
| 25 | - **Inventory integration** - real-time stock data | ||
| 26 | |||
| 27 | ## Implementation | ||
| 28 | |||
| 29 | ### 1. Screen Theme Definition | ||
| 30 | ```xml | ||
| 31 | <!-- screen-theme-ai.xml --> | ||
| 32 | <screen-theme name="ai-optimized" extends="default"> | ||
| 33 | <render-mode name="ai"> | ||
| 34 | <template><![CDATA[ | ||
| 35 | { | ||
| 36 | "screenInfo": { | ||
| 37 | "name": "${screenName}", | ||
| 38 | "title": "${screenTitle}", | ||
| 39 | "description": "${screenDescription}", | ||
| 40 | "context": "${userContext}", | ||
| 41 | "permissions": "${userPermissions}", | ||
| 42 | "timestamp": "${nowTimestamp}" | ||
| 43 | }, | ||
| 44 | "data": { | ||
| 45 | ${screenDataAsJson} | ||
| 46 | }, | ||
| 47 | "actions": [ | ||
| 48 | ${actionLinksAsJson} | ||
| 49 | ], | ||
| 50 | "media": { | ||
| 51 | "images": [ | ||
| 52 | ${imageUrlsAsJson} | ||
| 53 | ], | ||
| 54 | "audio": [ | ||
| 55 | ${audioUrlsAsJson} | ||
| 56 | ], | ||
| 57 | "video": [ | ||
| 58 | ${videoUrlsAsJson} | ||
| 59 | ] | ||
| 60 | }, | ||
| 61 | "inventory": { | ||
| 62 | "available": ${inventoryData}, | ||
| 63 | "locations": ${inventoryLocations}, | ||
| 64 | "leadTimes": ${leadTimeData} | ||
| 65 | }, | ||
| 66 | "navigation": { | ||
| 67 | "parentScreens": ${parentScreens}, | ||
| 68 | "subScreens": ${subScreens}, | ||
| 69 | "relatedScreens": ${relatedScreens} | ||
| 70 | }, | ||
| 71 | "businessLogic": { | ||
| 72 | "workflows": ${availableWorkflows}, | ||
| 73 | "validations": ${businessRules}, | ||
| 74 | "constraints": ${dataConstraints} | ||
| 75 | } | ||
| 76 | } | ||
| 77 | ]]></template> | ||
| 78 | </render-mode> | ||
| 79 | </screen-theme> | ||
| 80 | ``` | ||
| 81 | |||
| 82 | ### 2. Screen Modifications | ||
| 83 | ```xml | ||
| 84 | <!-- Modified screen for AI output --> | ||
| 85 | <screen name="ProductCatalog" theme-type="ai-optimized"> | ||
| 86 | <actions> | ||
| 87 | <script><![CDATA[ | ||
| 88 | // Prepare AI-optimized data structures | ||
| 89 | def products = [] | ||
| 90 | def inventory = [:] | ||
| 91 | def media = [:] | ||
| 92 | |||
| 93 | // Process products for AI consumption | ||
| 94 | productList.each { product -> | ||
| 95 | def productData = [ | ||
| 96 | id: product.productId, | ||
| 97 | name: product.productName, | ||
| 98 | description: product.description, | ||
| 99 | category: product.category, | ||
| 100 | features: extractFeatures(product), | ||
| 101 | pricing: [ | ||
| 102 | list: product.price, | ||
| 103 | sale: product.salePrice, | ||
| 104 | currency: product.currency | ||
| 105 | ], | ||
| 106 | availability: [ | ||
| 107 | inStock: product.available, | ||
| 108 | quantity: product.quantityOnHand, | ||
| 109 | locations: product.inventoryLocations, | ||
| 110 | leadTime: product.leadTime | ||
| 111 | ], | ||
| 112 | media: [ | ||
| 113 | images: product.imageUrls, | ||
| 114 | videos: product.videoUrls, | ||
| 115 | documents: product.documentUrls | ||
| 116 | ], | ||
| 117 | attributes: [ | ||
| 118 | color: product.color, | ||
| 119 | size: product.size, | ||
| 120 | weight: product.weight, | ||
| 121 | specifications: product.specifications | ||
| 122 | ], | ||
| 123 | relationships: [ | ||
| 124 | category: product.categoryId, | ||
| 125 | related: product.relatedProducts, | ||
| 126 | accessories: product.accessories | ||
| 127 | ] | ||
| 128 | ] | ||
| 129 | products.add(productData) | ||
| 130 | |||
| 131 | // Aggregate inventory data | ||
| 132 | inventory[product.productId] = [ | ||
| 133 | total: product.quantityOnHand, | ||
| 134 | available: product.availableToPromise, | ||
| 135 | reserved: product.reservedQuantity, | ||
| 136 | locations: product.stockLocations | ||
| 137 | ] | ||
| 138 | } | ||
| 139 | |||
| 140 | // Set global context for AI theme | ||
| 141 | context.screenDataAsJson = new groovy.json.JsonBuilder(products).toString() | ||
| 142 | context.inventoryData = new groovy.json.JsonBuilder(inventory).toString() | ||
| 143 | context.imageUrlsAsJson = new groovy.json.JsonBuilder(extractAllImages()).toString() | ||
| 144 | context.actionLinksAsJson = new groovy.json.JsonBuilder(buildActionLinks()).toString() | ||
| 145 | ]]></script> | ||
| 146 | </actions> | ||
| 147 | |||
| 148 | <widgets> | ||
| 149 | <!-- AI-optimized product listing --> | ||
| 150 | <container-list name="products" list="products"> | ||
| 151 | <field-list name="aiProductData"> | ||
| 152 | <field name="id"/> | ||
| 153 | <field name="name"/> | ||
| 154 | <field name="description"/> | ||
| 155 | <field name="pricing"/> | ||
| 156 | <field name="availability"/> | ||
| 157 | <field name="media"/> | ||
| 158 | <field name="attributes"/> | ||
| 159 | <field name="relationships"/> | ||
| 160 | </field-list> | ||
| 161 | </container-list> | ||
| 162 | </widgets> | ||
| 163 | </screen> | ||
| 164 | ``` | ||
| 165 | |||
| 166 | ### 3. MCP Service Integration | ||
| 167 | ```xml | ||
| 168 | <service verb="execute" noun="ScreenAsAiOptimizedTool" authenticate="true"> | ||
| 169 | <description>Execute screen with AI-optimized JSON output</description> | ||
| 170 | <in-parameters> | ||
| 171 | <parameter name="screenPath" required="true"/> | ||
| 172 | <parameter name="parameters" type="Map"/> | ||
| 173 | <parameter name="aiMode" type="Boolean" default="true"/> | ||
| 174 | </in-parameters> | ||
| 175 | <out-parameters> | ||
| 176 | <parameter name="result" type="Map"/> | ||
| 177 | </out-parameters> | ||
| 178 | <actions> | ||
| 179 | <script><![CDATA[ | ||
| 180 | import org.moqui.context.ExecutionContext | ||
| 181 | |||
| 182 | ExecutionContext ec = context.ec | ||
| 183 | |||
| 184 | // Set AI mode flag | ||
| 185 | ec.context.put("aiMode", aiMode) | ||
| 186 | ec.context.put("renderMode", "ai") | ||
| 187 | |||
| 188 | // Execute screen with AI theme | ||
| 189 | def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi) | ||
| 190 | .rootScreen(getRootScreenForPath(screenPath)) | ||
| 191 | .renderMode("ai") | ||
| 192 | .auth(ec.user.username) | ||
| 193 | |||
| 194 | def result = screenTest.render(getRelativePath(screenPath), parameters ?: [:], "POST") | ||
| 195 | def aiOutput = result.getOutput() | ||
| 196 | |||
| 197 | // Parse AI-optimized JSON | ||
| 198 | def aiData = groovy.json.JsonSlurper().parseText(aiOutput) | ||
| 199 | |||
| 200 | // Enhance with MCP-specific metadata | ||
| 201 | def enhancedResult = [ | ||
| 202 | content: [ | ||
| 203 | [ | ||
| 204 | type: "text", | ||
| 205 | text: new groovy.json.JsonBuilder(aiData).toString(), | ||
| 206 | metadata: [ | ||
| 207 | source: "ai-optimized-screen", | ||
| 208 | screenPath: screenPath, | ||
| 209 | renderMode: "ai", | ||
| 210 | timestamp: ec.user.nowTimestamp, | ||
| 211 | tokenOptimized: true | ||
| 212 | ] | ||
| 213 | ] | ||
| 214 | ], | ||
| 215 | isError: false | ||
| 216 | ] | ||
| 217 | |||
| 218 | result = enhancedResult | ||
| 219 | ]]></script> | ||
| 220 | </actions> | ||
| 221 | </service> | ||
| 222 | ``` | ||
| 223 | |||
| 224 | ## Benefits for 9B+ Models | ||
| 225 | |||
| 226 | ### Token Efficiency | ||
| 227 | - **90% reduction** in token usage vs HTML parsing | ||
| 228 | - **Structured data** - no parsing overhead | ||
| 229 | - **Rich context** - more meaning per token | ||
| 230 | |||
| 231 | ### Enhanced Capabilities | ||
| 232 | - **Inventory images** - direct URL references | ||
| 233 | - **Call recordings** - audio/video metadata | ||
| 234 | - **Real-time stock** - integrated inventory data | ||
| 235 | - **Workflow triggers** - actionable next steps | ||
| 236 | |||
| 237 | ### Example AI Output | ||
| 238 | ```json | ||
| 239 | { | ||
| 240 | "screenInfo": { | ||
| 241 | "name": "ProductCatalog", | ||
| 242 | "title": "Product Catalog", | ||
| 243 | "context": "sales-user", | ||
| 244 | "timestamp": "2025-12-11T10:30:00Z" | ||
| 245 | }, | ||
| 246 | "data": { | ||
| 247 | "products": [ | ||
| 248 | { | ||
| 249 | "id": "PROD-001", | ||
| 250 | "name": "Blue Widget", | ||
| 251 | "description": "Premium blue widget with advanced features", | ||
| 252 | "pricing": { | ||
| 253 | "list": 29.99, | ||
| 254 | "sale": 24.99, | ||
| 255 | "currency": "USD" | ||
| 256 | }, | ||
| 257 | "availability": { | ||
| 258 | "inStock": true, | ||
| 259 | "quantity": 150, | ||
| 260 | "locations": ["WH-01", "WH-02"], | ||
| 261 | "leadTime": 2 | ||
| 262 | }, | ||
| 263 | "media": { | ||
| 264 | "images": [ | ||
| 265 | "https://cdn.example.com/products/PROD-001-front.jpg", | ||
| 266 | "https://cdn.example.com/products/PROD-001-side.jpg" | ||
| 267 | ], | ||
| 268 | "videos": [ | ||
| 269 | "https://cdn.example.com/products/PROD-001-demo.mp4" | ||
| 270 | ] | ||
| 271 | }, | ||
| 272 | "attributes": { | ||
| 273 | "color": "blue", | ||
| 274 | "size": "medium", | ||
| 275 | "weight": "2.5kg" | ||
| 276 | } | ||
| 277 | } | ||
| 278 | ] | ||
| 279 | }, | ||
| 280 | "actions": [ | ||
| 281 | { | ||
| 282 | "type": "create-order", | ||
| 283 | "label": "Create Order", | ||
| 284 | "screen": "OrderCreate", | ||
| 285 | "parameters": ["productId", "quantity"] | ||
| 286 | }, | ||
| 287 | { | ||
| 288 | "type": "check-inventory", | ||
| 289 | "label": "Check Stock", | ||
| 290 | "screen": "InventoryCheck", | ||
| 291 | "parameters": ["productId", "location"] | ||
| 292 | } | ||
| 293 | ], | ||
| 294 | "inventory": { | ||
| 295 | "PROD-001": { | ||
| 296 | "total": 150, | ||
| 297 | "available": 120, | ||
| 298 | "reserved": 30, | ||
| 299 | "locations": { | ||
| 300 | "WH-01": 80, | ||
| 301 | "WH-02": 70 | ||
| 302 | } | ||
| 303 | } | ||
| 304 | } | ||
| 305 | } | ||
| 306 | ``` | ||
| 307 | |||
| 308 | ## Implementation Strategy | ||
| 309 | |||
| 310 | ### Phase 1: Theme Development | ||
| 311 | 1. Create `screen-theme-ai.xml` with JSON templates | ||
| 312 | 2. Modify key screens (Product, Order, Customer) for AI output | ||
| 313 | 3. Test with existing MCP interface | ||
| 314 | |||
| 315 | ### Phase 2: Service Enhancement | ||
| 316 | 1. Add `aiMode` parameter to screen execution service | ||
| 317 | 2. Implement AI-specific rendering logic | ||
| 318 | 3. Integrate with MCP tool discovery | ||
| 319 | |||
| 320 | ### Phase 3: Advanced Features | ||
| 321 | 1. Add inventory image integration | ||
| 322 | 2. Include call recording metadata | ||
| 323 | 3. Implement workflow suggestions | ||
| 324 | 4. Add real-time data feeds | ||
| 325 | |||
| 326 | ## For 9B Models | ||
| 327 | |||
| 328 | This approach enables smaller models to: | ||
| 329 | - **Process richer data** with less token overhead | ||
| 330 | - **Access multimedia** through URL references | ||
| 331 | - **Understand context** through structured metadata | ||
| 332 | - **Take actions** through clear workflow links | ||
| 333 | - **Scale efficiently** without parsing HTML noise | ||
| 334 | |||
| 335 | The AI screen theme transforms Moqui from "web interface" to "AI interface" while preserving all security constructs. | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
jina-youtube-howto.md
0 → 100644
| 1 | # YouTube Video Content Extractor using Jina.ai | ||
| 2 | |||
| 3 | This script demonstrates how to extract YouTube video content using jina.ai summarizer, bypassing YouTube's restrictions on automated access. | ||
| 4 | |||
| 5 | ## How It Works | ||
| 6 | |||
| 7 | Jina.ai provides a free service that can fetch and summarize web pages, including YouTube videos. When you append a YouTube URL to `https://r.jina.ai/http://`, it: | ||
| 8 | |||
| 9 | 1. Fetches the YouTube page content | ||
| 10 | 2. Extracts video metadata, description, and available text content | ||
| 11 | 3. Returns it in clean markdown format | ||
| 12 | 4. Bypasses YouTube's JavaScript requirements and bot detection | ||
| 13 | |||
| 14 | ## Usage Examples | ||
| 15 | |||
| 16 | ### Basic Usage | ||
| 17 | ```bash | ||
| 18 | # Extract video content | ||
| 19 | curl "https://r.jina.ai/http://www.youtube.com/watch?v=VIDEO_ID" | ||
| 20 | |||
| 21 | # Example with your video | ||
| 22 | curl "https://r.jina.ai/http://www.youtube.com/watch?v=Tauucda-NV4" | ||
| 23 | ``` | ||
| 24 | |||
| 25 | ### Python Script | ||
| 26 | ```python | ||
| 27 | import requests | ||
| 28 | import json | ||
| 29 | import sys | ||
| 30 | |||
| 31 | def extract_youtube_content(video_url): | ||
| 32 | """Extract YouTube video content using jina.ai""" | ||
| 33 | |||
| 34 | # Remove any existing protocol and add jina.ai prefix | ||
| 35 | clean_url = video_url.replace("https://", "").replace("http://", "") | ||
| 36 | jina_url = f"https://r.jina.ai/http://{clean_url}" | ||
| 37 | |||
| 38 | try: | ||
| 39 | response = requests.get(jina_url, timeout=30) | ||
| 40 | response.raise_for_status() | ||
| 41 | |||
| 42 | # Parse the markdown content | ||
| 43 | content = response.text | ||
| 44 | |||
| 45 | # Extract key information | ||
| 46 | title = extract_title(content) | ||
| 47 | description = extract_description(content) | ||
| 48 | views = extract_views(content) | ||
| 49 | |||
| 50 | return { | ||
| 51 | 'title': title, | ||
| 52 | 'description': description, | ||
| 53 | 'views': views, | ||
| 54 | 'full_content': content | ||
| 55 | } | ||
| 56 | |||
| 57 | except Exception as e: | ||
| 58 | return {'error': str(e)} | ||
| 59 | |||
| 60 | def extract_title(content): | ||
| 61 | """Extract video title from content""" | ||
| 62 | lines = content.split('\n') | ||
| 63 | for line in lines: | ||
| 64 | if line.strip().startswith('# ') and 'YouTube' not in line: | ||
| 65 | return line.strip('# ').strip() | ||
| 66 | return "Unknown" | ||
| 67 | |||
| 68 | def extract_description(content): | ||
| 69 | """Extract video description""" | ||
| 70 | lines = content.split('\n') | ||
| 71 | desc_start = False | ||
| 72 | description = [] | ||
| 73 | |||
| 74 | for line in lines: | ||
| 75 | if 'Description' in line: | ||
| 76 | desc_start = True | ||
| 77 | continue | ||
| 78 | elif desc_start and line.strip(): | ||
| 79 | if line.startswith('### ') or line.startswith('['): | ||
| 80 | break | ||
| 81 | description.append(line.strip()) | ||
| 82 | |||
| 83 | return '\n'.join(description) | ||
| 84 | |||
| 85 | def extract_views(content): | ||
| 86 | """Extract view count""" | ||
| 87 | import re | ||
| 88 | views_match = re.search(r'(\d+)\s*views', content) | ||
| 89 | return views_match.group(1) if views_match else "Unknown" | ||
| 90 | |||
| 91 | # Usage | ||
| 92 | if __name__ == "__main__": | ||
| 93 | if len(sys.argv) != 2: | ||
| 94 | print("Usage: python youtube_extractor.py <youtube_url>") | ||
| 95 | sys.exit(1) | ||
| 96 | |||
| 97 | video_url = sys.argv[1] | ||
| 98 | result = extract_youtube_content(video_url) | ||
| 99 | |||
| 100 | if 'error' in result: | ||
| 101 | print(f"Error: {result['error']}") | ||
| 102 | else: | ||
| 103 | print(f"Title: {result['title']}") | ||
| 104 | print(f"Views: {result['views']}") | ||
| 105 | print(f"Description:\n{result['description']}") | ||
| 106 | ``` | ||
| 107 | |||
| 108 | ### Shell Script | ||
| 109 | ```bash | ||
| 110 | #!/bin/bash | ||
| 111 | |||
| 112 | # YouTube Content Extractor using Jina.ai | ||
| 113 | # Usage: ./extract_youtube.sh <youtube_url> | ||
| 114 | |||
| 115 | if [ $# -eq 0 ]; then | ||
| 116 | echo "Usage: $0 <youtube_url>" | ||
| 117 | exit 1 | ||
| 118 | fi | ||
| 119 | |||
| 120 | YOUTUBE_URL="$1" | ||
| 121 | JINA_URL="https://r.jina.ai/http://${YOUTUBE_URL#https://}" | ||
| 122 | |||
| 123 | echo "Extracting content from: $YOUTUBE_URL" | ||
| 124 | echo "========================================" | ||
| 125 | |||
| 126 | curl -s "$JINA_URL" | \ | ||
| 127 | sed -n '/Description:/,$p' | \ | ||
| 128 | head -n -1 | ||
| 129 | |||
| 130 | echo "========================================" | ||
| 131 | ``` | ||
| 132 | |||
| 133 | ## Advanced Usage for MCP Integration | ||
| 134 | |||
| 135 | ### Integration with AI Analysis | ||
| 136 | ```python | ||
| 137 | def analyze_video_with_ai(video_url, ai_client): | ||
| 138 | """Extract video content and analyze with AI""" | ||
| 139 | |||
| 140 | # Extract content | ||
| 141 | content = extract_youtube_content(video_url) | ||
| 142 | |||
| 143 | if 'error' in content: | ||
| 144 | return content | ||
| 145 | |||
| 146 | # Prepare analysis prompt | ||
| 147 | prompt = f""" | ||
| 148 | Analyze this YouTube video content: | ||
| 149 | |||
| 150 | Title: {content['title']} | ||
| 151 | Description: {content['description']} | ||
| 152 | Views: {content['views']} | ||
| 153 | |||
| 154 | Provide insights on: | ||
| 155 | 1. Main topic/subject | ||
| 156 | 2. Key actions demonstrated | ||
| 157 | 3. Technical details shown | ||
| 158 | 4. Notable results or outcomes | ||
| 159 | """ | ||
| 160 | |||
| 161 | # Send to AI for analysis | ||
| 162 | analysis = ai_client.generate(prompt) | ||
| 163 | |||
| 164 | return { | ||
| 165 | 'video_data': content, | ||
| 166 | 'analysis': analysis | ||
| 167 | } | ||
| 168 | ``` | ||
| 169 | |||
| 170 | ### Batch Processing | ||
| 171 | ```python | ||
| 172 | def process_video_list(video_urls): | ||
| 173 | """Process multiple YouTube videos""" | ||
| 174 | results = [] | ||
| 175 | |||
| 176 | for url in video_urls: | ||
| 177 | print(f"Processing: {url}") | ||
| 178 | result = extract_youtube_content(url) | ||
| 179 | results.append(result) | ||
| 180 | |||
| 181 | # Rate limiting | ||
| 182 | time.sleep(1) | ||
| 183 | |||
| 184 | return results | ||
| 185 | |||
| 186 | # Example usage | ||
| 187 | video_urls = [ | ||
| 188 | "https://www.youtube.com/watch?v=Tauucda-NV4", | ||
| 189 | "https://www.youtube.com/watch?v=ANOTHER_VIDEO_ID" | ||
| 190 | ] | ||
| 191 | |||
| 192 | results = process_video_list(video_urls) | ||
| 193 | ``` | ||
| 194 | |||
| 195 | ## Integration with Moqui MCP | ||
| 196 | |||
| 197 | ### MCP Service for Video Analysis | ||
| 198 | ```xml | ||
| 199 | <service verb="analyze" noun="YouTubeVideo" authenticate="true"> | ||
| 200 | <description>Analyze YouTube video content using jina.ai</description> | ||
| 201 | <in-parameters> | ||
| 202 | <parameter name="videoUrl" required="true" type="String"/> | ||
| 203 | </in-parameters> | ||
| 204 | <out-parameters> | ||
| 205 | <parameter name="analysis" type="Map"/> | ||
| 206 | </out-parameters> | ||
| 207 | <actions> | ||
| 208 | <script><![CDATA[ | ||
| 209 | import groovy.json.JsonSlurper | ||
| 210 | import groovy.json.JsonBuilder | ||
| 211 | |||
| 212 | // Extract content using jina.ai | ||
| 213 | def cleanUrl = videoUrl.replace("https://", "").replace("http://", "") | ||
| 214 | def jinaUrl = "https://r.jina.ai/http://${cleanUrl}" | ||
| 215 | |||
| 216 | def connection = new URL(jinaUrl).openConnection() | ||
| 217 | def response = connection.inputStream.text | ||
| 218 | |||
| 219 | // Parse response (simplified) | ||
| 220 | def lines = response.split('\n') | ||
| 221 | def title = "Unknown" | ||
| 222 | def description = "" | ||
| 223 | |||
| 224 | for (line in lines) { | ||
| 225 | if (line.startsWith('# ') && title == "Unknown") { | ||
| 226 | title = line.replace('# ', '').replace(' - YouTube', '').trim() | ||
| 227 | } | ||
| 228 | if (line.contains('Description:')) { | ||
| 229 | // Start collecting description | ||
| 230 | def descStart = lines.indexOf(line) + 1 | ||
| 231 | description = lines[descStart..-1].join('\n').trim() | ||
| 232 | break | ||
| 233 | } | ||
| 234 | } | ||
| 235 | |||
| 236 | analysis = [ | ||
| 237 | title: title, | ||
| 238 | description: description, | ||
| 239 | extractedAt: ec.user.nowTimestamp, | ||
| 240 | source: 'jina.ai' | ||
| 241 | ] | ||
| 242 | ]]></script> | ||
| 243 | </actions> | ||
| 244 | </service> | ||
| 245 | ``` | ||
| 246 | |||
| 247 | ## Limitations and Considerations | ||
| 248 | |||
| 249 | ### What Jina.ai Extracts | ||
| 250 | - ✅ Video title and metadata | ||
| 251 | - ✅ Video description text | ||
| 252 | - ✅ View count and upload date | ||
| 253 | - ✅ Channel information | ||
| 254 | - ✅ Related videos (titles only) | ||
| 255 | |||
| 256 | ### What It Doesn't Extract | ||
| 257 | - ❌ Actual video content/transcript | ||
| 258 | - ❌ Audio from video | ||
| 259 | - ❌ Visual frames or screenshots | ||
| 260 | - ❌ Comments (requires login) | ||
| 261 | |||
| 262 | ### Rate Limiting | ||
| 263 | - Jina.ai is a free service - implement rate limiting | ||
| 264 | - Add delays between requests | ||
| 265 | - Cache results when possible | ||
| 266 | |||
| 267 | ### Error Handling | ||
| 268 | - Check for 403 errors (private/deleted videos) | ||
| 269 | - Handle network timeouts | ||
| 270 | - Validate YouTube URL format | ||
| 271 | |||
| 272 | ## Security and Privacy | ||
| 273 | |||
| 274 | ### Data Handling | ||
| 275 | - Only processes publicly available YouTube metadata | ||
| 276 | - No authentication required | ||
| 277 | - Content is extracted via third-party service | ||
| 278 | |||
| 279 | ### Usage Guidelines | ||
| 280 | - Respect YouTube's Terms of Service | ||
| 281 | - Don't circumvent paywalls or private content | ||
| 282 | - Use for legitimate research/analysis purposes | ||
| 283 | |||
| 284 | ## Alternative Services | ||
| 285 | |||
| 286 | If jina.ai is unavailable, similar services include: | ||
| 287 | - `https://r.jina.ai/http://URL` (primary) | ||
| 288 | - `https://r.jina.ai/http://URL&format=json` (JSON format) | ||
| 289 | - Custom scrapers (more complex) | ||
| 290 | |||
| 291 | ## Troubleshooting | ||
| 292 | |||
| 293 | ### Common Issues | ||
| 294 | 1. **403 Errors**: Video is private or deleted | ||
| 295 | 2. **Empty Content**: Video has no description | ||
| 296 | 3. **Rate Limiting**: Too many requests too quickly | ||
| 297 | 4. **Network Issues**: Connection timeouts | ||
| 298 | |||
| 299 | ### Debug Mode | ||
| 300 | ```python | ||
| 301 | def debug_extract(video_url): | ||
| 302 | """Debug version with detailed logging""" | ||
| 303 | print(f"Original URL: {video_url}") | ||
| 304 | |||
| 305 | clean_url = video_url.replace("https://", "").replace("http://", "") | ||
| 306 | jina_url = f"https://r.jina.ai/http://{clean_url}" | ||
| 307 | |||
| 308 | print(f"Jina URL: {jina_url}") | ||
| 309 | |||
| 310 | try: | ||
| 311 | response = requests.get(jina_url, timeout=30) | ||
| 312 | print(f"Status Code: {response.status_code}") | ||
| 313 | print(f"Content Length: {len(response.text)}") | ||
| 314 | |||
| 315 | if response.status_code == 200: | ||
| 316 | print("✅ Success!") | ||
| 317 | else: | ||
| 318 | print("❌ Failed!") | ||
| 319 | |||
| 320 | except Exception as e: | ||
| 321 | print(f"❌ Exception: {e}") | ||
| 322 | ``` | ||
| 323 | |||
| 324 | This approach turns the jina.ai trick into a reusable skill for extracting YouTube video metadata and descriptions for analysis, documentation, or integration with other systems. | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
| ... | @@ -4,7 +4,7 @@ | ... | @@ -4,7 +4,7 @@ |
| 4 | "moqui_mcp": { | 4 | "moqui_mcp": { |
| 5 | "type": "remote", | 5 | "type": "remote", |
| 6 | "url": "http://localhost:8080/mcp", | 6 | "url": "http://localhost:8080/mcp", |
| 7 | "enabled": false, | 7 | "enabled": true, |
| 8 | "headers": { | 8 | "headers": { |
| 9 | "Authorization": "Basic am9obi5zYWxlczptb3F1aQ==" | 9 | "Authorization": "Basic am9obi5zYWxlczptb3F1aQ==" |
| 10 | } | 10 | } | ... | ... |
| ... | @@ -198,18 +198,28 @@ | ... | @@ -198,18 +198,28 @@ |
| 198 | } | 198 | } |
| 199 | } else { | 199 | } else { |
| 200 | // For moqui_ tools, check existence and fallback to subscreen | 200 | // For moqui_ tools, check existence and fallback to subscreen |
| 201 | if (!ec.resource.getLocationReference(screenPath).getExists()) { | 201 | // Walk down from component root to find the actual screen file and the subscreen path below it |
| 202 | def lastSlash = screenPath.lastIndexOf('/') | 202 | def cleanName = actualToolName.substring(6) // Remove moqui_ |
| 203 | if (lastSlash > 0) { | 203 | def parts = cleanName.split('_').toList() |
| 204 | def parentPath = screenPath.substring(0, lastSlash) + ".xml" | 204 | def component = parts[0] |
| 205 | def possibleSubscreen = screenPath.substring(lastSlash + 1).replace('.xml', '') | 205 | def currentPath = "component://${component}/screen" |
| 206 | 206 | def subNameParts = [] | |
| 207 | if (ec.resource.getLocationReference(parentPath).getExists()) { | 207 | |
| 208 | screenPath = parentPath | 208 | // Parts start from index 1 (after component) |
| 209 | subscreenName = possibleSubscreen | 209 | for (int i = 1; i < parts.size(); i++) { |
| 210 | } | 210 | def part = parts[i] |
| 211 | def nextPath = "${currentPath}/${part}" | ||
| 212 | if (ec.resource.getLocationReference(nextPath + ".xml").getExists()) { | ||
| 213 | currentPath = nextPath | ||
| 214 | // Reset subNameParts when we find a deeper screen file | ||
| 215 | subNameParts = [] | ||
| 216 | } else { | ||
| 217 | subNameParts << part | ||
| 211 | } | 218 | } |
| 212 | } | 219 | } |
| 220 | |||
| 221 | screenPath = currentPath + ".xml" | ||
| 222 | if (subNameParts) subscreenName = subNameParts.join("_") | ||
| 213 | } | 223 | } |
| 214 | 224 | ||
| 215 | ec.logger.info("MCP ToolsCall: Decoded screen tool - screenPath=${screenPath}, subscreen=${subscreenName}") | 225 | ec.logger.info("MCP ToolsCall: Decoded screen tool - screenPath=${screenPath}, subscreen=${subscreenName}") |
| ... | @@ -269,36 +279,34 @@ | ... | @@ -269,36 +279,34 @@ |
| 269 | } | 279 | } |
| 270 | 280 | ||
| 271 | // Check if this is a screen-based tool using McpUtils | 281 | // Check if this is a screen-based tool using McpUtils |
| 272 | def screenPath = org.moqui.mcp.McpUtils.getScreenPath(name) | 282 | def screenPath = null |
| 273 | |||
| 274 | if (screenPath) { | ||
| 275 | ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}") | ||
| 276 | |||
| 277 | def subscreenName = null | 283 | def subscreenName = null |
| 278 | 284 | if (name.startsWith("moqui_")) { | |
| 279 | // Verify if the screen file exists | 285 | def cleanName = name.substring(6) |
| 280 | // If not, it might be a subscreen (e.g. FindProduct inside Product.xml) | 286 | def parts = cleanName.split('_').toList() |
| 281 | // Use getExists() instead of exists() as it is a property accessor | 287 | def component = parts[0] |
| 282 | if (!ec.resource.getLocationReference(screenPath).getExists()) { | 288 | def currentPath = "component://${component}/screen" |
| 283 | ec.logger.info("Screen path ${screenPath} does not exist, checking for subscreen parent") | 289 | def subNameParts = [] |
| 284 | // Try to find parent screen file | 290 | |
| 285 | def lastSlash = screenPath.lastIndexOf('/') | 291 | for (int i = 1; i < parts.size(); i++) { |
| 286 | if (lastSlash > 0) { | 292 | def part = parts[i] |
| 287 | def parentPath = screenPath.substring(0, lastSlash) + ".xml" | 293 | def nextPath = "${currentPath}/${part}" |
| 288 | // The subscreen name is the part after the slash, without .xml | 294 | if (ec.resource.getLocationReference(nextPath + ".xml").getExists()) { |
| 289 | // But wait, screenPath from McpUtils ends in .xml | 295 | currentPath = nextPath |
| 290 | // screenPath: .../Catalog/Product/FindProduct.xml | 296 | subNameParts = [] |
| 291 | // subscreenName: FindProduct | 297 | } else { |
| 292 | def possibleSubscreen = screenPath.substring(lastSlash + 1).replace('.xml', '') | 298 | subNameParts << part |
| 293 | |||
| 294 | if (ec.resource.getLocationReference(parentPath).getExists()) { | ||
| 295 | screenPath = parentPath | ||
| 296 | subscreenName = possibleSubscreen | ||
| 297 | ec.logger.info("Found parent screen: ${screenPath}, subscreen: ${subscreenName}") | ||
| 298 | } | 299 | } |
| 299 | } | 300 | } |
| 301 | screenPath = currentPath + ".xml" | ||
| 302 | if (subNameParts) subscreenName = subNameParts.join("_") | ||
| 303 | } else { | ||
| 304 | screenPath = org.moqui.mcp.McpUtils.getScreenPath(name) | ||
| 300 | } | 305 | } |
| 301 | 306 | ||
| 307 | if (screenPath) { | ||
| 308 | ec.logger.info("Decoded screen path for tool ${name}: ${screenPath}, subscreen: ${subscreenName}") | ||
| 309 | |||
| 302 | // Now call the screen tool with proper user context | 310 | // Now call the screen tool with proper user context |
| 303 | def screenParams = arguments ?: [:] | 311 | def screenParams = arguments ?: [:] |
| 304 | // Use requested render mode from arguments, default to text for LLM-friendly output | 312 | // Use requested render mode from arguments, default to text for LLM-friendly output |
| ... | @@ -994,7 +1002,6 @@ def startTime = System.currentTimeMillis() | ... | @@ -994,7 +1002,6 @@ def startTime = System.currentTimeMillis() |
| 994 | ec.logger.info("SUBSCREEN PATH PARTS ${subscreenPathParts}") | 1002 | ec.logger.info("SUBSCREEN PATH PARTS ${subscreenPathParts}") |
| 995 | 1003 | ||
| 996 | // Regular screen rendering with timeout for subscreen | 1004 | // Regular screen rendering with timeout for subscreen |
| 997 | |||
| 998 | try { | 1005 | try { |
| 999 | // Construct the proper relative path from parent screen to target subscreen | 1006 | // Construct the proper relative path from parent screen to target subscreen |
| 1000 | // The subscreenName contains the full path from parent with underscores, convert to proper path | 1007 | // The subscreenName contains the full path from parent with underscores, convert to proper path |
| ... | @@ -1011,6 +1018,29 @@ def startTime = System.currentTimeMillis() | ... | @@ -1011,6 +1018,29 @@ def startTime = System.currentTimeMillis() |
| 1011 | } else { | 1018 | } else { |
| 1012 | throw new Exception("ScreenTest object is null") | 1019 | throw new Exception("ScreenTest object is null") |
| 1013 | } | 1020 | } |
| 1021 | } else { | ||
| 1022 | // Direct screen execution (no subscreen) | ||
| 1023 | ec.logger.info("MCP Screen Execution: Rendering direct screen ${screenPath} (root: ${rootScreen}, path: ${testScreenPath})") | ||
| 1024 | |||
| 1025 | def screenTest = new org.moqui.mcp.CustomScreenTestImpl(ec.ecfi) | ||
| 1026 | .rootScreen(rootScreen) | ||
| 1027 | .renderMode(renderMode ? renderMode : "html") | ||
| 1028 | .auth(ec.user.username) | ||
| 1029 | |||
| 1030 | if (screenTest) { | ||
| 1031 | def renderParams = parameters ?: [:] | ||
| 1032 | renderParams.userId = ec.user.userId | ||
| 1033 | renderParams.username = ec.user.username | ||
| 1034 | |||
| 1035 | try { | ||
| 1036 | ec.logger.info("TESTRENDER ${testScreenPath} ${renderParams}") | ||
| 1037 | def testRender = screenTest.render(testScreenPath, renderParams, "POST") | ||
| 1038 | output = testRender.getOutput() | ||
| 1039 | ec.logger.info("MCP Screen Execution: Successfully rendered screen ${screenPath}, output length: ${output?.length() ?: 0}") | ||
| 1040 | } catch (java.util.concurrent.TimeoutException e) { | ||
| 1041 | throw new Exception("Screen rendering timed out after 30 seconds for ${screenPath}") | ||
| 1042 | } | ||
| 1043 | } | ||
| 1014 | } | 1044 | } |
| 1015 | } catch (Exception e) { | 1045 | } catch (Exception e) { |
| 1016 | isError = true | 1046 | isError = true |
| ... | @@ -1068,17 +1098,18 @@ def startTime = System.currentTimeMillis() | ... | @@ -1068,17 +1098,18 @@ def startTime = System.currentTimeMillis() |
| 1068 | // Helper function to convert web paths to MCP tool names | 1098 | // Helper function to convert web paths to MCP tool names |
| 1069 | def convertWebPathToMcpTool = { path -> | 1099 | def convertWebPathToMcpTool = { path -> |
| 1070 | try { | 1100 | try { |
| 1101 | ec.logger.info("Converting web path to MCP tool: ${path}") | ||
| 1071 | // Handle simple catalog paths (dropdown menu items) | 1102 | // Handle simple catalog paths (dropdown menu items) |
| 1072 | if (path == "Category" || path.startsWith("Category/")) { | 1103 | if (path == "Category" || path.startsWith("Category/") || path == "Product/FindProduct/getCategoryList") { |
| 1073 | return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Category" | 1104 | return "moqui_SimpleScreens_SimpleScreens_Catalog_Category" |
| 1074 | } else if (path == "Feature" || path.startsWith("Feature/")) { | 1105 | } else if (path == "Feature" || path.startsWith("Feature/")) { |
| 1075 | return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Feature" | 1106 | return "moqui_SimpleScreens_SimpleScreens_Catalog_Feature" |
| 1076 | } else if (path == "FeatureGroup" || path.startsWith("FeatureGroup/")) { | 1107 | } else if (path == "FeatureGroup" || path.startsWith("FeatureGroup/")) { |
| 1077 | return "screen_SimpleScreens_screen_SimpleScreens_Catalog_FeatureGroup" | 1108 | return "moqui_SimpleScreens_SimpleScreens_Catalog_FeatureGroup" |
| 1078 | } else if (path == "Product" || path.startsWith("Product/")) { | 1109 | } else if (path == "Product" || path.startsWith("Product/")) { |
| 1079 | return "screen_SimpleScreens_screen_SimpleScreens_Catalog_Product" | 1110 | return "moqui_SimpleScreens_SimpleScreens_Catalog_Product" |
| 1080 | } else if (path.startsWith("Search")) { | 1111 | } else if (path.startsWith("Search")) { |
| 1081 | return "screen_SimpleScreens_screen_SimpleScreens_Search" | 1112 | return "moqui_SimpleScreens_SimpleScreens_Search" |
| 1082 | } | 1113 | } |
| 1083 | 1114 | ||
| 1084 | // Handle full catalog paths | 1115 | // Handle full catalog paths |
| ... | @@ -1389,18 +1420,51 @@ def startTime = System.currentTimeMillis() | ... | @@ -1389,18 +1420,51 @@ def startTime = System.currentTimeMillis() |
| 1389 | } else { | 1420 | } else { |
| 1390 | // Resolve path | 1421 | // Resolve path |
| 1391 | def screenPath = path.startsWith("moqui_") ? McpUtils.getScreenPath(path) : null | 1422 | def screenPath = path.startsWith("moqui_") ? McpUtils.getScreenPath(path) : null |
| 1423 | // Keep track of the tool name used to reach here, to maintain context | ||
| 1424 | def baseToolName = path.startsWith("moqui_") ? path : null | ||
| 1425 | |||
| 1392 | if (!screenPath && !path.startsWith("component://")) { | 1426 | if (!screenPath && !path.startsWith("component://")) { |
| 1393 | // Try to handle "popcommerce/admin" style by guessing | 1427 | // Try to handle "popcommerce/admin" style by guessing |
| 1394 | def toolName = "moqui_" + path.replace('/', '_') | 1428 | def toolName = "moqui_" + path.replace('/', '_') |
| 1395 | screenPath = McpUtils.getScreenPath(toolName) | 1429 | screenPath = McpUtils.getScreenPath(toolName) |
| 1430 | if (!baseToolName) baseToolName = toolName | ||
| 1396 | } else if (path.startsWith("component://")) { | 1431 | } else if (path.startsWith("component://")) { |
| 1397 | screenPath = path | 1432 | screenPath = path |
| 1433 | // If starting with component path, we can't easily establish a base tool name | ||
| 1434 | // that preserves context if it's not a standard path. | ||
| 1435 | // But McpUtils.getToolName will try. | ||
| 1436 | if (!baseToolName) baseToolName = McpUtils.getToolName(path) | ||
| 1398 | } | 1437 | } |
| 1399 | 1438 | ||
| 1400 | if (screenPath) { | 1439 | if (screenPath) { |
| 1440 | // Check if screen exists, if not try to resolve as subscreen | ||
| 1441 | if (!ec.resource.getLocationReference(screenPath).getExists()) { | ||
| 1442 | def lastSlash = screenPath.lastIndexOf('/') | ||
| 1443 | if (lastSlash > 0) { | ||
| 1444 | def parentPath = screenPath.substring(0, lastSlash) + ".xml" | ||
| 1445 | def subscreenName = screenPath.substring(lastSlash + 1).replace('.xml', '') | ||
| 1446 | |||
| 1447 | if (ec.resource.getLocationReference(parentPath).getExists()) { | ||
| 1448 | // Found parent, now find the subscreen location | ||
| 1449 | try { | ||
| 1450 | def parentDef = ec.screen.getScreenDefinition(parentPath) | ||
| 1451 | def subscreenItem = parentDef?.getSubscreensItem(subscreenName) | ||
| 1452 | if (subscreenItem && subscreenItem.getLocation()) { | ||
| 1453 | ec.logger.info("Redirecting browse from ${screenPath} to ${subscreenItem.getLocation()}") | ||
| 1454 | screenPath = subscreenItem.getLocation() | ||
| 1455 | } | ||
| 1456 | } catch (Exception e) { | ||
| 1457 | ec.logger.warn("Error resolving subscreen location: ${e.message}") | ||
| 1458 | } | ||
| 1459 | } | ||
| 1460 | } | ||
| 1461 | } | ||
| 1462 | |||
| 1401 | try { | 1463 | try { |
| 1402 | // First, add the current screen itself as a tool if it's executable | 1464 | // First, add the current screen itself as a tool if it's executable |
| 1403 | def currentToolName = McpUtils.getToolName(screenPath) | 1465 | // Use baseToolName if available to preserve context (e.g. moqui_PopCommerce_...) |
| 1466 | // even if the implementation is in another component (e.g. SimpleScreens) | ||
| 1467 | def currentToolName = baseToolName ?: McpUtils.getToolName(screenPath) | ||
| 1404 | tools << [ | 1468 | tools << [ |
| 1405 | name: currentToolName, | 1469 | name: currentToolName, |
| 1406 | description: "Execute screen: ${currentToolName}", | 1470 | description: "Execute screen: ${currentToolName}", |
| ... | @@ -1427,7 +1491,7 @@ def startTime = System.currentTimeMillis() | ... | @@ -1427,7 +1491,7 @@ def startTime = System.currentTimeMillis() |
| 1427 | // Construct sub-tool name by extending the parent path | 1491 | // Construct sub-tool name by extending the parent path |
| 1428 | // This assumes subscreens are structurally nested in the tool name | 1492 | // This assumes subscreens are structurally nested in the tool name |
| 1429 | // e.g. moqui_PopCommerce_Admin -> moqui_PopCommerce_Admin_Catalog | 1493 | // e.g. moqui_PopCommerce_Admin -> moqui_PopCommerce_Admin_Catalog |
| 1430 | def parentToolName = McpUtils.getToolName(screenPath) | 1494 | def parentToolName = baseToolName ?: McpUtils.getToolName(screenPath) |
| 1431 | def subToolName = parentToolName + "_" + subName | 1495 | def subToolName = parentToolName + "_" + subName |
| 1432 | 1496 | ||
| 1433 | subscreens << [ | 1497 | subscreens << [ | ... | ... |
-
Please register or sign in to post a comment