webscout 8.3.4__py3-none-any.whl → 8.3.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of webscout might be problematic. Click here for more details.

Files changed (98) hide show
  1. webscout/AIutel.py +52 -1016
  2. webscout/Bard.py +12 -6
  3. webscout/DWEBS.py +66 -57
  4. webscout/Provider/AISEARCH/PERPLEXED_search.py +214 -0
  5. webscout/Provider/AISEARCH/__init__.py +11 -10
  6. webscout/Provider/AISEARCH/felo_search.py +7 -3
  7. webscout/Provider/AISEARCH/scira_search.py +2 -0
  8. webscout/Provider/AISEARCH/stellar_search.py +53 -8
  9. webscout/Provider/Deepinfra.py +13 -1
  10. webscout/Provider/Flowith.py +6 -1
  11. webscout/Provider/GithubChat.py +1 -0
  12. webscout/Provider/GptOss.py +207 -0
  13. webscout/Provider/Kimi.py +445 -0
  14. webscout/Provider/Netwrck.py +3 -6
  15. webscout/Provider/OPENAI/README.md +2 -1
  16. webscout/Provider/OPENAI/TogetherAI.py +12 -8
  17. webscout/Provider/OPENAI/TwoAI.py +94 -1
  18. webscout/Provider/OPENAI/__init__.py +4 -4
  19. webscout/Provider/OPENAI/copilot.py +20 -4
  20. webscout/Provider/OPENAI/deepinfra.py +12 -0
  21. webscout/Provider/OPENAI/e2b.py +60 -8
  22. webscout/Provider/OPENAI/flowith.py +4 -3
  23. webscout/Provider/OPENAI/generate_api_key.py +48 -0
  24. webscout/Provider/OPENAI/gptoss.py +288 -0
  25. webscout/Provider/OPENAI/kimi.py +469 -0
  26. webscout/Provider/OPENAI/netwrck.py +8 -12
  27. webscout/Provider/OPENAI/refact.py +274 -0
  28. webscout/Provider/OPENAI/scirachat.py +4 -0
  29. webscout/Provider/OPENAI/textpollinations.py +11 -10
  30. webscout/Provider/OPENAI/toolbaz.py +1 -0
  31. webscout/Provider/OPENAI/venice.py +1 -0
  32. webscout/Provider/Perplexitylabs.py +163 -147
  33. webscout/Provider/Qodo.py +30 -6
  34. webscout/Provider/TTI/__init__.py +1 -0
  35. webscout/Provider/TTI/bing.py +14 -2
  36. webscout/Provider/TTI/together.py +11 -9
  37. webscout/Provider/TTI/venice.py +368 -0
  38. webscout/Provider/TTS/README.md +0 -1
  39. webscout/Provider/TTS/__init__.py +0 -1
  40. webscout/Provider/TTS/base.py +479 -159
  41. webscout/Provider/TTS/deepgram.py +409 -156
  42. webscout/Provider/TTS/elevenlabs.py +425 -111
  43. webscout/Provider/TTS/freetts.py +317 -140
  44. webscout/Provider/TTS/gesserit.py +192 -128
  45. webscout/Provider/TTS/murfai.py +248 -113
  46. webscout/Provider/TTS/openai_fm.py +347 -129
  47. webscout/Provider/TTS/speechma.py +620 -586
  48. webscout/Provider/TextPollinationsAI.py +11 -10
  49. webscout/Provider/TogetherAI.py +12 -4
  50. webscout/Provider/TwoAI.py +96 -2
  51. webscout/Provider/TypliAI.py +33 -27
  52. webscout/Provider/UNFINISHED/VercelAIGateway.py +339 -0
  53. webscout/Provider/UNFINISHED/fetch_together_models.py +6 -11
  54. webscout/Provider/Venice.py +1 -0
  55. webscout/Provider/WiseCat.py +18 -20
  56. webscout/Provider/__init__.py +2 -96
  57. webscout/Provider/cerebras.py +83 -33
  58. webscout/Provider/copilot.py +42 -23
  59. webscout/Provider/scira_chat.py +4 -0
  60. webscout/Provider/toolbaz.py +6 -10
  61. webscout/Provider/typefully.py +1 -11
  62. webscout/__init__.py +3 -15
  63. webscout/auth/__init__.py +19 -4
  64. webscout/auth/api_key_manager.py +189 -189
  65. webscout/auth/auth_system.py +25 -40
  66. webscout/auth/config.py +105 -6
  67. webscout/auth/database.py +377 -22
  68. webscout/auth/models.py +185 -130
  69. webscout/auth/request_processing.py +175 -11
  70. webscout/auth/routes.py +99 -2
  71. webscout/auth/server.py +9 -2
  72. webscout/auth/simple_logger.py +236 -0
  73. webscout/conversation.py +22 -20
  74. webscout/sanitize.py +1078 -0
  75. webscout/scout/README.md +20 -23
  76. webscout/scout/core/crawler.py +125 -38
  77. webscout/scout/core/scout.py +26 -5
  78. webscout/version.py +1 -1
  79. webscout/webscout_search.py +13 -6
  80. webscout/webscout_search_async.py +10 -8
  81. webscout/yep_search.py +13 -5
  82. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/METADATA +10 -149
  83. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/RECORD +88 -87
  84. webscout/Provider/Glider.py +0 -225
  85. webscout/Provider/OPENAI/README_AUTOPROXY.md +0 -238
  86. webscout/Provider/OPENAI/c4ai.py +0 -394
  87. webscout/Provider/OPENAI/glider.py +0 -330
  88. webscout/Provider/OPENAI/typegpt.py +0 -368
  89. webscout/Provider/OPENAI/uncovrAI.py +0 -477
  90. webscout/Provider/TTS/sthir.py +0 -94
  91. webscout/Provider/WritingMate.py +0 -273
  92. webscout/Provider/typegpt.py +0 -284
  93. webscout/Provider/uncovr.py +0 -333
  94. /webscout/Provider/{samurai.py → UNFINISHED/samurai.py} +0 -0
  95. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/WHEEL +0 -0
  96. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/entry_points.txt +0 -0
  97. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/licenses/LICENSE.md +0 -0
  98. {webscout-8.3.4.dist-info → webscout-8.3.6.dist-info}/top_level.txt +0 -0
webscout/auth/routes.py CHANGED
@@ -41,6 +41,7 @@ from .request_processing import (
41
41
  handle_streaming_response, handle_non_streaming_response
42
42
  )
43
43
  from .auth_system import get_auth_components
44
+ from .simple_logger import request_logger
44
45
  from webscout.DWEBS import GoogleSearch
45
46
  from webscout.yep_search import YepSearch
46
47
  from webscout.webscout_search import WEBS
@@ -165,6 +166,7 @@ class Api:
165
166
  self._register_chat_routes()
166
167
  self._register_auth_routes()
167
168
  self._register_websearch_routes()
169
+ self._register_monitoring_routes()
168
170
 
169
171
  def _register_model_routes(self):
170
172
  """Register model listing routes."""
@@ -241,6 +243,7 @@ class Api:
241
243
  }
242
244
  )
243
245
  async def chat_completions(
246
+ request: Request,
244
247
  chat_request: ChatCompletionRequest = Body(...)
245
248
  ):
246
249
  """Handle chat completion requests with comprehensive error handling."""
@@ -271,11 +274,39 @@ class Api:
271
274
  # Prepare parameters for provider
272
275
  params = prepare_provider_params(chat_request, model_name, processed_messages)
273
276
 
277
+ # Extract client IP address
278
+ client_ip = request.client.host if request.client else "unknown"
279
+ if "x-forwarded-for" in request.headers:
280
+ client_ip = request.headers["x-forwarded-for"].split(",")[0].strip()
281
+ elif "x-real-ip" in request.headers:
282
+ client_ip = request.headers["x-real-ip"]
283
+
284
+ # Extract question from messages (last user message)
285
+ question = ""
286
+ for msg in reversed(processed_messages):
287
+ if msg.get("role") == "user":
288
+ content = msg.get("content", "")
289
+ if isinstance(content, str):
290
+ question = content
291
+ elif isinstance(content, list) and content:
292
+ # Handle content with multiple parts (text, images, etc.)
293
+ for part in content:
294
+ if isinstance(part, dict) and part.get("type") == "text":
295
+ question = part.get("text", "")
296
+ break
297
+ break
298
+
274
299
  # Handle streaming vs non-streaming
275
300
  if chat_request.stream:
276
- return await handle_streaming_response(provider, params, request_id)
301
+ return await handle_streaming_response(
302
+ provider, params, request_id, client_ip, question, model_name, start_time,
303
+ provider_class.__name__, request
304
+ )
277
305
  else:
278
- return await handle_non_streaming_response(provider, params, request_id, start_time)
306
+ return await handle_non_streaming_response(
307
+ provider, params, request_id, start_time, client_ip, question, model_name,
308
+ provider_class.__name__, request
309
+ )
279
310
 
280
311
  except APIError:
281
312
  # Re-raise API errors as-is
@@ -565,3 +596,69 @@ class Api:
565
596
  "error": f"Search request failed: {msg}",
566
597
  "footer": github_footer
567
598
  }
599
+
600
+ def _register_monitoring_routes(self):
601
+ """Register monitoring and analytics routes for no-auth mode."""
602
+
603
+ @self.app.get(
604
+ "/monitor/requests",
605
+ tags=["Monitoring"],
606
+ description="Get recent API requests (no-auth mode only)"
607
+ )
608
+ async def get_recent_requests(limit: int = Query(10, description="Number of recent requests to fetch")):
609
+ """Get recent API requests for monitoring."""
610
+ if AppConfig.auth_required:
611
+ return {"error": "Monitoring is only available in no-auth mode"}
612
+
613
+ try:
614
+ return await request_logger.get_recent_requests(limit)
615
+ except Exception as e:
616
+ return {"error": f"Failed to fetch requests: {str(e)}"}
617
+
618
+ @self.app.get(
619
+ "/monitor/stats",
620
+ tags=["Monitoring"],
621
+ description="Get API usage statistics (no-auth mode only)"
622
+ )
623
+ async def get_api_stats():
624
+ """Get API usage statistics."""
625
+ if AppConfig.auth_required:
626
+ return {"error": "Monitoring is only available in no-auth mode"}
627
+
628
+ try:
629
+ return await request_logger.get_stats()
630
+ except Exception as e:
631
+ return {"error": f"Failed to fetch stats: {str(e)}"}
632
+
633
+ @self.app.get(
634
+ "/monitor/health",
635
+ tags=["Monitoring"],
636
+ description="Health check with database status"
637
+ )
638
+ async def enhanced_health_check():
639
+ """Enhanced health check including database connectivity."""
640
+ try:
641
+ # Check database connectivity
642
+ db_status = "disconnected"
643
+ if request_logger.supabase_client:
644
+ try:
645
+ # Try a simple query to check connectivity
646
+ result = request_logger.supabase_client.table("api_requests").select("id").limit(1).execute()
647
+ db_status = "connected"
648
+ except Exception as e:
649
+ db_status = f"error: {str(e)[:100]}"
650
+
651
+ return {
652
+ "status": "healthy",
653
+ "database": db_status,
654
+ "auth_required": AppConfig.auth_required,
655
+ "rate_limit_enabled": AppConfig.rate_limit_enabled,
656
+ "request_logging_enabled": AppConfig.request_logging_enabled,
657
+ "timestamp": datetime.now(timezone.utc).isoformat()
658
+ }
659
+ except Exception as e:
660
+ return {
661
+ "status": "unhealthy",
662
+ "error": str(e),
663
+ "timestamp": datetime.now(timezone.utc).isoformat()
664
+ }
webscout/auth/server.py CHANGED
@@ -40,8 +40,15 @@ logger = Logger(
40
40
  fmt=LogFormat.DEFAULT
41
41
  )
42
42
 
43
- # Global configuration instance
44
- config = ServerConfig()
43
+ # Global configuration instance - lazy initialization
44
+ config = None
45
+
46
+ def get_config() -> ServerConfig:
47
+ """Get or create the global configuration instance."""
48
+ global config
49
+ if config is None:
50
+ config = ServerConfig()
51
+ return config
45
52
 
46
53
 
47
54
  def create_app():
@@ -0,0 +1,236 @@
1
+ """
2
+ Simple database logging for no-auth mode.
3
+ Logs API requests directly to Supabase without authentication.
4
+ """
5
+
6
+ import os
7
+ import uuid
8
+ import asyncio
9
+ from datetime import datetime, timezone
10
+ from typing import Optional, Dict, Any
11
+ import json
12
+
13
+ from webscout.Litlogger import Logger, LogLevel, LogFormat, ConsoleHandler
14
+ import sys
15
+
16
+ # Setup logger
17
+ logger = Logger(
18
+ name="webscout.api.simple_db",
19
+ level=LogLevel.INFO,
20
+ handlers=[ConsoleHandler(stream=sys.stdout)],
21
+ fmt=LogFormat.DEFAULT
22
+ )
23
+
24
+ try:
25
+ from supabase import create_client, Client
26
+ SUPABASE_AVAILABLE = True
27
+ except ImportError:
28
+ logger.warning("Supabase not available. Install with: pip install supabase")
29
+ SUPABASE_AVAILABLE = False
30
+
31
+
32
+ class SimpleRequestLogger:
33
+ """Simple request logger for no-auth mode."""
34
+
35
+ def __init__(self):
36
+ self.supabase_client: Optional[Client] = None
37
+ self.initialize_supabase()
38
+
39
+ def initialize_supabase(self):
40
+ """Initialize Supabase client if credentials are available."""
41
+ if not SUPABASE_AVAILABLE:
42
+ logger.warning("Supabase package not installed. Request logging disabled.")
43
+ return
44
+
45
+ supabase_url = os.getenv("SUPABASE_URL")
46
+ supabase_key = os.getenv("SUPABASE_ANON_KEY")
47
+
48
+ if supabase_url and supabase_key:
49
+ try:
50
+ self.supabase_client = create_client(supabase_url, supabase_key)
51
+ logger.info("Supabase client initialized for request logging")
52
+
53
+ except Exception as e:
54
+ logger.error(f"Failed to initialize Supabase client: {e}")
55
+ self.supabase_client = None
56
+ else:
57
+ logger.info("Supabase credentials not found. Request logging disabled.")
58
+
59
+ async def log_request(
60
+ self,
61
+ request_id: str,
62
+ ip_address: str,
63
+ model: str,
64
+ question: str,
65
+ answer: str,
66
+ provider: Optional[str] = None,
67
+ request_time: Optional[datetime] = None,
68
+ response_time: Optional[datetime] = None,
69
+ processing_time_ms: Optional[float] = None,
70
+ tokens_used: Optional[int] = None,
71
+ error: Optional[str] = None,
72
+ user_agent: Optional[str] = None
73
+ ) -> bool:
74
+ """
75
+ Log API request details to Supabase.
76
+
77
+ Args:
78
+ request_id: Unique identifier for the request
79
+ ip_address: Client IP address
80
+ model: Model used for the request
81
+ question: User's question/prompt
82
+ answer: AI's response
83
+ provider: Provider used (e.g., ChatGPT, Claude, etc.)
84
+ request_time: When the request was received
85
+ response_time: When the response was sent
86
+ processing_time_ms: Processing time in milliseconds
87
+ tokens_used: Number of tokens consumed
88
+ error: Error message if any
89
+ user_agent: User agent string
90
+
91
+ Returns:
92
+ bool: True if logged successfully, False otherwise
93
+ """
94
+ if not self.supabase_client:
95
+ # Still log to console for debugging
96
+ logger.info(f"Request {request_id}: {model} - {question[:100]}...")
97
+ return False
98
+
99
+ if not request_time:
100
+ request_time = datetime.now(timezone.utc)
101
+
102
+ if not response_time:
103
+ response_time = datetime.now(timezone.utc)
104
+
105
+ try:
106
+ data = {
107
+ "request_id": request_id,
108
+ "ip_address": ip_address,
109
+ "model": model,
110
+ "provider": provider or "unknown",
111
+ "question": question[:2000] if question else "", # Truncate long questions
112
+ "answer": answer[:5000] if answer else "", # Truncate long answers
113
+ "request_time": request_time.isoformat(),
114
+ "response_time": response_time.isoformat(),
115
+ "processing_time_ms": processing_time_ms,
116
+ "tokens_used": tokens_used,
117
+ "error": error[:1000] if error else None, # Truncate long errors
118
+ "user_agent": user_agent[:500] if user_agent else None,
119
+ "created_at": datetime.now(timezone.utc).isoformat()
120
+ }
121
+
122
+ result = self.supabase_client.table("api_requests").insert(data).execute()
123
+
124
+ if result.data:
125
+ logger.info(f"✅ Request {request_id} logged to database")
126
+ return True
127
+ else:
128
+ logger.error(f"❌ Failed to log request {request_id}: No data returned")
129
+ return False
130
+
131
+ except Exception as e:
132
+ logger.error(f"❌ Failed to log request {request_id}: {e}")
133
+ return False
134
+
135
+ async def get_recent_requests(self, limit: int = 10) -> Dict[str, Any]:
136
+ """Get recent API requests for monitoring."""
137
+ if not self.supabase_client:
138
+ return {"error": "Database not available", "requests": []}
139
+
140
+ try:
141
+ result = self.supabase_client.table("api_requests")\
142
+ .select("request_id, ip_address, model, provider, created_at, processing_time_ms, error")\
143
+ .order("created_at", desc=True)\
144
+ .limit(limit)\
145
+ .execute()
146
+
147
+ return {
148
+ "requests": result.data if result.data else [],
149
+ "count": len(result.data) if result.data else 0
150
+ }
151
+
152
+ except Exception as e:
153
+ logger.error(f"Failed to get recent requests: {e}")
154
+ return {"error": str(e), "requests": []}
155
+
156
+ async def get_stats(self) -> Dict[str, Any]:
157
+ """Get basic statistics about API usage."""
158
+ if not self.supabase_client:
159
+ return {"error": "Database not available"}
160
+
161
+ try:
162
+ # Get total requests today
163
+ today = datetime.now(timezone.utc).date().isoformat()
164
+
165
+ today_requests = self.supabase_client.table("api_requests")\
166
+ .select("request_id", count="exact")\
167
+ .gte("created_at", f"{today}T00:00:00Z")\
168
+ .execute()
169
+
170
+ # Get requests by model (last 100)
171
+ model_requests = self.supabase_client.table("api_requests")\
172
+ .select("model")\
173
+ .order("created_at", desc=True)\
174
+ .limit(100)\
175
+ .execute()
176
+
177
+ model_counts = {}
178
+ if model_requests.data:
179
+ for req in model_requests.data:
180
+ model = req.get("model", "unknown")
181
+ model_counts[model] = model_counts.get(model, 0) + 1
182
+
183
+ return {
184
+ "today_requests": today_requests.count if hasattr(today_requests, 'count') else 0,
185
+ "model_usage": model_counts,
186
+ "available": True
187
+ }
188
+
189
+ except Exception as e:
190
+ logger.error(f"Failed to get stats: {e}")
191
+ return {"error": str(e), "available": False}
192
+
193
+
194
+ # Global instance
195
+ request_logger = SimpleRequestLogger()
196
+
197
+
198
+ async def log_api_request(
199
+ request_id: str,
200
+ ip_address: str,
201
+ model: str,
202
+ question: str,
203
+ answer: str,
204
+ **kwargs
205
+ ) -> bool:
206
+ """Convenience function to log API requests."""
207
+ return await request_logger.log_request(
208
+ request_id=request_id,
209
+ ip_address=ip_address,
210
+ model=model,
211
+ question=question,
212
+ answer=answer,
213
+ **kwargs
214
+ )
215
+
216
+
217
+ def get_client_ip(request) -> str:
218
+ """Extract client IP address from request."""
219
+ # Check for X-Forwarded-For header (common with proxies/load balancers)
220
+ forwarded_for = request.headers.get("X-Forwarded-For")
221
+ if forwarded_for:
222
+ # Take the first IP in the chain
223
+ return forwarded_for.split(",")[0].strip()
224
+
225
+ # Check for X-Real-IP header
226
+ real_ip = request.headers.get("X-Real-IP")
227
+ if real_ip:
228
+ return real_ip.strip()
229
+
230
+ # Fall back to direct client IP
231
+ return getattr(request.client, "host", "unknown")
232
+
233
+
234
+ def generate_request_id() -> str:
235
+ """Generate a unique request ID."""
236
+ return str(uuid.uuid4())
webscout/conversation.py CHANGED
@@ -165,29 +165,19 @@ class Conversation:
165
165
  ))
166
166
 
167
167
  def _compress_history(self) -> None:
168
- """Compress history when it exceeds threshold."""
168
+ """Delete old history when it exceeds threshold."""
169
169
  if len(self.messages) > self.compression_threshold:
170
- # Keep recent messages and summarize older ones
171
- keep_recent = 100 # Adjust based on needs
172
- self.messages = (
173
- [self._summarize_messages(self.messages[:-keep_recent])] +
174
- self.messages[-keep_recent:]
175
- )
176
-
177
- def _summarize_messages(self, messages: List[Message]) -> Message:
178
- """Create a summary message from older messages."""
179
- return Message(
180
- role="system",
181
- content="[History Summary] Previous conversation summarized for context",
182
- metadata={"summarized_count": len(messages)}
183
- )
170
+ # Remove oldest messages, keep only the most recent ones
171
+ self.messages = self.messages[-self.compression_threshold:]
172
+
173
+ # _summarize_messages removed
184
174
 
185
175
  def gen_complete_prompt(self, prompt: str, intro: Optional[str] = None) -> str:
186
176
  """Generate complete prompt with enhanced context management."""
187
177
  if not self.status:
188
178
  return prompt
189
179
 
190
- intro = intro or self.intro
180
+ intro = intro or self.intro or ""
191
181
 
192
182
  # Add tool information if available
193
183
  tools_description = self.get_tools_description()
@@ -260,6 +250,7 @@ Your goal is to assist the user effectively. Analyze each query and choose one o
260
250
 
261
251
  def _trim_chat_history(self, chat_history: str, intro: str) -> str:
262
252
  """Trim chat history with improved token management."""
253
+ intro = intro or ""
263
254
  total_length = len(intro) + len(chat_history)
264
255
 
265
256
  if total_length > self.history_offset:
@@ -273,20 +264,31 @@ Your goal is to assist the user effectively. Analyze each query and choose one o
273
264
  return chat_history
274
265
 
275
266
  def add_message(self, role: str, content: str, metadata: Optional[Dict[str, Any]] = None) -> None:
276
- """Add a message with enhanced validation and metadata support."""
267
+ """Add a message with enhanced validation and metadata support. Deletes oldest messages if total word count exceeds max_tokens_to_sample."""
277
268
  try:
278
269
  role = role.lower() # Normalize role to lowercase
279
270
  if not self.validate_message(role, content):
280
271
  raise MessageValidationError("Invalid message role or content")
281
272
 
273
+ # Calculate total word count in history
274
+ def total_word_count(messages):
275
+ return sum(len(msg.content.split()) for msg in messages)
276
+
277
+ # Remove oldest messages until total word count is below limit
278
+ temp_messages = self.messages.copy()
279
+ while temp_messages and (total_word_count(temp_messages) + len(content.split()) > self.max_tokens_to_sample):
280
+ temp_messages.pop(0)
281
+
282
+ self.messages = temp_messages
283
+
282
284
  message = Message(role=role, content=content, metadata=metadata or {})
283
285
  self.messages.append(message)
284
-
286
+
285
287
  if self.file and self.update_file:
286
288
  self._append_to_file(message)
287
-
289
+
288
290
  self._compress_history()
289
-
291
+
290
292
  except Exception as e:
291
293
  raise ConversationError(f"Failed to add message: {str(e)}") from e
292
294