webscout 8.2.7__py3-none-any.whl → 8.2.8__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.
- webscout/AIauto.py +1 -1
- webscout/AIutel.py +298 -249
- webscout/Extra/Act.md +309 -0
- webscout/Extra/GitToolkit/__init__.py +10 -0
- webscout/Extra/GitToolkit/gitapi/README.md +110 -0
- webscout/Extra/GitToolkit/gitapi/__init__.py +12 -0
- webscout/Extra/GitToolkit/gitapi/repository.py +195 -0
- webscout/Extra/GitToolkit/gitapi/user.py +96 -0
- webscout/Extra/GitToolkit/gitapi/utils.py +62 -0
- webscout/Extra/YTToolkit/README.md +375 -0
- webscout/Extra/YTToolkit/YTdownloader.py +957 -0
- webscout/Extra/YTToolkit/__init__.py +3 -0
- webscout/Extra/YTToolkit/transcriber.py +476 -0
- webscout/Extra/YTToolkit/ytapi/README.md +44 -0
- webscout/Extra/YTToolkit/ytapi/__init__.py +6 -0
- webscout/Extra/YTToolkit/ytapi/channel.py +307 -0
- webscout/Extra/YTToolkit/ytapi/errors.py +13 -0
- webscout/Extra/YTToolkit/ytapi/extras.py +118 -0
- webscout/Extra/YTToolkit/ytapi/https.py +88 -0
- webscout/Extra/YTToolkit/ytapi/patterns.py +61 -0
- webscout/Extra/YTToolkit/ytapi/playlist.py +59 -0
- webscout/Extra/YTToolkit/ytapi/pool.py +8 -0
- webscout/Extra/YTToolkit/ytapi/query.py +40 -0
- webscout/Extra/YTToolkit/ytapi/stream.py +63 -0
- webscout/Extra/YTToolkit/ytapi/utils.py +62 -0
- webscout/Extra/YTToolkit/ytapi/video.py +232 -0
- webscout/Extra/__init__.py +7 -0
- webscout/Extra/autocoder/__init__.py +9 -0
- webscout/Extra/autocoder/autocoder.py +1105 -0
- webscout/Extra/autocoder/autocoder_utiles.py +332 -0
- webscout/Extra/gguf.md +430 -0
- webscout/Extra/gguf.py +684 -0
- webscout/Extra/tempmail/README.md +488 -0
- webscout/Extra/tempmail/__init__.py +28 -0
- webscout/Extra/tempmail/async_utils.py +141 -0
- webscout/Extra/tempmail/base.py +161 -0
- webscout/Extra/tempmail/cli.py +187 -0
- webscout/Extra/tempmail/emailnator.py +84 -0
- webscout/Extra/tempmail/mail_tm.py +361 -0
- webscout/Extra/tempmail/temp_mail_io.py +292 -0
- webscout/Extra/weather.md +281 -0
- webscout/Extra/weather.py +194 -0
- webscout/Extra/weather_ascii.py +76 -0
- webscout/Litlogger/Readme.md +175 -0
- webscout/Litlogger/__init__.py +67 -0
- webscout/Litlogger/core/__init__.py +6 -0
- webscout/Litlogger/core/level.py +23 -0
- webscout/Litlogger/core/logger.py +165 -0
- webscout/Litlogger/handlers/__init__.py +12 -0
- webscout/Litlogger/handlers/console.py +33 -0
- webscout/Litlogger/handlers/file.py +143 -0
- webscout/Litlogger/handlers/network.py +173 -0
- webscout/Litlogger/styles/__init__.py +7 -0
- webscout/Litlogger/styles/colors.py +249 -0
- webscout/Litlogger/styles/formats.py +458 -0
- webscout/Litlogger/styles/text.py +87 -0
- webscout/Litlogger/utils/__init__.py +6 -0
- webscout/Litlogger/utils/detectors.py +153 -0
- webscout/Litlogger/utils/formatters.py +200 -0
- webscout/Provider/AI21.py +177 -0
- webscout/Provider/AISEARCH/DeepFind.py +254 -0
- webscout/Provider/AISEARCH/Perplexity.py +359 -0
- webscout/Provider/AISEARCH/README.md +279 -0
- webscout/Provider/AISEARCH/__init__.py +9 -0
- webscout/Provider/AISEARCH/felo_search.py +228 -0
- webscout/Provider/AISEARCH/genspark_search.py +350 -0
- webscout/Provider/AISEARCH/hika_search.py +198 -0
- webscout/Provider/AISEARCH/iask_search.py +436 -0
- webscout/Provider/AISEARCH/monica_search.py +246 -0
- webscout/Provider/AISEARCH/scira_search.py +324 -0
- webscout/Provider/AISEARCH/webpilotai_search.py +281 -0
- webscout/Provider/Aitopia.py +316 -0
- webscout/Provider/AllenAI.py +440 -0
- webscout/Provider/Andi.py +228 -0
- webscout/Provider/Blackboxai.py +673 -0
- webscout/Provider/ChatGPTClone.py +237 -0
- webscout/Provider/ChatGPTGratis.py +194 -0
- webscout/Provider/ChatSandbox.py +342 -0
- webscout/Provider/Cloudflare.py +324 -0
- webscout/Provider/Cohere.py +208 -0
- webscout/Provider/Deepinfra.py +340 -0
- webscout/Provider/ExaAI.py +261 -0
- webscout/Provider/ExaChat.py +358 -0
- webscout/Provider/Flowith.py +217 -0
- webscout/Provider/FreeGemini.py +250 -0
- webscout/Provider/Gemini.py +169 -0
- webscout/Provider/GithubChat.py +370 -0
- webscout/Provider/GizAI.py +295 -0
- webscout/Provider/Glider.py +225 -0
- webscout/Provider/Groq.py +801 -0
- webscout/Provider/HF_space/__init__.py +0 -0
- webscout/Provider/HF_space/qwen_qwen2.py +206 -0
- webscout/Provider/HeckAI.py +285 -0
- webscout/Provider/HuggingFaceChat.py +469 -0
- webscout/Provider/Hunyuan.py +283 -0
- webscout/Provider/Jadve.py +291 -0
- webscout/Provider/Koboldai.py +384 -0
- webscout/Provider/LambdaChat.py +411 -0
- webscout/Provider/Llama3.py +259 -0
- webscout/Provider/MCPCore.py +315 -0
- webscout/Provider/Marcus.py +198 -0
- webscout/Provider/Nemotron.py +218 -0
- webscout/Provider/Netwrck.py +270 -0
- webscout/Provider/OLLAMA.py +396 -0
- webscout/Provider/OPENAI/BLACKBOXAI.py +735 -0
- webscout/Provider/OPENAI/Cloudflare.py +378 -0
- webscout/Provider/OPENAI/FreeGemini.py +282 -0
- webscout/Provider/OPENAI/NEMOTRON.py +244 -0
- webscout/Provider/OPENAI/README.md +1253 -0
- webscout/Provider/OPENAI/__init__.py +36 -0
- webscout/Provider/OPENAI/ai4chat.py +293 -0
- webscout/Provider/OPENAI/api.py +810 -0
- webscout/Provider/OPENAI/base.py +249 -0
- webscout/Provider/OPENAI/c4ai.py +373 -0
- webscout/Provider/OPENAI/chatgpt.py +556 -0
- webscout/Provider/OPENAI/chatgptclone.py +488 -0
- webscout/Provider/OPENAI/chatsandbox.py +172 -0
- webscout/Provider/OPENAI/deepinfra.py +319 -0
- webscout/Provider/OPENAI/e2b.py +1356 -0
- webscout/Provider/OPENAI/exaai.py +411 -0
- webscout/Provider/OPENAI/exachat.py +443 -0
- webscout/Provider/OPENAI/flowith.py +162 -0
- webscout/Provider/OPENAI/freeaichat.py +359 -0
- webscout/Provider/OPENAI/glider.py +323 -0
- webscout/Provider/OPENAI/groq.py +361 -0
- webscout/Provider/OPENAI/heckai.py +307 -0
- webscout/Provider/OPENAI/llmchatco.py +335 -0
- webscout/Provider/OPENAI/mcpcore.py +383 -0
- webscout/Provider/OPENAI/multichat.py +376 -0
- webscout/Provider/OPENAI/netwrck.py +356 -0
- webscout/Provider/OPENAI/opkfc.py +496 -0
- webscout/Provider/OPENAI/scirachat.py +471 -0
- webscout/Provider/OPENAI/sonus.py +303 -0
- webscout/Provider/OPENAI/standardinput.py +433 -0
- webscout/Provider/OPENAI/textpollinations.py +339 -0
- webscout/Provider/OPENAI/toolbaz.py +413 -0
- webscout/Provider/OPENAI/typefully.py +355 -0
- webscout/Provider/OPENAI/typegpt.py +358 -0
- webscout/Provider/OPENAI/uncovrAI.py +462 -0
- webscout/Provider/OPENAI/utils.py +307 -0
- webscout/Provider/OPENAI/venice.py +425 -0
- webscout/Provider/OPENAI/wisecat.py +381 -0
- webscout/Provider/OPENAI/writecream.py +163 -0
- webscout/Provider/OPENAI/x0gpt.py +378 -0
- webscout/Provider/OPENAI/yep.py +356 -0
- webscout/Provider/OpenGPT.py +209 -0
- webscout/Provider/Openai.py +496 -0
- webscout/Provider/PI.py +429 -0
- webscout/Provider/Perplexitylabs.py +415 -0
- webscout/Provider/QwenLM.py +254 -0
- webscout/Provider/Reka.py +214 -0
- webscout/Provider/StandardInput.py +290 -0
- webscout/Provider/TTI/AiForce/README.md +159 -0
- webscout/Provider/TTI/AiForce/__init__.py +22 -0
- webscout/Provider/TTI/AiForce/async_aiforce.py +224 -0
- webscout/Provider/TTI/AiForce/sync_aiforce.py +245 -0
- webscout/Provider/TTI/FreeAIPlayground/README.md +99 -0
- webscout/Provider/TTI/FreeAIPlayground/__init__.py +9 -0
- webscout/Provider/TTI/FreeAIPlayground/async_freeaiplayground.py +181 -0
- webscout/Provider/TTI/FreeAIPlayground/sync_freeaiplayground.py +180 -0
- webscout/Provider/TTI/ImgSys/README.md +174 -0
- webscout/Provider/TTI/ImgSys/__init__.py +23 -0
- webscout/Provider/TTI/ImgSys/async_imgsys.py +202 -0
- webscout/Provider/TTI/ImgSys/sync_imgsys.py +195 -0
- webscout/Provider/TTI/MagicStudio/README.md +101 -0
- webscout/Provider/TTI/MagicStudio/__init__.py +2 -0
- webscout/Provider/TTI/MagicStudio/async_magicstudio.py +111 -0
- webscout/Provider/TTI/MagicStudio/sync_magicstudio.py +109 -0
- webscout/Provider/TTI/Nexra/README.md +155 -0
- webscout/Provider/TTI/Nexra/__init__.py +22 -0
- webscout/Provider/TTI/Nexra/async_nexra.py +286 -0
- webscout/Provider/TTI/Nexra/sync_nexra.py +258 -0
- webscout/Provider/TTI/PollinationsAI/README.md +146 -0
- webscout/Provider/TTI/PollinationsAI/__init__.py +23 -0
- webscout/Provider/TTI/PollinationsAI/async_pollinations.py +311 -0
- webscout/Provider/TTI/PollinationsAI/sync_pollinations.py +265 -0
- webscout/Provider/TTI/README.md +128 -0
- webscout/Provider/TTI/__init__.py +12 -0
- webscout/Provider/TTI/aiarta/README.md +134 -0
- webscout/Provider/TTI/aiarta/__init__.py +2 -0
- webscout/Provider/TTI/aiarta/async_aiarta.py +482 -0
- webscout/Provider/TTI/aiarta/sync_aiarta.py +440 -0
- webscout/Provider/TTI/artbit/README.md +100 -0
- webscout/Provider/TTI/artbit/__init__.py +22 -0
- webscout/Provider/TTI/artbit/async_artbit.py +155 -0
- webscout/Provider/TTI/artbit/sync_artbit.py +148 -0
- webscout/Provider/TTI/fastflux/README.md +129 -0
- webscout/Provider/TTI/fastflux/__init__.py +22 -0
- webscout/Provider/TTI/fastflux/async_fastflux.py +261 -0
- webscout/Provider/TTI/fastflux/sync_fastflux.py +252 -0
- webscout/Provider/TTI/huggingface/README.md +114 -0
- webscout/Provider/TTI/huggingface/__init__.py +22 -0
- webscout/Provider/TTI/huggingface/async_huggingface.py +199 -0
- webscout/Provider/TTI/huggingface/sync_huggingface.py +195 -0
- webscout/Provider/TTI/piclumen/README.md +161 -0
- webscout/Provider/TTI/piclumen/__init__.py +23 -0
- webscout/Provider/TTI/piclumen/async_piclumen.py +268 -0
- webscout/Provider/TTI/piclumen/sync_piclumen.py +233 -0
- webscout/Provider/TTI/pixelmuse/README.md +79 -0
- webscout/Provider/TTI/pixelmuse/__init__.py +4 -0
- webscout/Provider/TTI/pixelmuse/async_pixelmuse.py +249 -0
- webscout/Provider/TTI/pixelmuse/sync_pixelmuse.py +182 -0
- webscout/Provider/TTI/talkai/README.md +139 -0
- webscout/Provider/TTI/talkai/__init__.py +4 -0
- webscout/Provider/TTI/talkai/async_talkai.py +229 -0
- webscout/Provider/TTI/talkai/sync_talkai.py +207 -0
- webscout/Provider/TTS/README.md +192 -0
- webscout/Provider/TTS/__init__.py +9 -0
- webscout/Provider/TTS/base.py +159 -0
- webscout/Provider/TTS/deepgram.py +156 -0
- webscout/Provider/TTS/elevenlabs.py +111 -0
- webscout/Provider/TTS/gesserit.py +128 -0
- webscout/Provider/TTS/murfai.py +113 -0
- webscout/Provider/TTS/parler.py +111 -0
- webscout/Provider/TTS/speechma.py +580 -0
- webscout/Provider/TTS/sthir.py +94 -0
- webscout/Provider/TTS/streamElements.py +333 -0
- webscout/Provider/TTS/utils.py +280 -0
- webscout/Provider/TeachAnything.py +229 -0
- webscout/Provider/TextPollinationsAI.py +308 -0
- webscout/Provider/TwoAI.py +280 -0
- webscout/Provider/TypliAI.py +305 -0
- webscout/Provider/UNFINISHED/ChatHub.py +209 -0
- webscout/Provider/UNFINISHED/Youchat.py +330 -0
- webscout/Provider/UNFINISHED/liner_api_request.py +263 -0
- webscout/Provider/UNFINISHED/oivscode.py +351 -0
- webscout/Provider/UNFINISHED/test_lmarena.py +119 -0
- webscout/Provider/Venice.py +258 -0
- webscout/Provider/VercelAI.py +253 -0
- webscout/Provider/WiseCat.py +233 -0
- webscout/Provider/WrDoChat.py +370 -0
- webscout/Provider/Writecream.py +246 -0
- webscout/Provider/WritingMate.py +269 -0
- webscout/Provider/__init__.py +172 -0
- webscout/Provider/ai4chat.py +149 -0
- webscout/Provider/akashgpt.py +335 -0
- webscout/Provider/asksteve.py +220 -0
- webscout/Provider/cerebras.py +290 -0
- webscout/Provider/chatglm.py +215 -0
- webscout/Provider/cleeai.py +213 -0
- webscout/Provider/copilot.py +425 -0
- webscout/Provider/elmo.py +283 -0
- webscout/Provider/freeaichat.py +285 -0
- webscout/Provider/geminiapi.py +208 -0
- webscout/Provider/granite.py +235 -0
- webscout/Provider/hermes.py +266 -0
- webscout/Provider/julius.py +223 -0
- webscout/Provider/koala.py +170 -0
- webscout/Provider/learnfastai.py +325 -0
- webscout/Provider/llama3mitril.py +215 -0
- webscout/Provider/llmchat.py +258 -0
- webscout/Provider/llmchatco.py +306 -0
- webscout/Provider/lmarena.py +198 -0
- webscout/Provider/meta.py +801 -0
- webscout/Provider/multichat.py +364 -0
- webscout/Provider/samurai.py +223 -0
- webscout/Provider/scira_chat.py +299 -0
- webscout/Provider/scnet.py +243 -0
- webscout/Provider/searchchat.py +292 -0
- webscout/Provider/sonus.py +258 -0
- webscout/Provider/talkai.py +194 -0
- webscout/Provider/toolbaz.py +353 -0
- webscout/Provider/turboseek.py +266 -0
- webscout/Provider/typefully.py +202 -0
- webscout/Provider/typegpt.py +289 -0
- webscout/Provider/uncovr.py +368 -0
- webscout/Provider/x0gpt.py +299 -0
- webscout/Provider/yep.py +389 -0
- webscout/__init__.py +4 -2
- webscout/cli.py +3 -28
- webscout/conversation.py +35 -35
- webscout/litagent/Readme.md +276 -0
- webscout/litagent/__init__.py +29 -0
- webscout/litagent/agent.py +455 -0
- webscout/litagent/constants.py +60 -0
- webscout/litprinter/__init__.py +59 -0
- webscout/scout/README.md +402 -0
- webscout/scout/__init__.py +8 -0
- webscout/scout/core/__init__.py +7 -0
- webscout/scout/core/crawler.py +140 -0
- webscout/scout/core/scout.py +568 -0
- webscout/scout/core/search_result.py +96 -0
- webscout/scout/core/text_analyzer.py +63 -0
- webscout/scout/core/text_utils.py +277 -0
- webscout/scout/core/web_analyzer.py +52 -0
- webscout/scout/element.py +460 -0
- webscout/scout/parsers/__init__.py +69 -0
- webscout/scout/parsers/html5lib_parser.py +172 -0
- webscout/scout/parsers/html_parser.py +236 -0
- webscout/scout/parsers/lxml_parser.py +178 -0
- webscout/scout/utils.py +37 -0
- webscout/swiftcli/Readme.md +323 -0
- webscout/swiftcli/__init__.py +95 -0
- webscout/swiftcli/core/__init__.py +7 -0
- webscout/swiftcli/core/cli.py +297 -0
- webscout/swiftcli/core/context.py +104 -0
- webscout/swiftcli/core/group.py +241 -0
- webscout/swiftcli/decorators/__init__.py +28 -0
- webscout/swiftcli/decorators/command.py +221 -0
- webscout/swiftcli/decorators/options.py +220 -0
- webscout/swiftcli/decorators/output.py +252 -0
- webscout/swiftcli/exceptions.py +21 -0
- webscout/swiftcli/plugins/__init__.py +9 -0
- webscout/swiftcli/plugins/base.py +135 -0
- webscout/swiftcli/plugins/manager.py +262 -0
- webscout/swiftcli/utils/__init__.py +59 -0
- webscout/swiftcli/utils/formatting.py +252 -0
- webscout/swiftcli/utils/parsing.py +267 -0
- webscout/version.py +1 -1
- webscout/webscout_search.py +2 -182
- webscout/webscout_search_async.py +1 -179
- webscout/zeroart/README.md +89 -0
- webscout/zeroart/__init__.py +135 -0
- webscout/zeroart/base.py +66 -0
- webscout/zeroart/effects.py +101 -0
- webscout/zeroart/fonts.py +1239 -0
- {webscout-8.2.7.dist-info → webscout-8.2.8.dist-info}/METADATA +115 -60
- webscout-8.2.8.dist-info/RECORD +334 -0
- {webscout-8.2.7.dist-info → webscout-8.2.8.dist-info}/WHEEL +1 -1
- webscout-8.2.7.dist-info/RECORD +0 -26
- {webscout-8.2.7.dist-info → webscout-8.2.8.dist-info}/entry_points.txt +0 -0
- {webscout-8.2.7.dist-info → webscout-8.2.8.dist-info}/licenses/LICENSE.md +0 -0
- {webscout-8.2.7.dist-info → webscout-8.2.8.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Utility functions for parsing and validating command-line arguments."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import json
|
|
5
|
+
import yaml
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional, Union, Type
|
|
8
|
+
|
|
9
|
+
from ..exceptions import BadParameter, UsageError
|
|
10
|
+
|
|
11
|
+
def parse_args(args: List[str]) -> Dict[str, Any]:
|
|
12
|
+
"""
|
|
13
|
+
Parse command line arguments into a dictionary.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
args: List of command line arguments
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Dictionary of parsed arguments
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
>>> parse_args(['--name', 'test', '--flag', '-n', '42'])
|
|
23
|
+
{'name': 'test', 'flag': True, 'n': '42'}
|
|
24
|
+
"""
|
|
25
|
+
parsed = {}
|
|
26
|
+
i = 0
|
|
27
|
+
while i < len(args):
|
|
28
|
+
arg = args[i]
|
|
29
|
+
|
|
30
|
+
# Handle flags/options
|
|
31
|
+
if arg.startswith('-'):
|
|
32
|
+
key = arg.lstrip('-').replace('-', '_')
|
|
33
|
+
|
|
34
|
+
# Check if next arg is a value or another flag
|
|
35
|
+
if i + 1 >= len(args) or args[i + 1].startswith('-'):
|
|
36
|
+
parsed[key] = True # Flag without value
|
|
37
|
+
else:
|
|
38
|
+
parsed[key] = args[i + 1]
|
|
39
|
+
i += 1
|
|
40
|
+
else:
|
|
41
|
+
# Positional argument
|
|
42
|
+
parsed[f'arg{len([k for k in parsed.keys() if k.startswith("arg")])}'] = arg
|
|
43
|
+
|
|
44
|
+
i += 1
|
|
45
|
+
|
|
46
|
+
return parsed
|
|
47
|
+
|
|
48
|
+
def validate_required(
|
|
49
|
+
params: Dict[str, Any],
|
|
50
|
+
required: List[str]
|
|
51
|
+
) -> None:
|
|
52
|
+
"""
|
|
53
|
+
Validate required parameters are present.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
params: Parameter dictionary
|
|
57
|
+
required: List of required parameter names
|
|
58
|
+
|
|
59
|
+
Raises:
|
|
60
|
+
UsageError: If required parameter is missing
|
|
61
|
+
"""
|
|
62
|
+
missing = [p for p in required if p not in params]
|
|
63
|
+
if missing:
|
|
64
|
+
raise UsageError(f"Missing required parameters: {', '.join(missing)}")
|
|
65
|
+
|
|
66
|
+
def convert_type(
|
|
67
|
+
value: str,
|
|
68
|
+
type_: Type,
|
|
69
|
+
param_name: str
|
|
70
|
+
) -> Any:
|
|
71
|
+
"""
|
|
72
|
+
Convert string value to specified type.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
value: String value to convert
|
|
76
|
+
type_: Target type
|
|
77
|
+
param_name: Parameter name for error messages
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Converted value
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
BadParameter: If conversion fails
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
if type_ == bool:
|
|
87
|
+
return value.lower() in ('true', 't', 'yes', 'y', '1')
|
|
88
|
+
return type_(value)
|
|
89
|
+
except (ValueError, TypeError):
|
|
90
|
+
raise BadParameter(
|
|
91
|
+
f"Invalid value for {param_name}: {value} (expected {type_.__name__})"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def validate_choice(
|
|
95
|
+
value: Any,
|
|
96
|
+
choices: List[Any],
|
|
97
|
+
param_name: str,
|
|
98
|
+
case_sensitive: bool = True
|
|
99
|
+
) -> None:
|
|
100
|
+
"""
|
|
101
|
+
Validate value is one of allowed choices.
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
value: Value to validate
|
|
105
|
+
choices: List of allowed choices
|
|
106
|
+
param_name: Parameter name for error messages
|
|
107
|
+
case_sensitive: Whether to do case-sensitive comparison
|
|
108
|
+
|
|
109
|
+
Raises:
|
|
110
|
+
BadParameter: If value not in choices
|
|
111
|
+
"""
|
|
112
|
+
if not case_sensitive and isinstance(value, str):
|
|
113
|
+
if value.lower() not in [str(c).lower() for c in choices]:
|
|
114
|
+
raise BadParameter(
|
|
115
|
+
f"Invalid choice for {param_name}: {value} "
|
|
116
|
+
f"(choose from {', '.join(str(c) for c in choices)})"
|
|
117
|
+
)
|
|
118
|
+
elif value not in choices:
|
|
119
|
+
raise BadParameter(
|
|
120
|
+
f"Invalid choice for {param_name}: {value} "
|
|
121
|
+
f"(choose from {', '.join(str(c) for c in choices)})"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def load_config_file(
|
|
125
|
+
path: Union[str, Path],
|
|
126
|
+
format: str = 'auto',
|
|
127
|
+
required: bool = True
|
|
128
|
+
) -> Dict[str, Any]:
|
|
129
|
+
"""
|
|
130
|
+
Load configuration from file.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
path: Path to config file
|
|
134
|
+
format: File format (json, yaml, or auto)
|
|
135
|
+
required: Whether file is required
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
Configuration dictionary
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
UsageError: If required file not found or invalid format
|
|
142
|
+
"""
|
|
143
|
+
path = Path(os.path.expanduser(path))
|
|
144
|
+
|
|
145
|
+
if not path.exists():
|
|
146
|
+
if required:
|
|
147
|
+
raise UsageError(f"Config file not found: {path}")
|
|
148
|
+
return {}
|
|
149
|
+
|
|
150
|
+
# Auto-detect format from extension
|
|
151
|
+
if format == 'auto':
|
|
152
|
+
format = path.suffix.lstrip('.').lower()
|
|
153
|
+
if format not in ('json', 'yaml', 'yml'):
|
|
154
|
+
raise UsageError(f"Unsupported config format: {format}")
|
|
155
|
+
|
|
156
|
+
try:
|
|
157
|
+
with open(path) as f:
|
|
158
|
+
if format == 'json':
|
|
159
|
+
return json.load(f)
|
|
160
|
+
elif format in ('yaml', 'yml'):
|
|
161
|
+
return yaml.safe_load(f)
|
|
162
|
+
else:
|
|
163
|
+
raise UsageError(f"Unsupported config format: {format}")
|
|
164
|
+
except Exception as e:
|
|
165
|
+
raise UsageError(f"Error loading config file: {str(e)}")
|
|
166
|
+
|
|
167
|
+
def parse_key_value(
|
|
168
|
+
value: str,
|
|
169
|
+
separator: str = '='
|
|
170
|
+
) -> tuple:
|
|
171
|
+
"""
|
|
172
|
+
Parse key-value string.
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
value: String in format "key=value"
|
|
176
|
+
separator: Key-value separator
|
|
177
|
+
|
|
178
|
+
Returns:
|
|
179
|
+
Tuple of (key, value)
|
|
180
|
+
|
|
181
|
+
Raises:
|
|
182
|
+
BadParameter: If string not in key=value format
|
|
183
|
+
"""
|
|
184
|
+
try:
|
|
185
|
+
key, value = value.split(separator, 1)
|
|
186
|
+
return key.strip(), value.strip()
|
|
187
|
+
except ValueError:
|
|
188
|
+
raise BadParameter(
|
|
189
|
+
f"Invalid key-value pair: {value} (expected format: key{separator}value)"
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
def parse_list(
|
|
193
|
+
value: str,
|
|
194
|
+
separator: str = ','
|
|
195
|
+
) -> List[str]:
|
|
196
|
+
"""
|
|
197
|
+
Parse comma-separated list.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
value: Comma-separated string
|
|
201
|
+
separator: List item separator
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
List of strings
|
|
205
|
+
"""
|
|
206
|
+
return [x.strip() for x in value.split(separator) if x.strip()]
|
|
207
|
+
|
|
208
|
+
def parse_dict(
|
|
209
|
+
value: str,
|
|
210
|
+
item_separator: str = ',',
|
|
211
|
+
key_value_separator: str = '='
|
|
212
|
+
) -> Dict[str, str]:
|
|
213
|
+
"""
|
|
214
|
+
Parse dictionary string.
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
value: String in format "key1=value1,key2=value2"
|
|
218
|
+
item_separator: Separator between items
|
|
219
|
+
key_value_separator: Separator between keys and values
|
|
220
|
+
|
|
221
|
+
Returns:
|
|
222
|
+
Dictionary of key-value pairs
|
|
223
|
+
|
|
224
|
+
Example:
|
|
225
|
+
>>> parse_dict("name=test,count=42")
|
|
226
|
+
{'name': 'test', 'count': '42'}
|
|
227
|
+
"""
|
|
228
|
+
result = {}
|
|
229
|
+
if not value:
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
items = parse_list(value, item_separator)
|
|
233
|
+
for item in items:
|
|
234
|
+
key, value = parse_key_value(item, key_value_separator)
|
|
235
|
+
result[key] = value
|
|
236
|
+
|
|
237
|
+
return result
|
|
238
|
+
|
|
239
|
+
def get_env_var(
|
|
240
|
+
name: str,
|
|
241
|
+
type_: Type = str,
|
|
242
|
+
required: bool = False,
|
|
243
|
+
default: Any = None
|
|
244
|
+
) -> Any:
|
|
245
|
+
"""
|
|
246
|
+
Get and validate environment variable.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
name: Environment variable name
|
|
250
|
+
type_: Expected type
|
|
251
|
+
required: Whether variable is required
|
|
252
|
+
default: Default value if not set
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Environment variable value
|
|
256
|
+
|
|
257
|
+
Raises:
|
|
258
|
+
UsageError: If required variable not set
|
|
259
|
+
"""
|
|
260
|
+
value = os.environ.get(name)
|
|
261
|
+
|
|
262
|
+
if value is None:
|
|
263
|
+
if required:
|
|
264
|
+
raise UsageError(f"Required environment variable not set: {name}")
|
|
265
|
+
return default
|
|
266
|
+
|
|
267
|
+
return convert_type(value, type_, name)
|
webscout/version.py
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
__version__ = "8.2.
|
|
1
|
+
__version__ = "8.2.8"
|
|
2
2
|
__prog__ = "webscout"
|
webscout/webscout_search.py
CHANGED
|
@@ -56,13 +56,7 @@ class WEBS:
|
|
|
56
56
|
"firefox133", "firefox135",
|
|
57
57
|
) # fmt: skip
|
|
58
58
|
_impersonates_os = ("android", "ios", "linux", "macos", "windows")
|
|
59
|
-
|
|
60
|
-
"gpt-4o-mini": "gpt-4o-mini",
|
|
61
|
-
"llama-3.3-70b": "meta-llama/Llama-3.3-70B-Instruct-Turbo",
|
|
62
|
-
"claude-3-haiku": "claude-3-haiku-20240307",
|
|
63
|
-
"o3-mini": "o3-mini",
|
|
64
|
-
"mistral-small-3": "mistralai/Mistral-Small-24B-Instruct-2501",
|
|
65
|
-
}
|
|
59
|
+
|
|
66
60
|
|
|
67
61
|
def __init__(
|
|
68
62
|
self,
|
|
@@ -119,11 +113,6 @@ class WEBS:
|
|
|
119
113
|
self.sleep_timestamp = 0.0
|
|
120
114
|
|
|
121
115
|
self._exception_event = Event()
|
|
122
|
-
self._chat_messages: list[dict[str, str]] = []
|
|
123
|
-
self._chat_tokens_count = 0
|
|
124
|
-
self._chat_vqd: str = ""
|
|
125
|
-
self._chat_vqd_hash: str = ""
|
|
126
|
-
self._chat_xfe: str = ""
|
|
127
116
|
|
|
128
117
|
def __enter__(self) -> WEBS:
|
|
129
118
|
return self
|
|
@@ -203,176 +192,7 @@ class WEBS:
|
|
|
203
192
|
resp_content = self._get_url("GET", "https://duckduckgo.com", params={"q": keywords}).content
|
|
204
193
|
return _extract_vqd(resp_content, keywords)
|
|
205
194
|
|
|
206
|
-
def chat_yield(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30, max_retries: int = 3) -> Iterator[str]:
|
|
207
|
-
"""Initiates a chat session with webscout AI.
|
|
208
195
|
|
|
209
|
-
Args:
|
|
210
|
-
keywords (str): The initial message or question to send to the AI.
|
|
211
|
-
model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
|
|
212
|
-
"o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
|
|
213
|
-
timeout (int): Timeout value for the HTTP client. Defaults to 30.
|
|
214
|
-
max_retries (int): Maximum number of retry attempts for rate limited requests. Defaults to 3.
|
|
215
|
-
|
|
216
|
-
Yields:
|
|
217
|
-
str: Chunks of the response from the AI.
|
|
218
|
-
"""
|
|
219
|
-
# Get Cloudflare Turnstile token
|
|
220
|
-
def get_turnstile_token():
|
|
221
|
-
try:
|
|
222
|
-
# Visit the DuckDuckGo chat page to get the Turnstile token
|
|
223
|
-
resp_content = self._get_url(
|
|
224
|
-
method="GET",
|
|
225
|
-
url="https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
|
|
226
|
-
).content
|
|
227
|
-
|
|
228
|
-
# Extract the Turnstile token if available
|
|
229
|
-
if b'cf-turnstile-response' in resp_content:
|
|
230
|
-
token = resp_content.split(b'cf-turnstile-response="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
|
|
231
|
-
return token
|
|
232
|
-
return ""
|
|
233
|
-
except Exception:
|
|
234
|
-
return ""
|
|
235
|
-
|
|
236
|
-
# x-fe-version
|
|
237
|
-
if not self._chat_xfe:
|
|
238
|
-
resp_content = self._get_url(
|
|
239
|
-
method="GET",
|
|
240
|
-
url="https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
|
|
241
|
-
).content
|
|
242
|
-
try:
|
|
243
|
-
xfe1 = resp_content.split(b'__DDG_BE_VERSION__="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
|
|
244
|
-
xfe2 = resp_content.split(b'__DDG_FE_CHAT_HASH__="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
|
|
245
|
-
self._chat_xfe = f"{xfe1}-{xfe2}"
|
|
246
|
-
except Exception as ex:
|
|
247
|
-
raise WebscoutE(
|
|
248
|
-
f"chat_yield() Error to get _chat_xfe: {type(ex).__name__}: {ex}"
|
|
249
|
-
) from ex
|
|
250
|
-
# vqd
|
|
251
|
-
if not self._chat_vqd:
|
|
252
|
-
resp = self._get_url(
|
|
253
|
-
method="GET", url="https://duckduckgo.com/duckchat/v1/status", headers={"x-vqd-accept": "1"}
|
|
254
|
-
)
|
|
255
|
-
self._chat_vqd = resp.headers.get("x-vqd-4", "")
|
|
256
|
-
self._chat_vqd_hash = resp.headers.get("x-vqd-hash-1", "")
|
|
257
|
-
|
|
258
|
-
self._chat_messages.append({"role": "user", "content": keywords})
|
|
259
|
-
self._chat_tokens_count += max(len(keywords) // 4, 1) # approximate number of tokens
|
|
260
|
-
if model not in self._chat_models:
|
|
261
|
-
warnings.warn(f"{model=} is unavailable. Using 'gpt-4o-mini'", stacklevel=1)
|
|
262
|
-
model = "gpt-4o-mini"
|
|
263
|
-
|
|
264
|
-
# Get Cloudflare Turnstile token
|
|
265
|
-
turnstile_token = get_turnstile_token()
|
|
266
|
-
|
|
267
|
-
json_data = {
|
|
268
|
-
"model": self._chat_models[model],
|
|
269
|
-
"messages": self._chat_messages,
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
# Add Turnstile token if available
|
|
273
|
-
if turnstile_token:
|
|
274
|
-
json_data["cf-turnstile-response"] = turnstile_token
|
|
275
|
-
|
|
276
|
-
# Enhanced headers to better mimic a real browser
|
|
277
|
-
chat_headers = {
|
|
278
|
-
"x-fe-version": self._chat_xfe,
|
|
279
|
-
"x-vqd-4": self._chat_vqd,
|
|
280
|
-
"x-vqd-hash-1": "",
|
|
281
|
-
"Accept": "text/event-stream",
|
|
282
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
283
|
-
"Cache-Control": "no-cache",
|
|
284
|
-
"Content-Type": "application/json",
|
|
285
|
-
"DNT": "1",
|
|
286
|
-
"Origin": "https://duckduckgo.com",
|
|
287
|
-
"Referer": "https://duckduckgo.com/",
|
|
288
|
-
"Sec-Fetch-Dest": "empty",
|
|
289
|
-
"Sec-Fetch-Mode": "cors",
|
|
290
|
-
"Sec-Fetch-Site": "same-origin",
|
|
291
|
-
"User-Agent": self.client.headers.get("User-Agent", "")
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
# Retry logic for rate limited requests
|
|
295
|
-
retry_count = 0
|
|
296
|
-
while retry_count <= max_retries:
|
|
297
|
-
try:
|
|
298
|
-
resp = self._get_url(
|
|
299
|
-
method="POST",
|
|
300
|
-
url="https://duckduckgo.com/duckchat/v1/chat",
|
|
301
|
-
headers=chat_headers,
|
|
302
|
-
json=json_data,
|
|
303
|
-
timeout=timeout,
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
self._chat_vqd = resp.headers.get("x-vqd-4", "")
|
|
307
|
-
self._chat_vqd_hash = resp.headers.get("x-vqd-hash-1", "")
|
|
308
|
-
chunks = []
|
|
309
|
-
|
|
310
|
-
# curl_cffi uses iter_content instead of stream
|
|
311
|
-
for chunk in resp.iter_content(chunk_size=1024):
|
|
312
|
-
lines = chunk.split(b"data:")
|
|
313
|
-
for line in lines:
|
|
314
|
-
if line := line.strip():
|
|
315
|
-
if line == b"[DONE]":
|
|
316
|
-
break
|
|
317
|
-
if line == b"[DONE][LIMIT_CONVERSATION]":
|
|
318
|
-
raise ConversationLimitException("ERR_CONVERSATION_LIMIT")
|
|
319
|
-
try:
|
|
320
|
-
x = json_loads(line)
|
|
321
|
-
if isinstance(x, dict):
|
|
322
|
-
if x.get("action") == "error":
|
|
323
|
-
err_message = x.get("type", "")
|
|
324
|
-
if x.get("status") == 429:
|
|
325
|
-
raise (
|
|
326
|
-
ConversationLimitException(err_message)
|
|
327
|
-
if err_message == "ERR_CONVERSATION_LIMIT"
|
|
328
|
-
else RatelimitE(err_message)
|
|
329
|
-
)
|
|
330
|
-
raise WebscoutE(err_message)
|
|
331
|
-
elif message := x.get("message"):
|
|
332
|
-
chunks.append(message)
|
|
333
|
-
yield message
|
|
334
|
-
except Exception as e:
|
|
335
|
-
# Skip invalid JSON data
|
|
336
|
-
continue
|
|
337
|
-
|
|
338
|
-
# If we get here, the request was successful
|
|
339
|
-
result = "".join(chunks)
|
|
340
|
-
self._chat_messages.append({"role": "assistant", "content": result})
|
|
341
|
-
self._chat_tokens_count += len(result)
|
|
342
|
-
return
|
|
343
|
-
|
|
344
|
-
except RatelimitE as ex:
|
|
345
|
-
retry_count += 1
|
|
346
|
-
if retry_count > max_retries:
|
|
347
|
-
raise WebscoutE(f"chat_yield() Rate limit exceeded after {max_retries} retries: {ex}") from ex
|
|
348
|
-
|
|
349
|
-
# Get a fresh Turnstile token for the retry
|
|
350
|
-
turnstile_token = get_turnstile_token()
|
|
351
|
-
if turnstile_token:
|
|
352
|
-
json_data["cf-turnstile-response"] = turnstile_token
|
|
353
|
-
|
|
354
|
-
# Exponential backoff
|
|
355
|
-
sleep_time = 2 ** retry_count
|
|
356
|
-
sleep(sleep_time)
|
|
357
|
-
|
|
358
|
-
except Exception as ex:
|
|
359
|
-
raise WebscoutE(f"chat_yield() {type(ex).__name__}: {ex}") from ex
|
|
360
|
-
|
|
361
|
-
def chat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30, max_retries: int = 3) -> str:
|
|
362
|
-
"""Initiates a chat session with webscout AI.
|
|
363
|
-
|
|
364
|
-
Args:
|
|
365
|
-
keywords (str): The initial message or question to send to the AI.
|
|
366
|
-
model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
|
|
367
|
-
"o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
|
|
368
|
-
timeout (int): Timeout value for the HTTP client. Defaults to 30.
|
|
369
|
-
max_retries (int): Maximum number of retry attempts for rate limited requests. Defaults to 3.
|
|
370
|
-
|
|
371
|
-
Returns:
|
|
372
|
-
str: The response from the AI.
|
|
373
|
-
"""
|
|
374
|
-
answer_generator = self.chat_yield(keywords, model, timeout, max_retries)
|
|
375
|
-
return "".join(answer_generator)
|
|
376
196
|
|
|
377
197
|
def text(
|
|
378
198
|
self,
|
|
@@ -1361,4 +1181,4 @@ class WEBS:
|
|
|
1361
1181
|
"visibility_m": hour.get("visibility"),
|
|
1362
1182
|
})
|
|
1363
1183
|
|
|
1364
|
-
return formatted_data
|
|
1184
|
+
return formatted_data
|
|
@@ -103,11 +103,6 @@ class AsyncWEBS:
|
|
|
103
103
|
self.sleep_timestamp = 0.0
|
|
104
104
|
|
|
105
105
|
self._exception_event = asyncio.Event()
|
|
106
|
-
self._chat_messages: List[Dict[str, str]] = []
|
|
107
|
-
self._chat_tokens_count = 0
|
|
108
|
-
self._chat_vqd: str = ""
|
|
109
|
-
self._chat_vqd_hash: str = ""
|
|
110
|
-
self._chat_xfe: str = ""
|
|
111
106
|
|
|
112
107
|
async def __aenter__(self) -> AsyncWEBS:
|
|
113
108
|
return self
|
|
@@ -189,179 +184,6 @@ class AsyncWEBS:
|
|
|
189
184
|
resp_content = (await self._get_url("GET", "https://duckduckgo.com", params={"q": keywords})).content
|
|
190
185
|
return _extract_vqd(resp_content, keywords)
|
|
191
186
|
|
|
192
|
-
async def achat_yield(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30, max_retries: int = 3) -> AsyncIterator[str]:
|
|
193
|
-
"""Initiates an async chat session with webscout AI.
|
|
194
|
-
|
|
195
|
-
Args:
|
|
196
|
-
keywords (str): The initial message or question to send to the AI.
|
|
197
|
-
model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
|
|
198
|
-
"o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
|
|
199
|
-
timeout (int): Timeout value for the HTTP client. Defaults to 30.
|
|
200
|
-
max_retries (int): Maximum number of retry attempts for rate limited requests. Defaults to 3.
|
|
201
|
-
|
|
202
|
-
Yields:
|
|
203
|
-
str: Chunks of the response from the AI.
|
|
204
|
-
"""
|
|
205
|
-
# Get Cloudflare Turnstile token
|
|
206
|
-
async def get_turnstile_token():
|
|
207
|
-
try:
|
|
208
|
-
# Visit the DuckDuckGo chat page to get the Turnstile token
|
|
209
|
-
resp_content = (await self._get_url(
|
|
210
|
-
method="GET",
|
|
211
|
-
url="https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
|
|
212
|
-
)).content
|
|
213
|
-
|
|
214
|
-
# Extract the Turnstile token if available
|
|
215
|
-
if b'cf-turnstile-response' in resp_content:
|
|
216
|
-
token = resp_content.split(b'cf-turnstile-response="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
|
|
217
|
-
return token
|
|
218
|
-
return ""
|
|
219
|
-
except Exception:
|
|
220
|
-
return ""
|
|
221
|
-
|
|
222
|
-
# x-fe-version
|
|
223
|
-
if not self._chat_xfe:
|
|
224
|
-
resp_content = (await self._get_url(
|
|
225
|
-
method="GET",
|
|
226
|
-
url="https://duckduckgo.com/?q=DuckDuckGo+AI+Chat&ia=chat&duckai=1",
|
|
227
|
-
)).content
|
|
228
|
-
try:
|
|
229
|
-
xfe1 = resp_content.split(b'__DDG_BE_VERSION__="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
|
|
230
|
-
xfe2 = resp_content.split(b'__DDG_FE_CHAT_HASH__="', maxsplit=1)[1].split(b'"', maxsplit=1)[0].decode()
|
|
231
|
-
self._chat_xfe = f"{xfe1}-{xfe2}"
|
|
232
|
-
except Exception as ex:
|
|
233
|
-
raise WebscoutE(
|
|
234
|
-
f"achat_yield() Error to get _chat_xfe: {type(ex).__name__}: {ex}"
|
|
235
|
-
) from ex
|
|
236
|
-
# vqd
|
|
237
|
-
if not self._chat_vqd:
|
|
238
|
-
resp = await self._get_url(
|
|
239
|
-
method="GET", url="https://duckduckgo.com/duckchat/v1/status", headers={"x-vqd-accept": "1"}
|
|
240
|
-
)
|
|
241
|
-
self._chat_vqd = resp.headers.get("x-vqd-4", "")
|
|
242
|
-
self._chat_vqd_hash = resp.headers.get("x-vqd-hash-1", "")
|
|
243
|
-
|
|
244
|
-
self._chat_messages.append({"role": "user", "content": keywords})
|
|
245
|
-
self._chat_tokens_count += max(len(keywords) // 4, 1) # approximate number of tokens
|
|
246
|
-
if model not in self._chat_models:
|
|
247
|
-
warnings.warn(f"{model=} is unavailable. Using 'gpt-4o-mini'", stacklevel=1)
|
|
248
|
-
model = "gpt-4o-mini"
|
|
249
|
-
|
|
250
|
-
# Get Cloudflare Turnstile token
|
|
251
|
-
turnstile_token = await get_turnstile_token()
|
|
252
|
-
|
|
253
|
-
json_data = {
|
|
254
|
-
"model": self._chat_models[model],
|
|
255
|
-
"messages": self._chat_messages,
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
# Add Turnstile token if available
|
|
259
|
-
if turnstile_token:
|
|
260
|
-
json_data["cf-turnstile-response"] = turnstile_token
|
|
261
|
-
|
|
262
|
-
# Enhanced headers to better mimic a real browser
|
|
263
|
-
chat_headers = {
|
|
264
|
-
"x-fe-version": self._chat_xfe,
|
|
265
|
-
"x-vqd-4": self._chat_vqd,
|
|
266
|
-
"x-vqd-hash-1": "",
|
|
267
|
-
"Accept": "text/event-stream",
|
|
268
|
-
"Accept-Language": "en-US,en;q=0.9",
|
|
269
|
-
"Cache-Control": "no-cache",
|
|
270
|
-
"Content-Type": "application/json",
|
|
271
|
-
"DNT": "1",
|
|
272
|
-
"Origin": "https://duckduckgo.com",
|
|
273
|
-
"Referer": "https://duckduckgo.com/",
|
|
274
|
-
"Sec-Fetch-Dest": "empty",
|
|
275
|
-
"Sec-Fetch-Mode": "cors",
|
|
276
|
-
"Sec-Fetch-Site": "same-origin",
|
|
277
|
-
"User-Agent": self.client.headers.get("User-Agent", "")
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
# Retry logic for rate limited requests
|
|
281
|
-
retry_count = 0
|
|
282
|
-
while retry_count <= max_retries:
|
|
283
|
-
try:
|
|
284
|
-
resp = await self._get_url(
|
|
285
|
-
method="POST",
|
|
286
|
-
url="https://duckduckgo.com/duckchat/v1/chat",
|
|
287
|
-
headers=chat_headers,
|
|
288
|
-
json=json_data,
|
|
289
|
-
timeout=timeout,
|
|
290
|
-
)
|
|
291
|
-
|
|
292
|
-
self._chat_vqd = resp.headers.get("x-vqd-4", "")
|
|
293
|
-
self._chat_vqd_hash = resp.headers.get("x-vqd-hash-1", "")
|
|
294
|
-
chunks = []
|
|
295
|
-
|
|
296
|
-
# curl_cffi uses aiter_content instead of aiter_bytes
|
|
297
|
-
async for chunk in resp.aiter_content(chunk_size=1024):
|
|
298
|
-
lines = chunk.split(b"data:")
|
|
299
|
-
for line in lines:
|
|
300
|
-
if line := line.strip():
|
|
301
|
-
if line == b"[DONE]":
|
|
302
|
-
break
|
|
303
|
-
if line == b"[DONE][LIMIT_CONVERSATION]":
|
|
304
|
-
raise ConversationLimitException("ERR_CONVERSATION_LIMIT")
|
|
305
|
-
try:
|
|
306
|
-
x = json_loads(line)
|
|
307
|
-
if isinstance(x, dict):
|
|
308
|
-
if x.get("action") == "error":
|
|
309
|
-
err_message = x.get("type", "")
|
|
310
|
-
if x.get("status") == 429:
|
|
311
|
-
raise (
|
|
312
|
-
ConversationLimitException(err_message)
|
|
313
|
-
if err_message == "ERR_CONVERSATION_LIMIT"
|
|
314
|
-
else RatelimitE(err_message)
|
|
315
|
-
)
|
|
316
|
-
raise WebscoutE(err_message)
|
|
317
|
-
elif message := x.get("message"):
|
|
318
|
-
chunks.append(message)
|
|
319
|
-
yield message
|
|
320
|
-
except Exception:
|
|
321
|
-
# Skip invalid JSON data
|
|
322
|
-
continue
|
|
323
|
-
|
|
324
|
-
# If we get here, the request was successful
|
|
325
|
-
result = "".join(chunks)
|
|
326
|
-
self._chat_messages.append({"role": "assistant", "content": result})
|
|
327
|
-
self._chat_tokens_count += len(result)
|
|
328
|
-
return
|
|
329
|
-
|
|
330
|
-
except RatelimitE as ex:
|
|
331
|
-
retry_count += 1
|
|
332
|
-
if retry_count > max_retries:
|
|
333
|
-
raise WebscoutE(f"achat_yield() Rate limit exceeded after {max_retries} retries: {ex}") from ex
|
|
334
|
-
|
|
335
|
-
# Get a fresh Turnstile token for the retry
|
|
336
|
-
turnstile_token = await get_turnstile_token()
|
|
337
|
-
if turnstile_token:
|
|
338
|
-
json_data["cf-turnstile-response"] = turnstile_token
|
|
339
|
-
|
|
340
|
-
# Exponential backoff
|
|
341
|
-
sleep_time = 2 ** retry_count
|
|
342
|
-
await asyncio.sleep(sleep_time)
|
|
343
|
-
|
|
344
|
-
except Exception as ex:
|
|
345
|
-
raise WebscoutE(f"achat_yield() {type(ex).__name__}: {ex}") from ex
|
|
346
|
-
|
|
347
|
-
async def achat(self, keywords: str, model: str = "gpt-4o-mini", timeout: int = 30, max_retries: int = 3) -> str:
|
|
348
|
-
"""Initiates an async chat session with webscout AI.
|
|
349
|
-
|
|
350
|
-
Args:
|
|
351
|
-
keywords (str): The initial message or question to send to the AI.
|
|
352
|
-
model (str): The model to use: "gpt-4o-mini", "llama-3.3-70b", "claude-3-haiku",
|
|
353
|
-
"o3-mini", "mistral-small-3". Defaults to "gpt-4o-mini".
|
|
354
|
-
timeout (int): Timeout value for the HTTP client. Defaults to 30.
|
|
355
|
-
max_retries (int): Maximum number of retry attempts for rate limited requests. Defaults to 3.
|
|
356
|
-
|
|
357
|
-
Returns:
|
|
358
|
-
str: The response from the AI.
|
|
359
|
-
"""
|
|
360
|
-
chunks = []
|
|
361
|
-
async for chunk in self.achat_yield(keywords, model, timeout, max_retries):
|
|
362
|
-
chunks.append(chunk)
|
|
363
|
-
return "".join(chunks)
|
|
364
|
-
|
|
365
187
|
async def atext(
|
|
366
188
|
self,
|
|
367
189
|
keywords: str,
|
|
@@ -829,4 +651,4 @@ class AsyncWEBS:
|
|
|
829
651
|
TimeoutE: Inherits from WebscoutE, raised for API request timeouts.
|
|
830
652
|
"""
|
|
831
653
|
# These methods are not implemented in the async version yet
|
|
832
|
-
raise NotImplementedError("aweather method is not implemented yet")
|
|
654
|
+
raise NotImplementedError("aweather method is not implemented yet")
|