crystalwindow 4.3__py3-none-any.whl → 4.5__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.
- crystalwindow/__init__.py +8 -1
- crystalwindow/ai.py +132 -147
- crystalwindow/cw_client.py +95 -0
- crystalwindow/websearch.py +233 -0
- crystalwindow/window.py +6 -1
- {crystalwindow-4.3.dist-info → crystalwindow-4.5.dist-info}/METADATA +1 -1
- {crystalwindow-4.3.dist-info → crystalwindow-4.5.dist-info}/RECORD +10 -8
- {crystalwindow-4.3.dist-info → crystalwindow-4.5.dist-info}/WHEEL +0 -0
- {crystalwindow-4.3.dist-info → crystalwindow-4.5.dist-info}/licenses/LICENSE +0 -0
- {crystalwindow-4.3.dist-info → crystalwindow-4.5.dist-info}/top_level.txt +0 -0
crystalwindow/__init__.py
CHANGED
|
@@ -45,6 +45,7 @@ from .draw_tool import CrystalDraw
|
|
|
45
45
|
from .fun_helpers import random_name, DebugOverlay, random_color, random_palette, lerp
|
|
46
46
|
from .camera import Camera
|
|
47
47
|
from .color_handler import Colors, Color
|
|
48
|
+
from .websearch import WebSearchResult, WebSearch
|
|
48
49
|
|
|
49
50
|
# === 3D Engine ===
|
|
50
51
|
from .crystal3d import CW3D
|
|
@@ -52,6 +53,9 @@ from .crystal3d import CW3D
|
|
|
52
53
|
# === AI Engine ===
|
|
53
54
|
from .ai import AI
|
|
54
55
|
|
|
56
|
+
# === Server Manager ===
|
|
57
|
+
from .cw_client import CWClient
|
|
58
|
+
|
|
55
59
|
__all__ = [
|
|
56
60
|
# --- Core ---
|
|
57
61
|
"Window", "Sprite", "TileMap", "Player", "Gravity", "FileHelper", "Mathematics",
|
|
@@ -84,11 +88,14 @@ __all__ = [
|
|
|
84
88
|
"gradient_rect", "CameraShake", "DrawHelper", "DrawTextManager", "CrystalDraw",
|
|
85
89
|
|
|
86
90
|
# --- Misc ---
|
|
87
|
-
"random_name", "DebugOverlay", "Camera", "Colors", "Color", "random_palette", "lerp"
|
|
91
|
+
"random_name", "DebugOverlay", "Camera", "Colors", "Color", "random_palette", "lerp", "WebSearchResult", "WebSearch",
|
|
88
92
|
|
|
89
93
|
# --- 3D ---
|
|
90
94
|
"CW3D",
|
|
91
95
|
|
|
92
96
|
# --- AI ---
|
|
93
97
|
"AI",
|
|
98
|
+
|
|
99
|
+
# --- Server Client ---
|
|
100
|
+
"CWClient",
|
|
94
101
|
]
|
crystalwindow/ai.py
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
1
|
# ==========================================================
|
|
2
|
-
# CrystalAI v0.7 —
|
|
2
|
+
# CrystalAI v0.7 — Stabilized & Corrected Engine
|
|
3
3
|
# ----------------------------------------------------------
|
|
4
|
-
# Focus: Pure Python 'thinking' (Knowledge Graph/Rule-Based)
|
|
5
|
-
# Changes:
|
|
6
|
-
# - Refactored _local_ai to _symbolic_engine
|
|
7
|
-
# - Added a simple knowledge base (K-Graph)
|
|
8
|
-
# - Added context/intent matching for 'thinking'
|
|
9
|
-
# - Removed Groq/External API integration
|
|
10
|
-
# ==========================================================
|
|
11
4
|
|
|
12
5
|
import os
|
|
13
6
|
import ast
|
|
14
7
|
import difflib
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
import requests
|
|
9
|
+
import json
|
|
10
|
+
from typing import Optional, Dict, Any
|
|
11
|
+
|
|
18
12
|
|
|
19
13
|
# ==========================================================
|
|
20
14
|
# Response Wrapper
|
|
@@ -32,33 +26,51 @@ class CrystalAIResponse:
|
|
|
32
26
|
# MAIN ENGINE
|
|
33
27
|
# ==========================================================
|
|
34
28
|
class AI:
|
|
35
|
-
DEFAULT_MODEL = "
|
|
29
|
+
DEFAULT_MODEL = "llama-3.1-70b-versatile"
|
|
36
30
|
DEFAULT_PERSONALITY = (
|
|
37
|
-
"You are
|
|
38
|
-
"
|
|
39
|
-
"
|
|
31
|
+
"You are CrystalWindow AI. You help users with Python code, "
|
|
32
|
+
"debugging, error analysis, documentation, and file analysis. "
|
|
33
|
+
"Be friendly, technical, clear, and precise."
|
|
40
34
|
)
|
|
41
|
-
|
|
42
|
-
PLACEHOLDER_KEY = "
|
|
35
|
+
|
|
36
|
+
PLACEHOLDER_KEY = "NO_KEY_PROVIDED"
|
|
43
37
|
|
|
44
38
|
# ------------------------------------------------------
|
|
45
39
|
def __init__(self, key=None, model=None):
|
|
46
|
-
# --- KEY/MODEL SETUP (for consistency, though ignored) ---
|
|
47
|
-
self.key = self.PLACEHOLDER_KEY
|
|
48
|
-
self.model = self.DEFAULT_MODEL
|
|
49
|
-
self.personality = self.DEFAULT_PERSONALITY
|
|
50
40
|
|
|
51
|
-
# ---
|
|
52
|
-
|
|
41
|
+
# --- KEY VALIDATION ---
|
|
42
|
+
# If no key passed try common environment variables (makes key optional)
|
|
43
|
+
if not key:
|
|
44
|
+
key = (
|
|
45
|
+
os.environ.get("CRYSTALAI_API_KEY")
|
|
46
|
+
or os.environ.get("GROQ_API_KEY")
|
|
47
|
+
or os.environ.get("OPENAI_API_KEY")
|
|
48
|
+
or os.environ.get("API_KEY")
|
|
49
|
+
)
|
|
50
|
+
if key:
|
|
51
|
+
key = str(key).strip()
|
|
52
|
+
|
|
53
|
+
if not key or len(str(key).strip()) == 0:
|
|
54
|
+
print("[CrystalAI] No API key provided → using offline mode.")
|
|
55
|
+
self.key = None # forces offline fallback
|
|
56
|
+
self.force_local = True
|
|
57
|
+
else:
|
|
58
|
+
self.key = key
|
|
59
|
+
self.force_local = False
|
|
60
|
+
|
|
61
|
+
# --- MODEL VALIDATION ---
|
|
62
|
+
# If model is omitted (None) use default silently; only warn if an invalid model is explicitly passed
|
|
63
|
+
if model is None:
|
|
64
|
+
self.model = self.DEFAULT_MODEL
|
|
65
|
+
elif not isinstance(model, str) or len(model) < 3:
|
|
66
|
+
print("[CrystalAI] Unknown model → using default.")
|
|
67
|
+
self.model = self.DEFAULT_MODEL
|
|
68
|
+
else:
|
|
69
|
+
self.model = model
|
|
53
70
|
|
|
54
|
-
|
|
55
|
-
self.memory
|
|
71
|
+
self.personality = self.DEFAULT_PERSONALITY
|
|
72
|
+
self.memory = []
|
|
56
73
|
self.use_memory = True
|
|
57
|
-
|
|
58
|
-
# v0.4 toggle for forcing local engine
|
|
59
|
-
self.force_local = True # Always True for a self-contained AI
|
|
60
|
-
|
|
61
|
-
# Library knowledge (loaded .py files)
|
|
62
74
|
self.library_context = ""
|
|
63
75
|
|
|
64
76
|
# ==========================================================
|
|
@@ -66,12 +78,12 @@ class AI:
|
|
|
66
78
|
# ==========================================================
|
|
67
79
|
def set_personality(self, txt):
|
|
68
80
|
if not isinstance(txt, str) or len(txt.strip()) < 10:
|
|
69
|
-
print("
|
|
81
|
+
print("[CrystalAI] Personality too short → reverting to default.")
|
|
70
82
|
self.personality = self.DEFAULT_PERSONALITY
|
|
71
83
|
return
|
|
72
84
|
|
|
73
85
|
if len(txt) > 3000:
|
|
74
|
-
print("
|
|
86
|
+
print("[CrystalAI] Personality too long → using default.")
|
|
75
87
|
self.personality = self.DEFAULT_PERSONALITY
|
|
76
88
|
return
|
|
77
89
|
|
|
@@ -83,24 +95,25 @@ class AI:
|
|
|
83
95
|
def index_library(self, folder):
|
|
84
96
|
"""
|
|
85
97
|
Load all Python files as context for smarter answers.
|
|
86
|
-
(Context is used in prompt but processed by local engine's rules)
|
|
87
98
|
"""
|
|
88
|
-
out = []
|
|
89
99
|
if not os.path.exists(folder):
|
|
90
100
|
print("[CrystalAI] Library folder not found.")
|
|
91
101
|
return
|
|
92
102
|
|
|
103
|
+
collected = []
|
|
93
104
|
for root, _, files in os.walk(folder):
|
|
94
105
|
for f in files:
|
|
95
106
|
if f.endswith(".py"):
|
|
96
107
|
try:
|
|
97
|
-
|
|
98
|
-
with open(
|
|
99
|
-
|
|
108
|
+
p = os.path.join(root, f)
|
|
109
|
+
with open(p, "r", encoding="utf8") as fp:
|
|
110
|
+
collected.append(
|
|
111
|
+
f"# FILE: {p}\n{fp.read()}\n\n"
|
|
112
|
+
)
|
|
100
113
|
except:
|
|
101
114
|
pass
|
|
102
115
|
|
|
103
|
-
self.library_context = "\n".join(
|
|
116
|
+
self.library_context = "\n".join(collected)[:120_000] # trimmed
|
|
104
117
|
|
|
105
118
|
# ==========================================================
|
|
106
119
|
# FILE READER
|
|
@@ -114,12 +127,12 @@ class AI:
|
|
|
114
127
|
with open(path, "r", encoding="utf8") as f:
|
|
115
128
|
return f.read()
|
|
116
129
|
except:
|
|
117
|
-
return "[CrystalAI]
|
|
130
|
+
return "[CrystalAI] could not read file."
|
|
118
131
|
|
|
119
132
|
# ==========================================================
|
|
120
|
-
# PROMPT BUILDER
|
|
133
|
+
# PROMPT BUILDER
|
|
121
134
|
# ==========================================================
|
|
122
|
-
def _build_prompt(self, user_text, file_data
|
|
135
|
+
def _build_prompt(self, user_text, file_data):
|
|
123
136
|
final = (
|
|
124
137
|
f"[SYSTEM]\n{self.personality}\n\n"
|
|
125
138
|
f"[USER]\n{user_text}\n\n"
|
|
@@ -134,10 +147,10 @@ class AI:
|
|
|
134
147
|
if self.library_context:
|
|
135
148
|
final += f"[LIBRARY]\n{self.library_context}\n\n"
|
|
136
149
|
|
|
137
|
-
if file_data:
|
|
150
|
+
if file_data and not file_data.startswith("[CrystalAI]"):
|
|
138
151
|
final += f"[FILE]\n{file_data}\n\n"
|
|
139
152
|
|
|
140
|
-
return final
|
|
153
|
+
return final[:190_000] # safety limit
|
|
141
154
|
|
|
142
155
|
def _save_memory(self, user, ai):
|
|
143
156
|
self.memory.append({"user": user, "ai": ai})
|
|
@@ -145,114 +158,92 @@ class AI:
|
|
|
145
158
|
self.memory.pop(0)
|
|
146
159
|
|
|
147
160
|
# ==========================================================
|
|
148
|
-
#
|
|
149
|
-
# ==========================================================
|
|
150
|
-
def _build_knowledge_graph(self) -> Dict[str, Any]:
|
|
151
|
-
"""
|
|
152
|
-
Defines the internal knowledge the pure AI can reason with.
|
|
153
|
-
"""
|
|
154
|
-
return {
|
|
155
|
-
"python": {
|
|
156
|
-
"desc": "A high-level, interpreted programming language.",
|
|
157
|
-
"keywords": ["language", "interpreted", "high-level"],
|
|
158
|
-
"syntax": {
|
|
159
|
-
"if_statement": "if condition: ... else: ...",
|
|
160
|
-
"loop": "for item in iterable: ..."
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
"ast": {
|
|
164
|
-
"desc": "Abstract Syntax Tree. Used for parsing code structure.",
|
|
165
|
-
"keywords": ["parsing", "code", "structure", "tree"]
|
|
166
|
-
},
|
|
167
|
-
"fix_code": {
|
|
168
|
-
"rule": "look for SyntaxError, especially missing colons or mismatched brackets",
|
|
169
|
-
"keywords": ["fix", "error", "bug", "syntax"]
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
# ==========================================================
|
|
174
|
-
# PURE AI 'THINKING' ENGINE (Replaces _local_ai)
|
|
161
|
+
# LOCAL FALLBACK AI
|
|
175
162
|
# ==========================================================
|
|
176
|
-
def
|
|
163
|
+
def _local_ai(self, user_text, file_data):
|
|
177
164
|
"""
|
|
178
|
-
|
|
165
|
+
Much cleaner, more reliable fallback engine.
|
|
179
166
|
"""
|
|
180
|
-
output = ["[SymbolicEngine] Processing request..."]
|
|
181
|
-
lower_prompt = prompt.lower()
|
|
182
167
|
|
|
183
|
-
# ---
|
|
168
|
+
# --- AST SECTION ---
|
|
184
169
|
if file_data and not file_data.startswith("[CrystalAI]"):
|
|
185
|
-
output.append("\n[Stage 1: Code Parsing]")
|
|
186
170
|
try:
|
|
187
171
|
ast.parse(file_data)
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
172
|
+
return (
|
|
173
|
+
"[LocalAI] File parsed successfully — no syntax errors.\n"
|
|
174
|
+
"Ask for refactoring, explanation, or improvements."
|
|
175
|
+
)
|
|
191
176
|
except SyntaxError as se:
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
f"
|
|
198
|
-
f"• Message: {se.msg}\n"
|
|
199
|
-
f"• Line: {lineno}\n"
|
|
200
|
-
f"• Column: {offset}\n"
|
|
201
|
-
f"• Rule suggestion: {fix_rule}"
|
|
177
|
+
snippet = self._snippet(file_data, se.lineno)
|
|
178
|
+
return (
|
|
179
|
+
"[LocalAI] SyntaxError found:\n"
|
|
180
|
+
f"• {se.msg}\n"
|
|
181
|
+
f"• Line {se.lineno}\n\n"
|
|
182
|
+
f"{snippet}"
|
|
202
183
|
)
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if key in lower_prompt or any(k in lower_prompt for k in knowledge.get("keywords", [])):
|
|
214
|
-
if key == "fix_code": continue # Already handled in Stage 1
|
|
215
|
-
|
|
216
|
-
output.append(f"🧠 Found Concept: **{key.upper()}**")
|
|
217
|
-
output.append(f"Description: {knowledge.get('desc', 'No detailed description.')}")
|
|
218
|
-
|
|
219
|
-
# Simple reasoning about related syntax
|
|
220
|
-
if 'syntax' in knowledge:
|
|
221
|
-
output.append("Related Syntax:")
|
|
222
|
-
for syn, code in knowledge['syntax'].items():
|
|
223
|
-
output.append(f" - {syn.replace('_', ' ')}: `{code}`")
|
|
224
|
-
|
|
225
|
-
found_concept = True
|
|
226
|
-
break
|
|
227
|
-
|
|
228
|
-
if not found_concept:
|
|
229
|
-
output.append("❓ Concept Unknown: I am limited to my internal knowledge base (Python, AST, Fix Code).")
|
|
230
|
-
output.append("Please ask about a defined concept or provide a file for AST analysis.")
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
return "\n".join(output)
|
|
184
|
+
|
|
185
|
+
# --- GENERIC HELP ---
|
|
186
|
+
lower = user_text.lower()
|
|
187
|
+
if "python" in lower or "fix" in lower or "error" in lower:
|
|
188
|
+
return (
|
|
189
|
+
"[LocalAI] Offline mode: I can help with general Python logic.\n"
|
|
190
|
+
"If you provide a file, I can analyze it using AST."
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
return "[LocalAI] Offline mode active — limited responses available."
|
|
234
194
|
|
|
235
195
|
# ==========================================================
|
|
236
|
-
# ASK
|
|
196
|
+
# MAIN "ASK" FUNCTION
|
|
237
197
|
# ==========================================================
|
|
238
198
|
def ask(self, text, file=None):
|
|
239
199
|
file_data = self._read_file(file)
|
|
240
200
|
prompt = self._build_prompt(text, file_data)
|
|
241
|
-
|
|
242
|
-
#
|
|
243
|
-
|
|
201
|
+
|
|
202
|
+
# If no API key → offline only
|
|
203
|
+
if self.force_local:
|
|
204
|
+
resp = self._local_ai(text, file_data)
|
|
205
|
+
self._save_memory(text, resp)
|
|
206
|
+
return CrystalAIResponse(resp)
|
|
207
|
+
|
|
208
|
+
# Online mode
|
|
209
|
+
try:
|
|
210
|
+
url = "https://api.groq.com/openai/v1/chat/completions"
|
|
211
|
+
headers = {
|
|
212
|
+
"Authorization": f"Bearer {self.key}",
|
|
213
|
+
"Content-Type": "application/json"
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
payload = {
|
|
217
|
+
"model": self.model,
|
|
218
|
+
"messages": [
|
|
219
|
+
{"role": "system", "content": self.personality},
|
|
220
|
+
{"role": "user", "content": prompt}
|
|
221
|
+
],
|
|
222
|
+
"temperature": 0.3
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
r = requests.post(url, json=payload, headers=headers, timeout=8)
|
|
226
|
+
data = r.json()
|
|
227
|
+
|
|
228
|
+
if "error" in data:
|
|
229
|
+
raise RuntimeError(data["error"])
|
|
230
|
+
|
|
231
|
+
resp = data["choices"][0]["message"]["content"]
|
|
232
|
+
|
|
233
|
+
except Exception:
|
|
234
|
+
resp = self._local_ai(text, file_data)
|
|
244
235
|
|
|
245
236
|
self._save_memory(text, resp)
|
|
246
237
|
return CrystalAIResponse(resp)
|
|
247
238
|
|
|
248
239
|
# ==========================================================
|
|
249
|
-
#
|
|
240
|
+
# TERMINAL HELPER
|
|
250
241
|
# ==========================================================
|
|
251
242
|
def ask_t(self, text, file=None):
|
|
252
243
|
return self.ask(f"[TERMINAL] {text}", file)
|
|
253
244
|
|
|
254
245
|
# ==========================================================
|
|
255
|
-
# AUTO FIX CODE
|
|
246
|
+
# AUTO FIX CODE
|
|
256
247
|
# ==========================================================
|
|
257
248
|
def fix_code(self, file_path):
|
|
258
249
|
orig = self._read_file(file_path)
|
|
@@ -266,24 +257,23 @@ class AI:
|
|
|
266
257
|
except SyntaxError as se:
|
|
267
258
|
fixed, notes = self._simple_fix(orig, se)
|
|
268
259
|
diff = self._make_diff(orig, fixed)
|
|
269
|
-
|
|
270
|
-
return CrystalAIResponse(
|
|
260
|
+
msg = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
|
|
261
|
+
return CrystalAIResponse(msg, {"diff": diff, "notes": notes})
|
|
271
262
|
|
|
272
263
|
# ==========================================================
|
|
273
264
|
# SIMPLE AUTO-FIX ENGINE
|
|
274
265
|
# ==========================================================
|
|
275
|
-
def _simple_fix(self, src,
|
|
266
|
+
def _simple_fix(self, src, err):
|
|
276
267
|
notes = []
|
|
277
268
|
lines = src.splitlines()
|
|
278
|
-
msg = getattr(
|
|
279
|
-
lineno =
|
|
269
|
+
msg = getattr(err, "msg", "")
|
|
270
|
+
lineno = err.lineno or 0
|
|
280
271
|
|
|
281
|
-
# missing colon
|
|
282
272
|
if "expected" in msg and ":" in msg:
|
|
283
273
|
if 1 <= lineno <= len(lines):
|
|
284
|
-
|
|
285
|
-
if not
|
|
286
|
-
lines[lineno - 1] =
|
|
274
|
+
l = lines[lineno - 1].rstrip()
|
|
275
|
+
if not l.endswith(":"):
|
|
276
|
+
lines[lineno - 1] = l + ":"
|
|
287
277
|
notes.append("[fix] added missing ':'")
|
|
288
278
|
candidate = "\n".join(lines)
|
|
289
279
|
try:
|
|
@@ -292,21 +282,17 @@ class AI:
|
|
|
292
282
|
except:
|
|
293
283
|
pass
|
|
294
284
|
|
|
295
|
-
|
|
296
|
-
notes.append("[info] auto-fix could not fix everything")
|
|
285
|
+
notes.append("[info] auto-fix could not fix the error")
|
|
297
286
|
return src, notes
|
|
298
287
|
|
|
299
288
|
# ==========================================================
|
|
300
|
-
# DIFF
|
|
289
|
+
# DIFF HELPER
|
|
301
290
|
# ==========================================================
|
|
302
291
|
def _make_diff(self, old, new):
|
|
303
292
|
return "\n".join(
|
|
304
293
|
difflib.unified_diff(
|
|
305
|
-
old.splitlines(),
|
|
306
|
-
new
|
|
307
|
-
fromfile="old",
|
|
308
|
-
tofile="new",
|
|
309
|
-
lineterm=""
|
|
294
|
+
old.splitlines(), new.splitlines(),
|
|
295
|
+
fromfile="old", tofile="new", lineterm=""
|
|
310
296
|
)
|
|
311
297
|
)
|
|
312
298
|
|
|
@@ -320,10 +306,9 @@ class AI:
|
|
|
320
306
|
out = []
|
|
321
307
|
for i in range(start, end):
|
|
322
308
|
mark = "->" if (i + 1) == lineno else " "
|
|
323
|
-
|
|
324
|
-
out.append(f"{mark} {i+1:<4}: {lines[i]}")
|
|
309
|
+
out.append(f"{mark} {i+1:4}: {lines[i]}")
|
|
325
310
|
return "\n".join(out)
|
|
326
311
|
|
|
327
312
|
# ==========================================================
|
|
328
|
-
# END OF
|
|
329
|
-
# ==========================================================
|
|
313
|
+
# END OF ENGINE
|
|
314
|
+
# ==========================================================
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# cw_client.py
|
|
2
|
+
# pip install websockets
|
|
3
|
+
import asyncio
|
|
4
|
+
import websockets
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
class CWClient:
|
|
10
|
+
"""
|
|
11
|
+
Lightweight client wrapper. Usage:
|
|
12
|
+
c = CWClient("MyName", "ws://host:8765")
|
|
13
|
+
c.on_event = lambda ev: print("ev", ev)
|
|
14
|
+
c.start()
|
|
15
|
+
c.move(x,y)
|
|
16
|
+
c.chat("hi")
|
|
17
|
+
"""
|
|
18
|
+
def __init__(self, pid, uri="ws://127.0.0.1:8765"):
|
|
19
|
+
self.pid = pid
|
|
20
|
+
self.uri = uri
|
|
21
|
+
self._send_q = asyncio.Queue()
|
|
22
|
+
self.on_event = None # callback for incoming events: fn(dict)
|
|
23
|
+
self._running = False
|
|
24
|
+
self._loop_thread = None
|
|
25
|
+
|
|
26
|
+
async def _run(self):
|
|
27
|
+
try:
|
|
28
|
+
async with websockets.connect(self.uri) as ws:
|
|
29
|
+
# send join
|
|
30
|
+
await ws.send(json.dumps({"type":"join","id":self.pid}))
|
|
31
|
+
self._running = True
|
|
32
|
+
sender_task = asyncio.create_task(self._sender(ws))
|
|
33
|
+
try:
|
|
34
|
+
async for raw in ws:
|
|
35
|
+
try:
|
|
36
|
+
data = json.loads(raw)
|
|
37
|
+
except:
|
|
38
|
+
continue
|
|
39
|
+
if self.on_event:
|
|
40
|
+
# run callback in same thread (safe)
|
|
41
|
+
try:
|
|
42
|
+
self.on_event(data)
|
|
43
|
+
except Exception as cb_e:
|
|
44
|
+
# swallow callback errors
|
|
45
|
+
print("on_event callback error:", cb_e)
|
|
46
|
+
finally:
|
|
47
|
+
sender_task.cancel()
|
|
48
|
+
except Exception as e:
|
|
49
|
+
# notify user on connection lost
|
|
50
|
+
if self.on_event:
|
|
51
|
+
try:
|
|
52
|
+
self.on_event({"type":"sys","msg":f"connection_lost:{e}"})
|
|
53
|
+
except:
|
|
54
|
+
pass
|
|
55
|
+
finally:
|
|
56
|
+
self._running = False
|
|
57
|
+
|
|
58
|
+
async def _sender(self, ws):
|
|
59
|
+
while True:
|
|
60
|
+
try:
|
|
61
|
+
obj = await self._send_q.get()
|
|
62
|
+
await ws.send(json.dumps(obj))
|
|
63
|
+
except Exception:
|
|
64
|
+
break
|
|
65
|
+
|
|
66
|
+
def start(self):
|
|
67
|
+
if self._loop_thread and self._loop_thread.is_alive():
|
|
68
|
+
return
|
|
69
|
+
def target():
|
|
70
|
+
asyncio.run(self._run())
|
|
71
|
+
self._loop_thread = threading.Thread(target=target, daemon=True)
|
|
72
|
+
self._loop_thread.start()
|
|
73
|
+
# give a bit of time to connect
|
|
74
|
+
time.sleep(0.05)
|
|
75
|
+
|
|
76
|
+
def send(self, obj):
|
|
77
|
+
# enqueue safely from any thread
|
|
78
|
+
async def push():
|
|
79
|
+
await self._send_q.put(obj)
|
|
80
|
+
try:
|
|
81
|
+
loop = asyncio.get_running_loop()
|
|
82
|
+
# if main thread has a loop, schedule
|
|
83
|
+
asyncio.run_coroutine_threadsafe(push(), loop)
|
|
84
|
+
except RuntimeError:
|
|
85
|
+
# no running loop in this thread: create a tiny loop to push
|
|
86
|
+
def run_push():
|
|
87
|
+
asyncio.run(push())
|
|
88
|
+
threading.Thread(target=run_push, daemon=True).start()
|
|
89
|
+
|
|
90
|
+
# convenience APIs
|
|
91
|
+
def move(self, x, y):
|
|
92
|
+
self.send({"type":"move","id":self.pid,"x":int(x),"y":int(y)})
|
|
93
|
+
|
|
94
|
+
def chat(self, msg):
|
|
95
|
+
self.send({"type":"chat","id":self.pid,"msg":str(msg)})
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CrystalWindow WebSearch module — Google (Serper.dev) Edition
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import requests
|
|
7
|
+
import webbrowser
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import List, Tuple, Optional, Any
|
|
10
|
+
|
|
11
|
+
# Optional Qt integration
|
|
12
|
+
try:
|
|
13
|
+
from PySide6.QtWidgets import (
|
|
14
|
+
QWidget, QVBoxLayout, QLineEdit, QPushButton, QTextEdit,
|
|
15
|
+
QListWidget, QListWidgetItem, QLabel, QHBoxLayout
|
|
16
|
+
)
|
|
17
|
+
from PySide6.QtCore import Qt, QUrl
|
|
18
|
+
from PySide6.QtGui import QDesktopServices
|
|
19
|
+
PYSIDE6_AVAILABLE = True
|
|
20
|
+
except Exception:
|
|
21
|
+
PYSIDE6_AVAILABLE = False
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# --------------------------
|
|
25
|
+
# Data result structure
|
|
26
|
+
# --------------------------
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class WebSearchResult:
|
|
30
|
+
text: str
|
|
31
|
+
links: List[Tuple[str, str]] # (title, url)
|
|
32
|
+
raw: Any = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# --------------------------
|
|
36
|
+
# Main WebSearch class
|
|
37
|
+
# --------------------------
|
|
38
|
+
|
|
39
|
+
class WebSearch:
|
|
40
|
+
""" Real Web Search helper for CrystalWindow using Google (Serper.dev). """
|
|
41
|
+
|
|
42
|
+
# --- DuckDuckGo Instant Answer (kept for fallback) ---
|
|
43
|
+
DUCKDUCKGO_INSTANT = "https://api.duckduckgo.com/"
|
|
44
|
+
|
|
45
|
+
# --- Google Search (Serper.dev) ---
|
|
46
|
+
SERPER_URL = "https://google.serper.dev/search"
|
|
47
|
+
SERPER_API_KEY = "a8e02281a0c3b58bb29a3731b6a2aec1d8d8a487" # <<<<<<<< REPLACE THIS
|
|
48
|
+
|
|
49
|
+
# --------------------------------------------------
|
|
50
|
+
# DuckDuckGo Instant Answer (old API)
|
|
51
|
+
# --------------------------------------------------
|
|
52
|
+
@classmethod
|
|
53
|
+
def _duckduckgo_instant(cls, query: str) -> WebSearchResult:
|
|
54
|
+
params = {
|
|
55
|
+
"q": query,
|
|
56
|
+
"format": "json",
|
|
57
|
+
"no_html": 1,
|
|
58
|
+
"no_redirect": 1,
|
|
59
|
+
}
|
|
60
|
+
try:
|
|
61
|
+
r = requests.get(cls.DUCKDUCKGO_INSTANT, params=params, timeout=8)
|
|
62
|
+
r.raise_for_status()
|
|
63
|
+
except Exception as e:
|
|
64
|
+
return WebSearchResult(text=f"DuckDuckGo error: {e}", links=[])
|
|
65
|
+
|
|
66
|
+
data = r.json()
|
|
67
|
+
abstract = data.get("AbstractText") or data.get("Abstract") or ""
|
|
68
|
+
links: List[Tuple[str, str]] = []
|
|
69
|
+
|
|
70
|
+
if data.get("AbstractURL"):
|
|
71
|
+
links.append((data.get("Heading", "Result"), data["AbstractURL"]))
|
|
72
|
+
|
|
73
|
+
def extract_related(rt):
|
|
74
|
+
if isinstance(rt, dict):
|
|
75
|
+
if rt.get("FirstURL") and rt.get("Text"):
|
|
76
|
+
links.append((rt["Text"], rt["FirstURL"]))
|
|
77
|
+
for key in ("Topics", "RelatedTopics"):
|
|
78
|
+
if isinstance(rt.get(key), list):
|
|
79
|
+
for sub in rt[key]:
|
|
80
|
+
extract_related(sub)
|
|
81
|
+
elif isinstance(rt, list):
|
|
82
|
+
for item in rt:
|
|
83
|
+
extract_related(item)
|
|
84
|
+
|
|
85
|
+
extract_related(data.get("RelatedTopics", []))
|
|
86
|
+
if not abstract and links:
|
|
87
|
+
abstract = "\n".join([t for t, _ in links[:3]])
|
|
88
|
+
|
|
89
|
+
return WebSearchResult(text=abstract.strip(), links=links, raw=data)
|
|
90
|
+
|
|
91
|
+
# --------------------------------------------------
|
|
92
|
+
# Google Search via Serper.dev (NEW)
|
|
93
|
+
# --------------------------------------------------
|
|
94
|
+
@classmethod
|
|
95
|
+
def _google_serper(cls, query: str) -> WebSearchResult:
|
|
96
|
+
headers = {
|
|
97
|
+
"X-API-KEY": cls.SERPER_API_KEY,
|
|
98
|
+
"Content-Type": "application/json"
|
|
99
|
+
}
|
|
100
|
+
body = {"q": query}
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
r = requests.post(cls.SERPER_URL, headers=headers, json=body, timeout=8)
|
|
104
|
+
r.raise_for_status()
|
|
105
|
+
except Exception as e:
|
|
106
|
+
return WebSearchResult(text=f"Google search error: {e}", links=[])
|
|
107
|
+
|
|
108
|
+
data = r.json()
|
|
109
|
+
|
|
110
|
+
links = []
|
|
111
|
+
summary_parts = []
|
|
112
|
+
|
|
113
|
+
for item in data.get("organic", []):
|
|
114
|
+
title = item.get("title", "Untitled")
|
|
115
|
+
url = item.get("link", "")
|
|
116
|
+
snippet = item.get("snippet", "")
|
|
117
|
+
|
|
118
|
+
links.append((title, url))
|
|
119
|
+
if snippet:
|
|
120
|
+
summary_parts.append(snippet)
|
|
121
|
+
|
|
122
|
+
summary = "\n".join(summary_parts[:3]).strip()
|
|
123
|
+
if not summary:
|
|
124
|
+
summary = "(no summary)"
|
|
125
|
+
|
|
126
|
+
return WebSearchResult(summary, links, data)
|
|
127
|
+
|
|
128
|
+
# --------------------------------------------------
|
|
129
|
+
# Main public search()
|
|
130
|
+
# --------------------------------------------------
|
|
131
|
+
@classmethod
|
|
132
|
+
def search(cls, query: str, engine: str = "google") -> WebSearchResult:
|
|
133
|
+
if not query or not query.strip():
|
|
134
|
+
return WebSearchResult(text="", links=[])
|
|
135
|
+
|
|
136
|
+
q = query.strip().lower()
|
|
137
|
+
engine = engine.lower()
|
|
138
|
+
|
|
139
|
+
if engine == "google":
|
|
140
|
+
return cls._google_serper(q)
|
|
141
|
+
|
|
142
|
+
if engine == "duckduckgo":
|
|
143
|
+
return cls._duckduckgo_instant(q)
|
|
144
|
+
|
|
145
|
+
return WebSearchResult(text=f"Unknown engine '{engine}'", links=[])
|
|
146
|
+
|
|
147
|
+
# --------------------------------------------------
|
|
148
|
+
# URL opener
|
|
149
|
+
# --------------------------------------------------
|
|
150
|
+
@staticmethod
|
|
151
|
+
def open_url(url: str) -> None:
|
|
152
|
+
try:
|
|
153
|
+
webbrowser.open(url)
|
|
154
|
+
except Exception:
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
# --------------------------------------------------
|
|
158
|
+
# Optional PySide6 GUI
|
|
159
|
+
# --------------------------------------------------
|
|
160
|
+
if PYSIDE6_AVAILABLE:
|
|
161
|
+
class _CWWebTab(QWidget):
|
|
162
|
+
def __init__(self, parent=None):
|
|
163
|
+
super().__init__(parent)
|
|
164
|
+
self.setWindowTitle("WebSearch")
|
|
165
|
+
|
|
166
|
+
layout = QVBoxLayout(self)
|
|
167
|
+
|
|
168
|
+
# Search bar
|
|
169
|
+
bar = QHBoxLayout()
|
|
170
|
+
self.qbox = QLineEdit()
|
|
171
|
+
self.qbox.setPlaceholderText("Search the web...")
|
|
172
|
+
btn = QPushButton("Search")
|
|
173
|
+
btn.clicked.connect(self._do_search)
|
|
174
|
+
bar.addWidget(self.qbox)
|
|
175
|
+
bar.addWidget(btn)
|
|
176
|
+
layout.addLayout(bar)
|
|
177
|
+
|
|
178
|
+
# Summary text
|
|
179
|
+
self.summary = QTextEdit()
|
|
180
|
+
self.summary.setReadOnly(True)
|
|
181
|
+
self.summary.setFixedHeight(150)
|
|
182
|
+
layout.addWidget(self.summary)
|
|
183
|
+
|
|
184
|
+
# Links list
|
|
185
|
+
self.links = QListWidget()
|
|
186
|
+
self.links.itemActivated.connect(self._open_item)
|
|
187
|
+
layout.addWidget(self.links)
|
|
188
|
+
|
|
189
|
+
self.note = QLabel("Powered by Google (Serper.dev) API")
|
|
190
|
+
self.note.setAlignment(Qt.AlignCenter)
|
|
191
|
+
layout.addWidget(self.note)
|
|
192
|
+
|
|
193
|
+
def _populate(self, result: WebSearchResult):
|
|
194
|
+
self.summary.setPlainText(result.text)
|
|
195
|
+
self.links.clear()
|
|
196
|
+
for t, u in result.links:
|
|
197
|
+
item = QListWidgetItem(f"{t}\n{u}")
|
|
198
|
+
item.setData(Qt.UserRole, u)
|
|
199
|
+
self.links.addItem(item)
|
|
200
|
+
|
|
201
|
+
def _open_item(self, item: QListWidgetItem):
|
|
202
|
+
url = item.data(Qt.UserRole)
|
|
203
|
+
if url:
|
|
204
|
+
QDesktopServices.openUrl(QUrl(url))
|
|
205
|
+
|
|
206
|
+
def _do_search(self):
|
|
207
|
+
q = self.qbox.text().strip()
|
|
208
|
+
if not q:
|
|
209
|
+
self.summary.setPlainText("Type something to search.")
|
|
210
|
+
return
|
|
211
|
+
self.summary.setPlainText("Searching...")
|
|
212
|
+
res = WebSearch.search(q, engine="google")
|
|
213
|
+
self._populate(res)
|
|
214
|
+
|
|
215
|
+
@classmethod
|
|
216
|
+
def register_cw_tab(cls, window):
|
|
217
|
+
try:
|
|
218
|
+
tab = cls._CWWebTab()
|
|
219
|
+
if hasattr(window, "add_tab"):
|
|
220
|
+
window.add_tab("WebSearch", tab)
|
|
221
|
+
elif hasattr(window, "addTab"):
|
|
222
|
+
window.addTab(tab, "WebSearch")
|
|
223
|
+
else:
|
|
224
|
+
if hasattr(window, "setCentralWidget"):
|
|
225
|
+
window.setCentralWidget(tab)
|
|
226
|
+
return tab
|
|
227
|
+
except Exception:
|
|
228
|
+
return None
|
|
229
|
+
|
|
230
|
+
else:
|
|
231
|
+
@classmethod
|
|
232
|
+
def register_cw_tab(cls, window):
|
|
233
|
+
return None
|
crystalwindow/window.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# === Window Management (tkinter version) ===
|
|
2
2
|
import tkinter as tk
|
|
3
3
|
import base64, io, os, sys, contextlib, time
|
|
4
|
+
from .websearch import WebSearch
|
|
5
|
+
|
|
4
6
|
|
|
5
7
|
# === Boot ===
|
|
6
8
|
main_file = os.path.basename(sys.argv[0])
|
|
@@ -117,6 +119,8 @@ class Window:
|
|
|
117
119
|
self._key_last = {} # new dict to store last press time per key
|
|
118
120
|
self._key_cooldown = 0.00001 # seconds, tweak for faster/slower input
|
|
119
121
|
|
|
122
|
+
WebSearch.register_cw_tab(self)
|
|
123
|
+
|
|
120
124
|
# === Event bindings ===
|
|
121
125
|
self.root.bind("<KeyPress>", self._on_keydown)
|
|
122
126
|
self.root.bind("<KeyRelease>", self._on_keyup)
|
|
@@ -163,7 +167,8 @@ class Window:
|
|
|
163
167
|
|
|
164
168
|
def mouse_pressed(self, button=1):
|
|
165
169
|
return self._mouse_pressed[button - 1]
|
|
166
|
-
|
|
170
|
+
def add_tab(self, title, widget):
|
|
171
|
+
self.tabs.addTab(widget, title)
|
|
167
172
|
# === Drawing ===
|
|
168
173
|
def fill(self, color):
|
|
169
174
|
"""Fill background with a color"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: crystalwindow
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.5
|
|
4
4
|
Summary: A Tkinter powered window + GUI toolkit made by Crystal (MEEEEEE)! Easier apps, smoother UI and all-in-one helpers!
|
|
5
5
|
Home-page: https://pypi.org/project/crystalwindow/
|
|
6
6
|
Author: CrystalBallyHereXD
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
crystalwindow/FileHelper.py,sha256=aUnnRG7UwvzJt-idjWjmpwy3RM6nqLlC3-7Bae6Yb94,5471
|
|
2
|
-
crystalwindow/__init__.py,sha256=
|
|
3
|
-
crystalwindow/ai.py,sha256=
|
|
2
|
+
crystalwindow/__init__.py,sha256=nOEUvoB4OpcYq_le1Kee0Uef3Jq38bYYapOI1u1CN2E,2457
|
|
3
|
+
crystalwindow/ai.py,sha256=YIt6dNe1QljSAlNECCVa3DMUSIqQEJRIAAbQpYqFNNo,11525
|
|
4
4
|
crystalwindow/animation.py,sha256=zHjrdBXQeyNaLAuaGPldJueX05OZ5j31YR8NizmR0uQ,427
|
|
5
5
|
crystalwindow/assets.py,sha256=2Cj0zdhMWo3mWjdr9KU5n-9_8iKj_fJ9uShMFA-27HU,5193
|
|
6
6
|
crystalwindow/camera.py,sha256=tbn4X-jxMIszAUg3Iu-89gJN5nij0mjPMEzGotcLbJI,712
|
|
@@ -8,6 +8,7 @@ crystalwindow/clock.py,sha256=EAcNvxonD7ktkKSIbuVHqzhPhQqtUQtleMSBB_OuUQY,541
|
|
|
8
8
|
crystalwindow/collision.py,sha256=hpkHTp_KparghVK-itp_rxuYdd2GbQMxICHlUBv0rSw,472
|
|
9
9
|
crystalwindow/color_handler.py,sha256=ZnXnz8552GPiRAnCkW_8wycwRRMAaFRFLlCcsrv0j2E,4071
|
|
10
10
|
crystalwindow/crystal3d.py,sha256=-Te9IJgbtzl8Mpuc4BPi2bn2apnvBNTI2GwxSLd8sqg,2006
|
|
11
|
+
crystalwindow/cw_client.py,sha256=KxhvXtsTfrCupB2zi5e35ppKVPrOkVXTuhgCNT6V2eA,3264
|
|
11
12
|
crystalwindow/draw_helpers.py,sha256=HqjI5fTbdnA55g4LKYEuMUdIjrWaBm2U8RmeUXjcQGw,821
|
|
12
13
|
crystalwindow/draw_rects.py,sha256=o7siET3y35N2LPeNBGe8QhsQbOH8J-xF6fOUz07rymU,1484
|
|
13
14
|
crystalwindow/draw_text_helper.py,sha256=qv5fFCuTCKWeGDk9Z_ZpOzrTFP8YYwlgQrrorwrq9Hg,1298
|
|
@@ -21,7 +22,8 @@ crystalwindow/player.py,sha256=4wpIdUZLTlRXV8fDRQ11yVJRbx_cv8Ekpn2y7pQGgAQ,3442
|
|
|
21
22
|
crystalwindow/sprites.py,sha256=2kYW4QfSnyUwRLedZCkb99wUSxn2a0JdWOHcfkxcG0U,3411
|
|
22
23
|
crystalwindow/tilemap.py,sha256=PHoKL1eWuNeHIf0w-Jh5MGdQGEgklVsxqqJOS-GNMKI,322
|
|
23
24
|
crystalwindow/ver_warner.py,sha256=qEN3ulc1NixBy15FFx2R3Zu0DhyJTVJwiESGAPwpynM,3373
|
|
24
|
-
crystalwindow/
|
|
25
|
+
crystalwindow/websearch.py,sha256=O98LfYPuovdU6ct8iStNpoEJLTc26av9bz4nqX6lyN4,8027
|
|
26
|
+
crystalwindow/window.py,sha256=umJ8gNGS64N_mVzPBBxZKUNq0mDcGoy7ImJthLlZHSI,18337
|
|
25
27
|
crystalwindow/Icons/default_icon.png,sha256=Loq27Pxb8Wb3Sz-XwtNF1RmlLNxR4TcfOWfK-1lWcII,7724
|
|
26
28
|
crystalwindow/docs/getting_started.md,sha256=e_XEhJk8eatS22MX0nRX7hQNkYkwN9both1ObabZSTw,5759
|
|
27
29
|
crystalwindow/docs/index.md,sha256=bd14uLWtRSeRBm28zngGyfGDI1J6bJRvHkQLDpYwgOE,747
|
|
@@ -33,8 +35,8 @@ crystalwindow/gametests/guitesting.py,sha256=SrOssY5peCQEV6TQ1AiOWtjb9phVGdRzW-Q
|
|
|
33
35
|
crystalwindow/gametests/sandbox.py,sha256=Oo2tU2N0y3BPVa6T5vs_h9N6islhQrjSrr_78XLut5I,1007
|
|
34
36
|
crystalwindow/gametests/squaremove.py,sha256=poP2Zjl2oc2HVvIAgIK34H2jVj6otL4jEdvAOR6L9sI,572
|
|
35
37
|
crystalwindow/gametests/windowtesting.py,sha256=_9X6wnV1-_X_PtNS-0zu-k209NtFIwAc4vpxLPp7V2o,97
|
|
36
|
-
crystalwindow-4.
|
|
37
|
-
crystalwindow-4.
|
|
38
|
-
crystalwindow-4.
|
|
39
|
-
crystalwindow-4.
|
|
40
|
-
crystalwindow-4.
|
|
38
|
+
crystalwindow-4.5.dist-info/licenses/LICENSE,sha256=Gt5cJRchdNt0guxyQMHKsATN5PM5mjuDhdO6Gzs9qQc,1096
|
|
39
|
+
crystalwindow-4.5.dist-info/METADATA,sha256=nWirrrZxkZMaUYGi8BbZQ3fGFCh0d0OZTtLReJHzYls,7338
|
|
40
|
+
crystalwindow-4.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
41
|
+
crystalwindow-4.5.dist-info/top_level.txt,sha256=PeQSld4b19XWT-zvbYkvE2Xg8sakIMbDzSzSdOSRN8o,14
|
|
42
|
+
crystalwindow-4.5.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|