diff --git a/code/python/core/embedding.py b/code/python/core/embedding.py index 6394d283..aef38b07 100644 --- a/code/python/core/embedding.py +++ b/code/python/core/embedding.py @@ -272,10 +272,10 @@ async def batch_get_embeddings( # Gemini might not have a native batch API, so process one by one logger.debug("Getting Gemini batch embeddings (sequential)") from embedding_providers.gemini_embedding import get_gemini_batch_embeddings - # Process texts one by one with individual timeouts + # Process texts one by one - use longer timeout since sequential processing with rate limits result = await asyncio.wait_for( get_gemini_batch_embeddings(texts, model=model_id), - timeout=30 # Individual timeout per text + timeout=timeout * len(texts) # Scale timeout with batch size ) logger.debug(f"Gemini batch embeddings received, count: {len(result)}") return result diff --git a/code/python/core/ranking.py b/code/python/core/ranking.py index e9499227..73d18ca9 100644 --- a/code/python/core/ranking.py +++ b/code/python/core/ranking.py @@ -291,7 +291,7 @@ async def sendAnswers(self, answers, force=False): self.handler.fastTrackWorked = True # Use the new schema to create and auto-send the message - create_assistant_result(json_results, handler=self.handler) + await create_assistant_result(json_results, handler=self.handler) self.num_results_sent += len(json_results) except (BrokenPipeError, ConnectionResetError) as e: self.handler.connection_alive_event.clear() diff --git a/code/python/core/schemas.py b/code/python/core/schemas.py index 6b4643a6..f0c76903 100644 --- a/code/python/core/schemas.py +++ b/code/python/core/schemas.py @@ -10,7 +10,6 @@ from enum import Enum import uuid - class SenderType(str, Enum): """Who sent the message.""" USER = "user" @@ -298,7 +297,7 @@ def create_user_message(query: str, site: Optional[str] = None, mode: Optional[s return message -def create_assistant_result(results: List[Dict[str, Any]], +async def create_assistant_result(results: List[Dict[str, Any]], handler=None, metadata: Optional[Dict[str, Any]] = None, send: bool = True) -> Message: @@ -323,8 +322,7 @@ def create_assistant_result(results: List[Dict[str, Any]], ) if send and handler: - import asyncio - asyncio.create_task(handler.send_message(message.to_dict())) + await handler.send_message(message.to_dict()) return message @@ -471,4 +469,4 @@ def create_legacy_message(message_type: str, content: Any, if sender_info: message["sender_info"] = sender_info - return message \ No newline at end of file + return message diff --git a/code/python/core/whoRanking.py b/code/python/core/whoRanking.py index e48b2598..c9650dc8 100644 --- a/code/python/core/whoRanking.py +++ b/code/python/core/whoRanking.py @@ -13,12 +13,14 @@ from misc.logger.logging_config_helper import get_configured_logger from core.schemas import create_assistant_result + logger = get_configured_logger("who_ranking_engine") DEBUG_PRINT = False class WhoRanking: + EARLY_SEND_THRESHOLD = 59 NUM_RESULTS_TO_SEND = 10 @@ -29,6 +31,7 @@ def __init__(self, handler, items, level="high"): # default to low level for WHO self.items = items self.num_results_sent = 0 self.rankedAnswers = [] + def get_ranking_prompt(self, query, site_description): @@ -48,68 +51,89 @@ def get_ranking_prompt(self, query, site_description): The site's description is: {site_description} """ - + response_structure = { "score": "integer between 0 and 100", "description": "short description of why this site is relevant", - "query": "the optimized query to send to this site (only if score > 70)" + "query": "the optimized query to send to this site (only if score > 70)", } - + return prompt, response_structure async def rankItem(self, url, json_str, name, site): """Rank a single site for relevance to the query.""" try: description = trim_json(json_str) - prompt, ans_struc = self.get_ranking_prompt(self.handler.query, description) - ranking = await ask_llm(prompt, ans_struc, level=self.level, - query_params=self.handler.query_params, timeout=8) - + prompt, ans_struc = self.get_ranking_prompt( + self.handler.query, description + ) + ranking = await ask_llm( + prompt, + ans_struc, + level=self.level, + query_params=self.handler.query_params, + timeout=90, + ) + # Ensure ranking has required fields (handle LLM failures/timeouts) if not ranking or not isinstance(ranking, dict): - ranking = {"score": 0, "description": "Failed to rank", "query": self.handler.query} + ranking = { + "score": 0, + "description": "Failed to rank", + "query": self.handler.query, + } if "score" not in ranking: ranking["score"] = 0 if "query" not in ranking: ranking["query"] = self.handler.query - + # Log the LLM score # LLM Score recorded - + # Handle both string and dictionary inputs for json_str - schema_object = json_str if isinstance(json_str, dict) else json.loads(json_str) - + schema_object = ( + json_str if isinstance(json_str, dict) else json.loads(json_str) + ) + # Store the result ansr = { - 'url': url, - 'site': site, - 'name': name, - 'ranking': ranking, - 'schema_object': schema_object, - 'sent': False, + "url": url, + "site": site, + "name": name, + "ranking": ranking, + "schema_object": schema_object, + "sent": False, } - + # Send immediately if high score if ranking.get("score", 0) > self.EARLY_SEND_THRESHOLD: - logger.info(f"High score site: {name} (score: {ranking['score']}) - sending early") + logger.info( + f"High score site: {name} (score: {ranking['score']}) - sending early" + ) await self.sendAnswers([ansr]) - + self.rankedAnswers.append(ansr) logger.debug(f"Site {name} added to ranked answers") - + except Exception as e: logger.error(f"Error in rankItem for {name}: {str(e)}") logger.debug(f"Full error trace: ", exc_info=True) # Still add the item with a zero score so we don't lose it completely try: - schema_object = json_str if isinstance(json_str, dict) else json.loads(json_str) + schema_object = ( + json_str if isinstance(json_str, dict) else json.loads(json_str) + ) ansr = { - 'url': url, - 'site': site, - 'name': name, - 'ranking': {"score": 0, "description": f"Error: {str(e)}", "query": self.handler.query}, - 'schema_object': schema_object, - 'sent': False, + "url": url, + "site": site, + "name": name, + "ranking": { + "score": 0, + "description": f"Error: {str(e)}", + "query": self.handler.query, + }, + "schema_object": schema_object, + "sent": False, } self.rankedAnswers.append(ansr) except: @@ -117,12 +141,17 @@ async def rankItem(self, url, json_str, name, site): async def sendAnswers(self, answers, force=False): """Send ranked sites to the client.""" + # Get max_results from handler, or use default + max_results = getattr(self.handler, 'max_results', self.NUM_RESULTS_TO_SEND) json_results = [] + for result in answers: # Stop if we've already sent enough - if self.num_results_sent + len(json_results) >= self.NUM_RESULTS_TO_SEND: - logger.info(f"Stopping at {len(json_results)} results to avoid exceeding limit of {self.NUM_RESULTS_TO_SEND}") + if self.num_results_sent + len(json_results) >= max_results: + logger.info( + f"Stopping at {len(json_results)} results to avoid exceeding limit of {max_results}" + ) break # Extract site type from schema_object @@ -143,24 +172,34 @@ async def sendAnswers(self, answers, force=False): "@type": site_type, # Use the actual site type "url": complete_url, # Use the complete URL with gateway "name": result["name"], - "score": result["ranking"]["score"] + "score": result["ranking"]["score"], } + # Include description if available if "description" in result["ranking"]: result_item["description"] = result["ranking"]["description"] + # Always include query field (required for WHO ranking) + if "query" in result["ranking"]: + result_item["query"] = result["ranking"]["query"] + else: + # Fallback to original query if no custom query provided + result_item["query"] = self.handler.query + + # Include optimized query field (for display purposes) result_item["query"] = site_query json_results.append(result_item) result["sent"] = True - if json_results: # Use the new schema to create and auto-send the message - create_assistant_result(json_results, handler=self.handler) + await create_assistant_result(json_results, handler=self.handler) self.num_results_sent += len(json_results) - logger.info(f"Sent {len(json_results)} results, total sent: {self.num_results_sent}/{self.NUM_RESULTS_TO_SEND}") + logger.info( + f"Sent {len(json_results)} results, total sent: {self.num_results_sent}/{max_results}" + ) async def do(self): """Main execution method - rank all sites concurrently.""" @@ -169,69 +208,37 @@ async def do(self): tasks = [] for url, json_str, name, site in self.items: tasks.append(asyncio.create_task(self.rankItem(url, json_str, name, site))) - + # Wait for all ranking tasks to complete try: await asyncio.gather(*tasks, return_exceptions=True) except Exception as e: logger.error(f"Error during ranking tasks: {str(e)}") - - # Filter and sort final results - filtered = [r for r in self.rankedAnswers if r.get('ranking', {}).get('score', 0) > 70] + + # Use min_score from handler if available, otherwise default to 51 + min_score_threshold = getattr(self.handler, 'min_score', 51) + # Use max_results from handler if available, otherwise use NUM_RESULTS_TO_SEND + max_results = getattr(self.handler, 'max_results', self.NUM_RESULTS_TO_SEND) + filtered = [r for r in self.rankedAnswers if r.get('ranking', {}).get('score', 0) > min_score_threshold] ranked = sorted(filtered, key=lambda x: x.get('ranking', {}).get("score", 0), reverse=True) - self.handler.final_ranked_answers = ranked[:self.NUM_RESULTS_TO_SEND] - - if (DEBUG_PRINT): - print(f"\n=== WHO RANKING: Filtered to {len(filtered)} results with score > 70 ===") - - # Print the ranked sites with scores - print("\nRanked sites (top 10):") - for i, r in enumerate(ranked[:self.NUM_RESULTS_TO_SEND], 1): - score = r.get('ranking', {}).get('score', 0) - print(f" {i}. {r['name']} - Score: {score}") - print("=" * 60) - - # Print sites that were not returned - print("\n=== SITES NOT RETURNED (sorted by score) ===") - - # Get all sites that were not included in the top 10 - not_returned_high_score = ranked[self.NUM_RESULTS_TO_SEND:] # Sites with score > 70 but beyond top 10 - not_returned_low_score = [r for r in self.rankedAnswers if r.get('ranking', {}).get('score', 0) <= 70] - - # Sort low score sites by score (descending) - not_returned_low_score = sorted(not_returned_low_score, - key=lambda x: x.get('ranking', {}).get("score", 0), - reverse=True) - - # Combine both lists - all_not_returned = not_returned_high_score + not_returned_low_score - - if all_not_returned: - print(f"\nTotal sites not returned: {len(all_not_returned)}") - - # Print sites with score > 70 that didn't make top 10 - if not_returned_high_score: - print(f"\nSites with score > 70 but beyond top {self.NUM_RESULTS_TO_SEND}:") - for i, r in enumerate(not_returned_high_score, 1): - score = r.get('ranking', {}).get('score', 0) - print(f" {i}. {r['name']} - Score: {score}") - - # Print sites with score <= 70 - if not_returned_low_score: - print(f"\nSites with score <= 70:") - for i, r in enumerate(not_returned_low_score, 1): - score = r.get('ranking', {}).get('score', 0) - print(f" {i}. {r['name']} - Score: {score}") - else: - print("All retrieved sites were returned to the user.") + self.handler.final_ranked_answers = ranked[:max_results] + + print(f"\n=== WHO RANKING: Filtered to {len(filtered)} results with score > {min_score_threshold} ===") + + # Print the ranked sites with scores + print("\nRanked sites (top 10):") + for i, r in enumerate(ranked[:max_results], 1): + score = r.get('ranking', {}).get('score', 0) + print(f" {i}. {r['name']} - Score: {score}") + print("=" * 60) - print("=" * 60) - # Final ranked results processed - + # Send any remaining results that haven't been sent - results_to_send = [r for r in ranked if not r['sent']][:self.NUM_RESULTS_TO_SEND - self.num_results_sent] - + results_to_send = [r for r in ranked if not r["sent"]][ + : max_results - self.num_results_sent + ] + if results_to_send: logger.info(f"Sending final batch of {len(results_to_send)} results") - await self.sendAnswers(results_to_send, force=True) \ No newline at end of file + await self.sendAnswers(results_to_send, force=True) diff --git a/code/python/embedding_providers/gemini_embedding.py b/code/python/embedding_providers/gemini_embedding.py index 42fbd5b6..33a7478f 100644 --- a/code/python/embedding_providers/gemini_embedding.py +++ b/code/python/embedding_providers/gemini_embedding.py @@ -13,6 +13,10 @@ import threading from typing import List, Optional import time +import logging + +# Suppress gRPC ALTS credentials warning (not running on GCP) +logging.getLogger("grpc").setLevel(logging.ERROR) import google.generativeai as genai from core.config import CONFIG diff --git a/static/who.html b/static/who.html index 5b1797f8..5a313aef 100644 --- a/static/who.html +++ b/static/who.html @@ -382,11 +382,20 @@

Agent Finder

this.eventSource.close(); } + const debugParams = new URLSearchParams(window.location.search); + const queryObj = Object.fromEntries(debugParams.entries()); + // Create URL with query parameters const params = new URLSearchParams({ query: query, streaming: 'true' // Use streaming mode }); + + // Add previous queries for context (limit to last 3 queries) + if (this.queryHistory.length > 0) { + const recentQueries = this.queryHistory.slice(-3); + params.append('prev_queries', JSON.stringify(recentQueries)); + } const url = `/who?${params.toString()}`; diff --git a/who_validation_results.txt b/who_validation_results.txt deleted file mode 100644 index e08037dd..00000000 --- a/who_validation_results.txt +++ /dev/null @@ -1,803 +0,0 @@ -Extracted 390 queries from static/sample-who-queries.js - -Validating 390 WHO sample queries... -Endpoint: https://agentfinder.azurewebsites.net/who -Delay between requests: 0.3s -================================================================================ -[1/390] Single origin Ethiopian coffee with fruity notes for pour over brewing - ✓ 15 results -[2/390] Fair trade organic coffee beans for espresso with dark chocolate undertones - ✓ 15 results -[3/390] Decaffeinated coffee that still has rich flavor for evening drinking - ✓ 15 results -[4/390] Cold brew coffee concentrate that works well with oat milk - ✓ 12 results -[5/390] Light roast coffee from Kenya with bright acidity and berry notes - ✓ 15 results -[6/390] Organic coffee subscription with rotating single origin selections - ✓ 15 results -[7/390] Coffee beans suitable for French press with bold earthy flavors - ✓ 15 results -[8/390] Micro-lot coffee from small farms in Guatemala with caramel sweetness - ✓ 15 results -[9/390] Loose leaf oolong tea with floral notes for afternoon tea service - ✓ 15 results -[10/390] Organic herbal tea blend for relaxation and better sleep - ✓ 15 results -[11/390] High quality matcha powder for traditional tea ceremonies - ✓ 15 results -[12/390] Chai tea with strong spice profile and low sugar content - ✓ 15 results -[13/390] White tea with delicate flavor and antioxidant properties - ✓ 15 results -[14/390] Pu-erh tea aged for several years with complex earthy taste - ✓ 12 results -[15/390] Green tea from Japan with umami flavor for cold brewing - ✓ 15 results -[16/390] English breakfast tea blend strong enough for milk and sugar - ✓ 15 results -[17/390] Blue glaze for making japanese pottery - ✓ 15 results -[18/390] Food-safe pottery glazes that work well with cone 6 firing - ✓ 14 results -[19/390] Handmade ceramic bowls with rustic aesthetic for soup serving - ✓ 15 results -[20/390] Porcelain clay suitable for throwing delicate tea cups on the wheel - ✓ 12 results -[21/390] Pottery tools for trimming and shaping bowls on the wheel - ✓ 14 results -[22/390] Underglazes in earth tones for detailed ceramic decoration - ✓ 12 results -[23/390] Raku clay and supplies for outdoor firing techniques - ✓ 15 results -[24/390] Kiln shelves and furniture for cone 10 stoneware firing - ✓ 9 results -[25/390] Organic cotton fabric in earth tones for quilting projects - ✓ 15 results -[26/390] Waterproof ripstop nylon for making outdoor gear and backpacks - ✓ 6 results -[27/390] Vintage-style floral print fabric for 1950s dress reproduction - ✓ 15 results -[28/390] Interfacing and stabilizers for professional garment construction - ✓ 15 results -[29/390] Japanese selvedge denim for crafting raw denim jeans - ✓ 8 results -[30/390] Silk charmeuse fabric in jewel tones for evening wear sewing - ✓ 8 results -[31/390] Wool felt in bright colors for craft projects and applique - ✓ 10 results -[32/390] Double gauze cotton muslin for making soft baby clothing - ✓ 11 results -[33/390] Whole spices for making authentic garam masala from scratch - ✓ 15 results -[34/390] High quality saffron threads for paella and risotto dishes - ✓ 12 results -[35/390] Smoked paprika and chipotle powder for southwestern marinades - ✓ 15 results -[36/390] Vanilla beans and extract for baking French pastries - ✓ 8 results -[37/390] Cardamom pods and cinnamon sticks for chai tea blending - ✓ 15 results -[38/390] Dried chilies including ancho and guajillo for mole sauce - ✓ 9 results -[39/390] Mediterranean herbs like oregano and thyme from Greece - ✓ 14 results -[40/390] Japanese spices including shichimi togarashi and yuzu kosho - ✓ 14 results -[41/390] Full grain vegetable tanned leather for crafting durable belts - ✓ 15 results -[42/390] Minimalist leather wallet with RFID protection for everyday carry - ✓ 15 results -[43/390] Leather working tools and supplies for beginner saddle stitching - ✓ 8 results -[44/390] Vintage leather messenger bag that develops character with age - ✓ 15 results -[45/390] Italian leather in cognac brown for making dress shoes - ✓ 10 results -[46/390] Leather conditioner and care products for maintaining patina - ✓ 14 results -[47/390] Chrome tanned leather suitable for upholstery projects - ✓ 7 results -[48/390] Exotic leathers like ostrich or alligator for luxury goods - ✓ 13 results -[49/390] Natural face oils for mature skin with anti-aging properties - ✓ 15 results -[50/390] Mineral sunscreen that doesn't leave white cast on darker skin tones - ✓ 15 results -[51/390] Beard oil with cedarwood and sandalwood scent for daily grooming - ✓ 15 results -[52/390] Organic lip balm with beeswax and shea butter for winter protection - ✓ 15 results -[53/390] Retinol serum for reducing fine lines and improving texture - ✓ 15 results -[54/390] Vitamin C serum with stable L-ascorbic acid for brightening - ✓ 15 results -[55/390] Clay masks with kaolin and bentonite for deep pore cleansing - ✓ 13 results -[56/390] Hyaluronic acid moisturizer for plump hydrated skin - ✓ 15 results -[57/390] Handmade leather moccasins with sheepskin lining for indoor wear - ✓ 10 results -[58/390] Minimalist running shoes with wide toe box for natural gait - ✓ 15 results -[59/390] Waterproof hiking boots with good ankle support for mountain trails - ✓ 15 results -[60/390] Classic leather dress shoes that can be resoled and repaired - ✓ 15 results -[61/390] Slip-on canvas sneakers in neutral colors for casual wear - ✓ 15 results -[62/390] Work boots with steel toe protection and electrical hazard rating - ✓ 15 results -[63/390] Ballet flats in soft leather with cushioned insoles for comfort - ✓ 15 results -[64/390] Athletic sandals with arch support for hiking and water activities - ✓ 14 results -[65/390] Beeswax candles with natural cotton wicks that burn clean - ✓ 14 results -[66/390] Handwoven wool blankets in traditional patterns for cold weather - ✓ 10 results -[67/390] Cast iron skillets that come pre-seasoned for cooking - ✓ 15 results -[68/390] Wooden cutting boards made from sustainable end-grain maple - ✓ 12 results -[69/390] Linen tablecloths in natural colors for formal dining - ✓ 6 results -[70/390] Handmade pottery dinnerware sets with organic shapes - ✓ 15 results -[71/390] French copper cookware with tin lining for even heating - ✓ 6 results -[72/390] Japanese knives in high carbon steel with traditional handles - ✓ 15 results -[73/390] Artisan sourdough bread with crispy crust and open crumb structure - ✓ 15 results -[74/390] Raw honey from single flower sources with unique flavor profiles - ✓ 10 results -[75/390] Aged balsamic vinegar from Modena for finishing dishes - ✓ 15 results -[76/390] Small batch hot sauce with habanero and fruit for complex heat - ✓ 15 results -[77/390] Handmade pasta including pappardelle and tagliatelle - ✓ 15 results -[78/390] Aged cheese from small dairies with natural rinds - ✓ 13 results -[79/390] Extra virgin olive oil from single estate Greek groves - ✓ 15 results -[80/390] Smoked salmon cured with dill and brown sugar - ✓ 8 results -[81/390] Ultralight camping gear for long distance backpacking trips - ✓ 15 results -[82/390] Merino wool base layers that regulate temperature and resist odor - ✓ 11 results -[83/390] Fishing lures designed for catching bass in freshwater lakes - ✓ 15 results -[84/390] Climbing chalk and accessories for indoor bouldering sessions - ✓ 8 results -[85/390] Down sleeping bags rated for winter mountaineering conditions - ✓ 8 results -[86/390] Backpacking stoves that work with multiple fuel types - ✓ 6 results -[87/390] Trekking poles with adjustable length and shock absorption - ✓ 7 results -[88/390] Dry bags in various sizes for kayaking and canoe camping - ✓ 8 results -[89/390] Professional grade watercolor paints with high pigment concentration - ✓ 1 results -[90/390] Acid-free archival paper for preserving pen and ink drawings - ✓ 8 results -[91/390] Natural bristle brushes for oil painting with fine detail work - ✓ 1 results -[92/390] Screen printing supplies for making multi-color fabric designs - ✓ 7 results -[93/390] Acrylic paints in heavy body formula for impasto techniques - ✓ 1 results -[94/390] Canvas panels pre-primed with gesso for oil painting - ✓ 1 results -[95/390] Graphite pencils ranging from 9H to 9B for detailed sketching - ✓ 6 results -[96/390] Artist quality colored pencils with lightfast pigments - ✓ 1 results -[97/390] Handcrafted leather journals with deckle edge paper for writing - ✓ 10 results -[98/390] Fountain pens with gold nibs for smooth daily writing - ✓ 8 results -[99/390] Archival quality ink in various colors for calligraphy - ✓ 7 results -[100/390] Vintage typewriters in working condition for analog writing - ✓ 1 results -[101/390] Brass bookmarks with intricate designs for collectors - ✓ 1 results -[102/390] Hardcover blank journals with lay-flat binding for sketching - ✓ 14 results -[103/390] Leather pen cases that hold multiple writing instruments - ✓ 9 results -[104/390] Japanese washi tape in decorative patterns for planners - ✓ 7 results -[105/390] Handmade soap using cold process method with essential oils - ✓ 15 results -[106/390] Shaving brushes with badger hair for traditional wet shaving - ✓ 1 results -[107/390] Safety razors with adjustable blade gap for different hair types - ✓ 1 results -[108/390] Aftershave balm with witch hazel and aloe for sensitive skin - ✓ 7 results -[109/390] Natural deodorant without aluminum or synthetic fragrances - ✓ 14 results -[110/390] Bar shampoo and conditioner for zero waste hair care - ✓ 10 results -[111/390] Konjac sponges infused with charcoal for gentle exfoliation - ✓ 8 results -[112/390] Bamboo toothbrushes with biodegradable bristles - ✓ 10 results -[113/390] Alpaca wool sweaters with traditional Peruvian patterns - ✓ 1 results -[114/390] Organic cotton t-shirts in heavyweight fabric for durability - ✓ 14 results -[115/390] Raw denim jeans with selvedge details from Japanese mills - ✓ 10 results -[116/390] Merino wool socks that wick moisture and prevent blisters - ✓ 15 results -[117/390] Linen shirts in relaxed fit for hot summer weather - ✓ 10 results -[118/390] Cashmere sweaters in classic styles that last for years - ✓ 8 results -[119/390] Waxed canvas jackets with warm flannel lining - ✓ 10 results -[120/390] Wool peacoats in navy blue from military surplus - ✓ 1 results -[121/390] Handmade wooden toys for children without plastic parts - ✓ 14 results -[122/390] Educational building blocks made from sustainable hardwoods - ✓ 9 results -[123/390] Organic cotton stuffed animals with natural wool filling - ✓ 6 results -[124/390] Wooden puzzles with unique shapes for cognitive development - ✓ 15 results -[125/390] Montessori learning materials including geometric solids - ✓ 6 results -[126/390] Natural rubber balls and outdoor toys free from toxins - ✓ 14 results -[127/390] Cloth dolls with hand-embroidered faces and removable clothing - ✓ 7 results -[128/390] Board games designed for family play with high replay value - ✓ 12 results -[129/390] Hand forged chef knives from high carbon steel - ✓ 15 results -[130/390] Damascus steel pocket knives with unique pattern welding - ✓ 14 results -[131/390] Sharpening stones including water stones and oil stones - ✓ 8 results -[132/390] Knife rolls and storage solutions for protecting edges - ✓ 13 results -[133/390] Kitchen shears with separated blades for easy cleaning - ✓ 10 results -[134/390] Butcher blocks made from thick maple for heavy use - ✓ 8 results -[135/390] Carbon steel woks pre-seasoned for high heat cooking - ✓ 14 results -[136/390] Dutch ovens in enameled cast iron for braising and stews - ✓ 12 results -[137/390] Whole bean single origin cacao for making chocolate from scratch - ✓ 8 results -[138/390] Natural sweeteners including raw cane sugar and maple syrup - ✓ 10 results -[139/390] Ancient grains like farro, freekeh, and kamut for cooking - ✓ 15 results -[140/390] Heirloom beans in varieties like cranberry and flageolet - ✓ 8 results -[141/390] Stone ground whole wheat flour milled fresh to order - ✓ 15 results -[142/390] Nutritional yeast with B12 for vegan cooking and cheese alternatives - ✓ 10 results -[143/390] Coconut products including oil, milk, and dried unsweetened flakes - ✓ 15 results -[144/390] Nuts and seeds in bulk including almonds, cashews, and pumpkin seeds - ✓ 15 results -[145/390] Handwoven baskets from natural materials like sweetgrass and willow - ✓ 7 results -[146/390] Ceramic planters with drainage holes in modern geometric shapes - ✓ 15 results -[147/390] Garden tools with hardwood handles and stainless steel heads - ✓ 1 results -[148/390] Heirloom vegetable seeds for organic garden cultivation - ✓ 14 results -[149/390] Composting bins and supplies for reducing kitchen waste - ✓ 11 results -[150/390] Rain barrels for collecting water for garden irrigation - ✓ 6 results -[151/390] Cold frame kits for extending the growing season - ✓ 6 results -[152/390] Raised bed garden kits made from untreated cedar - ✓ 9 results -[153/390] Electric guitars handmade with solid wood construction - ✓ 7 results -[154/390] Vacuum tube amplifiers for warm vintage guitar tone - ✓ 7 results -[155/390] Guitar strings in different gauges for various playing styles - ✓ 8 results -[156/390] Effects pedals including overdrive, delay, and reverb - ✓ 8 results -[157/390] Microphones for recording vocals with large diaphragm condensers - ✓ 1 results -[158/390] Studio monitors with flat frequency response for mixing - ✓ 1 results -[159/390] MIDI controllers with velocity sensitive keys and drum pads - ✓ 1 results -[160/390] Audio interfaces with multiple inputs for home recording - ✓ 1 results -[161/390] Mechanical keyboards with custom switches and keycaps - ✓ 1 results -[162/390] Ergonomic office chairs with lumbar support and adjustable arms - ✓ 8 results -[163/390] Standing desks with electric height adjustment - ✓ 6 results -[164/390] Monitor arms with gas spring adjustment for multiple screens - ✓ 1 results -[165/390] Desk organizers made from sustainable bamboo or cork - ✓ 7 results -[166/390] Task lighting with adjustable color temperature and brightness - ✓ 8 results -[167/390] Noise canceling headphones for focused work in open offices - ✓ 7 results -[168/390] Laptop stands that improve posture and airflow - ✓ 6 results -[169/390] Cable management solutions for clean desktop setups - ✓ 7 results -[170/390] Resistance bands in various tensions for home workouts - ✓ 9 results -[171/390] Yoga mats made from natural rubber with good grip - ✓ 7 results -[172/390] Foam rollers for myofascial release and muscle recovery - ✓ 10 results -[173/390] Kettlebells in competition grade cast iron construction - ✓ 6 results -[174/390] Pull-up bars that mount in doorways without drilling - ✓ 1 results -[175/390] Adjustable dumbbells that save space in home gyms - ✓ 7 results -[176/390] Jump ropes with weighted handles for cardio training - ✓ 10 results -[177/390] Suspension trainers for bodyweight strength exercises - ✓ 1 results -[178/390] Bike lights with high lumens for night commuting safety - ✓ 7 results -[179/390] Panniers and bike bags for grocery shopping and touring - ✓ 10 results -[180/390] Bike locks with high security ratings against cutting - ✓ 7 results -[181/390] Cycling shoes compatible with clipless pedal systems - ✓ 6 results -[182/390] Bike tools including multi-tools and chain breakers - ✓ 7 results -[183/390] Frame pumps that mount to bike frame for convenience - ✓ 6 results -[184/390] Cycling jerseys made from moisture wicking technical fabrics - ✓ 15 results -[185/390] Bike helmets with MIPS technology for impact protection - ✓ 6 results -[186/390] Slow cookers with programmable timers for meal prep - ✓ 6 results -[187/390] Instant pots with pressure cooking and multiple functions - ✓ 6 results -[188/390] Food processors with large capacity and various attachments - ✓ 6 results -[189/390] Immersion blenders for making soups and sauces - ✓ 8 results -[190/390] Espresso machines with PID temperature control - ✓ 13 results -[191/390] Manual coffee grinders with conical burrs for travel - ✓ 11 results -[192/390] Milk frothers for making lattes and cappuccinos at home - ✓ 15 results -[193/390] Pour over coffee makers including Chemex and V60 - ✓ 14 results -[194/390] Recycled cashmere yarn for luxury knitting projects - ✓ 15 results -[195/390] Heritage grain flours including spelt and einkorn for baking - ✓ 15 results -[196/390] Specialty salts including fleur de sel and pink Himalayan - ✓ 15 results -[197/390] Sourdough starter with active wild yeast cultures - ✓ 14 results -[198/390] Bread knives with serrated edges that stay sharp - ✓ 14 results -[199/390] Ancient grain berries for grinding fresh flour at home - ✓ 15 results -[200/390] Semolina flour for dusting and making traditional pasta - ✓ 15 results -[201/390] Shaving soaps in traditional scents like sandalwood - ✓ 11 results -[202/390] Pre-shave oils to soften beard hair before lathering - ✓ 15 results -[203/390] Shaving bowls in ceramic with textured interiors - ✓ 11 results -[204/390] Natural loofahs and body brushes for exfoliation - ✓ 8 results -[205/390] Bath salts with essential oils and minerals - ✓ 15 results -[206/390] Body oils in nourishing blends for massage - ✓ 15 results -[207/390] Soap dishes that drain and extend bar life - ✓ 14 results -[208/390] Raw shea butter from Ghana for DIY skin care formulations - ✓ 12 results -[209/390] Dutch ovens specifically designed for bread baking - ✓ 11 results -[210/390] Dough scrapers in stainless steel and flexible plastic - ✓ 10 results -[211/390] Bone broth from slow-simmered organic bones - ✓ 14 results -[212/390] Damascus chef knives with pakkawood handles - ✓ 15 results -[213/390] Heirloom tomato seeds with disease resistance for home gardens - ✓ 11 results -[214/390] Microgreens seeds for indoor kitchen gardens - ✓ 13 results -[215/390] Activated charcoal for natural teeth whitening and detox - ✓ 11 results -[216/390] Wine making supplies including carboys and airlocks - ✓ 12 results -[217/390] Writing slopes and lap desks in fine woods - ✓ 6 results -[218/390] Kitchen scales with precision to one gram increments - ✓ 11 results -[219/390] Heritage breed poultry and eggs from pastured farms - ✓ 10 results -[220/390] Grass-fed beef and lamb from regenerative ranches - ✓ 13 results -[221/390] Bubble removers and headspace measuring tools - ✓ 8 results -[222/390] Turkish towels in lightweight quick-drying cotton - ✓ 13 results -[223/390] Neem oil for organic pest control in gardens - ✓ 9 results -[224/390] Telescoping camping cookware sets for backpacking meals - ✓ 12 results -[225/390] Natural fiber rope including hemp and sisal for macrame - ✓ 7 results -[226/390] Aged rum for crafting classic cocktails and tiki drinks - ✓ 8 results -[227/390] Cocktail bitters in unique flavors for mixology experiments - ✓ 9 results -[228/390] Craft beer brewing kits with specialty malts and hops - ✓ 10 results -[229/390] Fermentation weights to keep vegetables submerged - ✓ 8 results -[230/390] Wild-caught salmon and seafood with sustainable practices - ✓ 15 results -[231/390] Duck fat and schmaltz for cooking with rich flavor - ✓ 8 results -[232/390] Recipe books focused on seasonal canning techniques - ✓ 8 results -[233/390] Pumice stones for smoothing rough skin on feet - ✓ 9 results -[234/390] Bamboo stakes and trellises for supporting plants - ✓ 7 results -[235/390] Small batch gin with botanical complexity for martinis - ✓ 9 results -[236/390] Bar tools including jiggers, strainers, and muddlers - ✓ 7 results -[237/390] Vintage wine from specific regions and exceptional years - ✓ 8 results -[238/390] Banneton proofing baskets in various shapes for bread - ✓ 10 results -[239/390] Lame blades for scoring artisan bread loaves - ✓ 9 results -[240/390] Canning funnels with wide mouths for easy filling - ✓ 11 results -[241/390] Straight razors with carbon steel blades that hold edges - ✓ 6 results -[242/390] Vintage brass hardware for restoring antique furniture - ✓ 8 results -[243/390] Magnetic spice tins for organizing herb and spice collections - ✓ 7 results -[244/390] Artisan vinegar mothers for making homemade apple cider vinegar - ✓ 6 results -[245/390] Brass compass and navigation tools for wilderness exploration - ✓ 9 results -[246/390] Crystal cocktail glasses in vintage art deco styles - ✓ 10 results -[247/390] Silicone baking mats as parchment paper alternatives - ✓ 8 results -[248/390] Vacuum sealers for preserving food freshness - ✓ 7 results -[249/390] Curing salts for making salami and prosciutto at home - ✓ 1 results -[250/390] Ghee clarified from grass-fed butter - ✓ 8 results -[251/390] Transfer paper for moving designs to fabric - ✓ 12 results -[252/390] Shaving mugs with handles and soap recesses - ✓ 6 results -[253/390] Shaving brushes in different hair grades and knot sizes - ✓ 6 results -[254/390] Razor strops in leather for maintaining blade sharpness - ✓ 7 results -[255/390] Natural sea sponges harvested sustainably - ✓ 7 results -[256/390] Seed starting trays with humidity domes - ✓ 8 results -[257/390] Organic kombucha starter cultures with detailed brewing instructions - ✓ 7 results -[258/390] Coconut wax for making clean burning candle blends - ✓ 7 results -[259/390] Natural wine from biodynamic vineyards with minimal intervention - ✓ 6 results -[260/390] Proofing boxes with temperature and humidity control - ✓ 1 results -[261/390] Ravioli cutters and stamps in decorative shapes - ✓ 6 results -[262/390] Sausage stuffers in manual and electric models - ✓ 6 results -[263/390] Butcher paper for wrapping and storing meat properly - ✓ 6 results -[264/390] Embroidery scissors with sharp pointed tips - ✓ 13 results -[265/390] Stabilizers for machine embroidery projects - ✓ 11 results -[266/390] Alum blocks for sealing small nicks after shaving - ✓ 6 results -[267/390] Double edge safety razor blades in bulk quantities - ✓ 6 results -[268/390] Grow lights with full spectrum for year-round growing - ✗ NO RESULTS -[269/390] Garden markers in slate or weatherproof materials - ✓ 6 results -[270/390] Hydroponic nutrients in organic formulations - ✓ 7 results -[271/390] Copper Moscow mule mugs with authentic hammered finish - ✓ 7 results -[272/390] Artisan pickle and fermentation crocks with water seals - ✓ 9 results -[273/390] Natural cork yoga blocks that provide firm support - ✓ 6 results -[274/390] Hand-forged blacksmithing tools for beginner metalworkers - ✓ 1 results -[275/390] Japanese pull saws with fine teeth for precision woodworking - ✓ 6 results -[276/390] Sake from premium rice polished to daiginjo grade - ✓ 1 results -[277/390] Pizza stones made from cordierite for high heat - ✓ 6 results -[278/390] Pasta machines with multiple thickness settings - ✓ 7 results -[279/390] Rolling pins in different materials and lengths - ✓ 6 results -[280/390] Chitarra cutters for making square spaghetti strands - ✓ 1 results -[281/390] Pastry wheels with fluted edges for decorative cuts - ✓ 6 results -[282/390] Dehydrators with multiple trays for making jerky - ✓ 1 results -[283/390] Cheese making kits with rennet and cultures - ✓ 6 results -[284/390] Cold press juicers that extract maximum nutrients - ✓ 7 results -[285/390] Tallow and lard from pastured animals for frying - ✓ 7 results -[286/390] Pattern books with traditional and modern motifs - ✓ 15 results -[287/390] Mason jars in various sizes with two-piece lids - ✓ 7 results -[288/390] Water bath canners for high-acid food preservation - ✓ 1 results -[289/390] Canning labels for dating and identifying preserves - ✓ 1 results -[290/390] Aftershave splashes with witch hazel and alcohol - ✓ 1 results -[291/390] Copper bathtub caddies for reading in the tub - ✓ 1 results -[292/390] Garden kneelers with padded surfaces and handles - ✓ 6 results -[293/390] Natural indigo dye for creating fade patterns on denim - ✓ 6 results -[294/390] Binchotan charcoal for water filtration and odor absorption - ✓ 1 results -[295/390] Vintage film cameras in working condition for analog photography - ✓ 1 results -[296/390] Developing tanks and chemicals for processing black and white film - ✓ 1 results -[297/390] Block printing tools and linoleum blocks for fabric design - ✓ 1 results -[298/390] Mezcal from traditional palenques in Oaxaca Mexico - ✓ 1 results -[299/390] Artisan vermouth from small producers for sophisticated aperitifs - ✓ 1 results -[300/390] Japanese whisky from independent distilleries with limited releases - ✓ 1 results -[301/390] Belgian beer including Trappist ales from monastery breweries - ✓ 6 results -[302/390] Armagnac aged in oak barrels for minimum twenty years - ✓ 1 results -[303/390] Wood burning stoves with soapstone heat retention - ✓ 1 results -[304/390] Vintage light fixtures rewired for modern electrical systems - ✓ 7 results -[305/390] Reclaimed barn wood beams for rustic interior design - ✓ 1 results -[306/390] Cast iron radiators restored and refinished for vintage homes - ✓ 1 results -[307/390] Antique doorknobs and escutcheons in brass and porcelain - ✓ 7 results -[308/390] Subway tiles in classic white with beveled edges - ✓ 8 results -[309/390] Clawfoot bathtubs refinished in original porcelain enamel - ✓ 1 results -[310/390] Farmhouse sinks in fireclay with apron fronts - ✓ 6 results -[311/390] Period-appropriate molding and trim for historic renovations - ✓ 8 results -[312/390] Handmade tiles with Arts and Crafts movement patterns - ✓ 10 results -[313/390] Loose leaf pipe tobacco in classic English and Virginia blends - ✓ 1 results -[314/390] Briar wood pipes with quality stems for pipe smoking - ✓ 1 results -[315/390] Humidors with Spanish cedar lining for cigar storage - ✓ 1 results -[316/390] Premium cigars from Cuban seed tobacco grown in Nicaragua - ✓ 1 results -[317/390] Pipe cleaners and reaming tools for maintenance - ✓ 9 results -[318/390] Tobacco pouches in leather with moisture-retaining linings - ✓ 6 results -[319/390] Cigar cutters with sharp guillotine blades - ✓ 1 results -[320/390] Torch lighters with adjustable flames for even lighting - ✓ 7 results -[321/390] Pipe stands displaying multiple pipes with style - ✓ 1 results -[322/390] Aging rooms and cabinets for maturing pipe tobacco - ✓ 6 results -[323/390] Handmade leather belts with solid brass buckles - ✓ 15 results -[324/390] Suspenders in woven elastic with leather trim details - ✓ 10 results -[325/390] Pocket squares in silk with hand-rolled edges - ✓ 7 results -[326/390] Tie bars and cufflinks in sterling silver or gold - ✓ 10 results -[327/390] Hat brushes and shapers for maintaining fedora crowns - ✓ 9 results -[328/390] Shoe trees made from aromatic cedar wood - ✓ 6 results -[329/390] Boot jacks for removing tall boots without bending - ✓ 6 results -[330/390] Collar stays in metal for crisp shirt presentations - ✓ 1 results -[331/390] Vintage typewriter ribbons for specific models - ✓ 1 results -[332/390] Tailoring supplies including chalk, shears, and measuring tapes - ✓ 14 results -[333/390] Garment bags in canvas for protecting suits during travel - ✓ 8 results -[334/390] Wax sealing kits with custom monogram stamps - ✓ 6 results -[335/390] Bookbinding tools including bone folders and awls - ✓ 8 results -[336/390] Calligraphy nibs in various sizes and flexibility - ✓ 6 results -[337/390] Fountain pen ink in archival quality with rich colors - ✓ 7 results -[338/390] Handmade paper from cotton rag with deckled edges - ✓ 6 results -[339/390] Desk blotters in leather with corner protectors - ✓ 6 results -[340/390] Leather book covers with hand-tooled decorative patterns - ✓ 9 results -[341/390] Letter openers in damascus steel with elegant handles - ✓ 6 results -[342/390] Pasta drying racks that hold multiple portions - ✓ 1 results -[343/390] Gnocchi boards with ridged surfaces for shaping - ✓ 1 results -[344/390] Meat grinders with various plate sizes for sausage - ✓ 1 results -[345/390] Smoking chips in different woods for unique flavors - ✓ 1 results -[346/390] Organ meats including liver and heart from quality sources - ✓ 8 results -[347/390] Collagen powder from grass-fed sources for health - ✓ 15 results -[348/390] Charcuterie from artisan producers with traditional methods - ✓ 15 results -[349/390] Basswood blanks for relief and chip carving - ✓ 6 results -[350/390] Wood carving tools with razor-sharp edges - ✓ 10 results -[351/390] Finishing oils including Danish and tung oil - ✓ 1 results -[352/390] Sandpaper in grits from coarse to ultra-fine - ✓ 7 results -[353/390] Wood stains in oil and water-based formulas - ✓ 8 results -[354/390] Chisels in various widths for mortise and tenon joints - ✓ 7 results -[355/390] Hand planes for smoothing and shaping wood surfaces - ✓ 6 results -[356/390] Marking gauges for precise measurements and layouts - ✓ 7 results -[357/390] Japanese water stones for sharpening edge tools - ✓ 9 results -[358/390] Dust collection systems for workshop cleanliness - ✓ 9 results -[359/390] Leather working stamps for tooling decorative patterns - ✓ 1 results -[360/390] Edge bevelers for finishing leather goods professionally - ✓ 6 results -[361/390] Stitching chisels in various prong configurations - ✓ 1 results -[362/390] Waxed thread in different weights and colors - ✓ 12 results -[363/390] Leather dyes and finishes in authentic tones - ✓ 8 results -[364/390] Rivet setters and snaps for functional closures - ✓ 6 results -[365/390] Skiving knives for thinning leather edges - ✓ 7 results -[366/390] Burnishing tools for polishing cut leather edges - ✓ 7 results -[367/390] Leather cement and contact adhesives for bonding - ✓ 7 results -[368/390] Belt blanks in various widths and thicknesses - ✓ 10 results -[369/390] Embroidery hoops in multiple sizes for hand stitching - ✓ 9 results -[370/390] DMC floss in complete color ranges for cross stitch - ✓ 1 results -[371/390] Sashiko thread and needles for Japanese mending - ✓ 10 results -[372/390] Crewel wool for dimensional embroidery work - ✓ 11 results -[373/390] Pressure canners for preserving low-acid foods safely - ✓ 1 results -[374/390] Aida cloth and evenweave fabric for counted thread - ✓ 6 results -[375/390] Needle threaders for fine embroidery needles - ✓ 10 results -[376/390] Pectin for making jams and jellies that set properly - ✓ 6 results -[377/390] Jar lifters for safely removing hot jars from water - ✓ 1 results -[378/390] Pickle crisp for maintaining vegetable texture - ✓ 1 results -[379/390] Hemp shower curtains that resist mildew naturally - ✓ 1 results -[380/390] Aquaponics systems combining fish and plant growth - ✓ 1 results -[381/390] Wooden bath mats with slats for drainage - ✓ 1 results -[382/390] Worm composting bins for kitchen scrap recycling - ✓ 1 results -[383/390] Pruning shears with bypass cutting action - ✓ 6 results -[384/390] Soil testing kits for pH and nutrient analysis - ✓ 6 results -[385/390] Diatomaceous earth for natural insect barriers - ✓ 7 results -[386/390] Beneficial nematodes for controlling soil pests - ✓ 1 results -[387/390] Companion planting guides for natural pest management - ✓ 1 results -[388/390] Cold frames and row covers extending growing seasons - ✓ 6 results -[389/390] Rain gauges for monitoring garden irrigation needs - ✓ 1 results -[390/390] Soil amendments including compost and worm castings - ✓ 13 results - -================================================================================ -VALIDATION SUMMARY -================================================================================ -Total queries: 390 -Queries with results: 389 (99.7%) -Queries without results: 1 (0.3%) -Queries with errors: 0 (0.0%) - -================================================================================ -QUERIES WITHOUT RESULTS -================================================================================ - -• Grow lights with full spectrum for year-round growing - Error: Request timeout - -Average results per successful query: 8.58