crystalwindow 4.4__tar.gz → 4.5__tar.gz

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.
Files changed (48) hide show
  1. {crystalwindow-4.4/crystalwindow.egg-info → crystalwindow-4.5}/PKG-INFO +1 -1
  2. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/__init__.py +8 -1
  3. crystalwindow-4.5/crystalwindow/ai.py +314 -0
  4. crystalwindow-4.5/crystalwindow/cw_client.py +95 -0
  5. crystalwindow-4.5/crystalwindow/websearch.py +233 -0
  6. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/window.py +6 -1
  7. {crystalwindow-4.4 → crystalwindow-4.5/crystalwindow.egg-info}/PKG-INFO +1 -1
  8. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow.egg-info/SOURCES.txt +2 -0
  9. {crystalwindow-4.4 → crystalwindow-4.5}/setup.py +1 -1
  10. crystalwindow-4.4/crystalwindow/ai.py +0 -366
  11. {crystalwindow-4.4 → crystalwindow-4.5}/LICENSE +0 -0
  12. {crystalwindow-4.4 → crystalwindow-4.5}/MANIFEST.in +0 -0
  13. {crystalwindow-4.4 → crystalwindow-4.5}/README.md +0 -0
  14. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/FileHelper.py +0 -0
  15. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/Icons/default_icon.png +0 -0
  16. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/animation.py +0 -0
  17. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/assets.py +0 -0
  18. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/camera.py +0 -0
  19. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/clock.py +0 -0
  20. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/collision.py +0 -0
  21. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/color_handler.py +0 -0
  22. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/crystal3d.py +0 -0
  23. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/docs/getting_started.md +0 -0
  24. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/docs/index.md +0 -0
  25. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_helpers.py +0 -0
  26. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_rects.py +0 -0
  27. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_text_helper.py +0 -0
  28. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_tool.py +0 -0
  29. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/fun_helpers.py +0 -0
  30. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/3dsquare.py +0 -0
  31. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/__init__.py +0 -0
  32. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/__main__.py +0 -0
  33. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/gravitytest.py +0 -0
  34. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/guitesting.py +0 -0
  35. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/sandbox.py +0 -0
  36. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/squaremove.py +0 -0
  37. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/windowtesting.py +0 -0
  38. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gravity.py +0 -0
  39. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gui.py +0 -0
  40. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gui_ext.py +0 -0
  41. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/math.py +0 -0
  42. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/player.py +0 -0
  43. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/sprites.py +0 -0
  44. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/tilemap.py +0 -0
  45. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/ver_warner.py +0 -0
  46. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow.egg-info/dependency_links.txt +0 -0
  47. {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow.egg-info/top_level.txt +0 -0
  48. {crystalwindow-4.4 → crystalwindow-4.5}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: crystalwindow
3
- Version: 4.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
@@ -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
  ]
@@ -0,0 +1,314 @@
1
+ # ==========================================================
2
+ # CrystalAI v0.7 — Stabilized & Corrected Engine
3
+ # ----------------------------------------------------------
4
+
5
+ import os
6
+ import ast
7
+ import difflib
8
+ import requests
9
+ import json
10
+ from typing import Optional, Dict, Any
11
+
12
+
13
+ # ==========================================================
14
+ # Response Wrapper
15
+ # ==========================================================
16
+ class CrystalAIResponse:
17
+ def __init__(self, text: str, meta: Optional[Dict[str, Any]] = None):
18
+ self.text = text
19
+ self.meta = meta or {}
20
+
21
+ def __str__(self):
22
+ return self.text
23
+
24
+
25
+ # ==========================================================
26
+ # MAIN ENGINE
27
+ # ==========================================================
28
+ class AI:
29
+ DEFAULT_MODEL = "llama-3.1-70b-versatile"
30
+ DEFAULT_PERSONALITY = (
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."
34
+ )
35
+
36
+ PLACEHOLDER_KEY = "NO_KEY_PROVIDED"
37
+
38
+ # ------------------------------------------------------
39
+ def __init__(self, key=None, model=None):
40
+
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
70
+
71
+ self.personality = self.DEFAULT_PERSONALITY
72
+ self.memory = []
73
+ self.use_memory = True
74
+ self.library_context = ""
75
+
76
+ # ==========================================================
77
+ # PERSONALITY
78
+ # ==========================================================
79
+ def set_personality(self, txt):
80
+ if not isinstance(txt, str) or len(txt.strip()) < 10:
81
+ print("[CrystalAI] Personality too short → reverting to default.")
82
+ self.personality = self.DEFAULT_PERSONALITY
83
+ return
84
+
85
+ if len(txt) > 3000:
86
+ print("[CrystalAI] Personality too long → using default.")
87
+ self.personality = self.DEFAULT_PERSONALITY
88
+ return
89
+
90
+ self.personality = txt.strip()
91
+
92
+ # ==========================================================
93
+ # LIBRARY INGESTION
94
+ # ==========================================================
95
+ def index_library(self, folder):
96
+ """
97
+ Load all Python files as context for smarter answers.
98
+ """
99
+ if not os.path.exists(folder):
100
+ print("[CrystalAI] Library folder not found.")
101
+ return
102
+
103
+ collected = []
104
+ for root, _, files in os.walk(folder):
105
+ for f in files:
106
+ if f.endswith(".py"):
107
+ try:
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
+ )
113
+ except:
114
+ pass
115
+
116
+ self.library_context = "\n".join(collected)[:120_000] # trimmed
117
+
118
+ # ==========================================================
119
+ # FILE READER
120
+ # ==========================================================
121
+ def _read_file(self, path):
122
+ if not path:
123
+ return None
124
+ if not os.path.exists(path):
125
+ return f"[CrystalAI] file not found: {path}"
126
+ try:
127
+ with open(path, "r", encoding="utf8") as f:
128
+ return f.read()
129
+ except:
130
+ return "[CrystalAI] could not read file."
131
+
132
+ # ==========================================================
133
+ # PROMPT BUILDER
134
+ # ==========================================================
135
+ def _build_prompt(self, user_text, file_data):
136
+ final = (
137
+ f"[SYSTEM]\n{self.personality}\n\n"
138
+ f"[USER]\n{user_text}\n\n"
139
+ )
140
+
141
+ if self.use_memory and self.memory:
142
+ final += "[MEMORY]\n"
143
+ for m in self.memory[-6:]:
144
+ final += f"User: {m['user']}\nAI: {m['ai']}\n"
145
+ final += "\n"
146
+
147
+ if self.library_context:
148
+ final += f"[LIBRARY]\n{self.library_context}\n\n"
149
+
150
+ if file_data and not file_data.startswith("[CrystalAI]"):
151
+ final += f"[FILE]\n{file_data}\n\n"
152
+
153
+ return final[:190_000] # safety limit
154
+
155
+ def _save_memory(self, user, ai):
156
+ self.memory.append({"user": user, "ai": ai})
157
+ if len(self.memory) > 60:
158
+ self.memory.pop(0)
159
+
160
+ # ==========================================================
161
+ # LOCAL FALLBACK AI
162
+ # ==========================================================
163
+ def _local_ai(self, user_text, file_data):
164
+ """
165
+ Much cleaner, more reliable fallback engine.
166
+ """
167
+
168
+ # --- AST SECTION ---
169
+ if file_data and not file_data.startswith("[CrystalAI]"):
170
+ try:
171
+ ast.parse(file_data)
172
+ return (
173
+ "[LocalAI] File parsed successfully — no syntax errors.\n"
174
+ "Ask for refactoring, explanation, or improvements."
175
+ )
176
+ except SyntaxError as se:
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}"
183
+ )
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."
194
+
195
+ # ==========================================================
196
+ # MAIN "ASK" FUNCTION
197
+ # ==========================================================
198
+ def ask(self, text, file=None):
199
+ file_data = self._read_file(file)
200
+ prompt = self._build_prompt(text, file_data)
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)
235
+
236
+ self._save_memory(text, resp)
237
+ return CrystalAIResponse(resp)
238
+
239
+ # ==========================================================
240
+ # TERMINAL HELPER
241
+ # ==========================================================
242
+ def ask_t(self, text, file=None):
243
+ return self.ask(f"[TERMINAL] {text}", file)
244
+
245
+ # ==========================================================
246
+ # AUTO FIX CODE
247
+ # ==========================================================
248
+ def fix_code(self, file_path):
249
+ orig = self._read_file(file_path)
250
+
251
+ if not orig or orig.startswith("[CrystalAI]"):
252
+ return CrystalAIResponse(orig or "[CrystalAI] file missing")
253
+
254
+ try:
255
+ ast.parse(orig)
256
+ return CrystalAIResponse("[AI] No syntax errors found.")
257
+ except SyntaxError as se:
258
+ fixed, notes = self._simple_fix(orig, se)
259
+ diff = self._make_diff(orig, fixed)
260
+ msg = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
261
+ return CrystalAIResponse(msg, {"diff": diff, "notes": notes})
262
+
263
+ # ==========================================================
264
+ # SIMPLE AUTO-FIX ENGINE
265
+ # ==========================================================
266
+ def _simple_fix(self, src, err):
267
+ notes = []
268
+ lines = src.splitlines()
269
+ msg = getattr(err, "msg", "")
270
+ lineno = err.lineno or 0
271
+
272
+ if "expected" in msg and ":" in msg:
273
+ if 1 <= lineno <= len(lines):
274
+ l = lines[lineno - 1].rstrip()
275
+ if not l.endswith(":"):
276
+ lines[lineno - 1] = l + ":"
277
+ notes.append("[fix] added missing ':'")
278
+ candidate = "\n".join(lines)
279
+ try:
280
+ ast.parse(candidate)
281
+ return candidate, notes
282
+ except:
283
+ pass
284
+
285
+ notes.append("[info] auto-fix could not fix the error")
286
+ return src, notes
287
+
288
+ # ==========================================================
289
+ # DIFF HELPER
290
+ # ==========================================================
291
+ def _make_diff(self, old, new):
292
+ return "\n".join(
293
+ difflib.unified_diff(
294
+ old.splitlines(), new.splitlines(),
295
+ fromfile="old", tofile="new", lineterm=""
296
+ )
297
+ )
298
+
299
+ # ==========================================================
300
+ # SNIPPET HELPER
301
+ # ==========================================================
302
+ def _snippet(self, src, lineno, ctx=2):
303
+ lines = src.splitlines()
304
+ start = max(0, lineno - ctx - 1)
305
+ end = min(len(lines), lineno + ctx)
306
+ out = []
307
+ for i in range(start, end):
308
+ mark = "->" if (i + 1) == lineno else " "
309
+ out.append(f"{mark} {i+1:4}: {lines[i]}")
310
+ return "\n".join(out)
311
+
312
+ # ==========================================================
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
@@ -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.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
@@ -12,6 +12,7 @@ crystalwindow/clock.py
12
12
  crystalwindow/collision.py
13
13
  crystalwindow/color_handler.py
14
14
  crystalwindow/crystal3d.py
15
+ crystalwindow/cw_client.py
15
16
  crystalwindow/draw_helpers.py
16
17
  crystalwindow/draw_rects.py
17
18
  crystalwindow/draw_text_helper.py
@@ -25,6 +26,7 @@ crystalwindow/player.py
25
26
  crystalwindow/sprites.py
26
27
  crystalwindow/tilemap.py
27
28
  crystalwindow/ver_warner.py
29
+ crystalwindow/websearch.py
28
30
  crystalwindow/window.py
29
31
  crystalwindow.egg-info/PKG-INFO
30
32
  crystalwindow.egg-info/SOURCES.txt
@@ -7,7 +7,7 @@ with open("README.md", encoding="utf-8") as f:
7
7
 
8
8
  setup(
9
9
  name="crystalwindow",
10
- version="4.4", # Force metadata refresh
10
+ version="4.5", # Force metadata refresh
11
11
  packages=find_packages(include=["crystalwindow", "crystalwindow.*"]),
12
12
 
13
13
  include_package_data=True, # include package_data files
@@ -1,366 +0,0 @@
1
- # ==========================================================
2
- # CrystalAI v0.8 — Hybrid Engine (Groq + Symbolic Fallback)
3
- # ----------------------------------------------------------
4
- # Combines:
5
- # - Groq API for general knowledge (Primary)
6
- # - Pure Python Symbolic Engine for Code Analysis (Fallback)
7
- # - Memory, File Reading, AST Parsing, and Diff Utilities
8
- # ==========================================================
9
-
10
- import os
11
- import ast
12
- import difflib
13
- import requests # Re-added for API communication
14
- from typing import Optional, Dict, Any, List
15
- # Removed groq import, using requests for simplicity
16
-
17
- # ==========================================================
18
- # Response Wrapper
19
- # ==========================================================
20
- class CrystalAIResponse:
21
- def __init__(self, text: str, meta: Optional[Dict[str, Any]] = None):
22
- self.text = text
23
- self.meta = meta or {}
24
-
25
- def __str__(self):
26
- return self.text
27
-
28
-
29
- # ==========================================================
30
- # MAIN ENGINE
31
- # ==========================================================
32
- class AI:
33
- DEFAULT_MODEL = "llama-3.1-8b"
34
- DEFAULT_PERSONALITY = (
35
- "You are CrystalWindow AI. You help users with Python code, "
36
- "debugging, errors, docs, and file analysis. You are currently running in "
37
- "Hybrid Mode. Be friendly, technical, clear, and precise."
38
- )
39
- # Placeholder Key for testing or when user key is invalid
40
- PLACEHOLDER_KEY = "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"
41
-
42
- # ------------------------------------------------------
43
- def __init__(self, key=None, model=None):
44
- # --- KEY VALIDATION ---
45
- if not key or len(key) < 20 or " " in key:
46
- print("[CrystalAI] Warning: Invalid or missing key → using placeholder. To get a Fixed Key go to 'console.groq.com/keys'")
47
- self.key = self.PLACEHOLDER_KEY
48
- else:
49
- self.key = key
50
-
51
- # --- MODEL VALIDATION ---
52
- if not model or not isinstance(model, str) or len(model) < 3:
53
- print(f"[CrystalAI] Unknown model → using default: {self.DEFAULT_MODEL}.")
54
- self.model = self.DEFAULT_MODEL
55
- else:
56
- self.model = model
57
-
58
- # Persona
59
- self.personality = self.DEFAULT_PERSONALITY
60
-
61
- # Pure AI knowledge (used in symbolic fallback)
62
- self.knowledge_graph: Dict[str, Any] = self._build_knowledge_graph()
63
-
64
- # Library knowledge (loaded .py files)
65
- self.library_context = ""
66
-
67
- # Memory system
68
- self.memory: List[Dict[str, str]] = []
69
- self.use_memory = True
70
-
71
- # Force local toggle (currently not used as logic is based on Groq success)
72
- self.force_local = False
73
-
74
- # ==========================================================
75
- # PERSONALITY
76
- # ==========================================================
77
- def set_personality(self, txt):
78
- if not isinstance(txt, str) or len(txt.strip()) < 10:
79
- print("Oops! that's not how to use it—reverting to default.")
80
- self.personality = self.DEFAULT_PERSONALITY
81
- return
82
-
83
- if len(txt) > 3000:
84
- print("Oops, personality too long → using default.")
85
- self.personality = self.DEFAULT_PERSONALITY
86
- return
87
-
88
- self.personality = txt.strip()
89
-
90
- # ==========================================================
91
- # LIBRARY INGESTION
92
- # ==========================================================
93
- def index_library(self, folder):
94
- """
95
- Load all Python files as context for smarter answers.
96
- """
97
- out = []
98
- if not os.path.exists(folder):
99
- print("[CrystalAI] Library folder not found.")
100
- return
101
-
102
- for root, _, files in os.walk(folder):
103
- for f in files:
104
- if f.endswith(".py"):
105
- try:
106
- path = os.path.join(root, f)
107
- with open(path, "r", encoding="utf8") as fp:
108
- out.append(f"# FILE: {path}\n" + fp.read() + "\n\n")
109
- except Exception:
110
- pass
111
-
112
- self.library_context = "\n".join(out)
113
-
114
- # ==========================================================
115
- # FILE READER
116
- # ==========================================================
117
- def _read_file(self, path):
118
- if not path:
119
- return None
120
- if not os.path.exists(path):
121
- return f"[CrystalAI] file not found: {path}"
122
- try:
123
- with open(path, "r", encoding="utf8") as f:
124
- return f.read()
125
- except Exception:
126
- return "[CrystalAI] couldn't read file."
127
-
128
- # ==========================================================
129
- # PROMPT BUILDER (Unified)
130
- # ==========================================================
131
- def _build_prompt(self, user_text, file_data=None):
132
- final = (
133
- f"[SYSTEM]\n{self.personality}\n\n"
134
- f"[USER]\n{user_text}\n\n"
135
- )
136
-
137
- if self.use_memory and self.memory:
138
- final += "[MEMORY]\n"
139
- for m in self.memory[-6:]:
140
- final += f"User: {m['user']}\nAI: {m['ai']}\n"
141
- final += "\n"
142
-
143
- if self.library_context:
144
- final += f"[LIBRARY]\n{self.library_context}\n\n"
145
-
146
- if file_data:
147
- final += f"[FILE]\n{file_data}\n\n"
148
-
149
- return final
150
-
151
- def _save_memory(self, user, ai):
152
- self.memory.append({"user": user, "ai": ai})
153
- if len(self.memory) > 60:
154
- self.memory.pop(0)
155
-
156
- # ==========================================================
157
- # PURE AI KNOWLEDGE BASE (Symbolic Core)
158
- # ==========================================================
159
- def _build_knowledge_graph(self) -> Dict[str, Any]:
160
- """
161
- Defines the internal knowledge the symbolic AI can reason with.
162
- """
163
- return {
164
- "python": {
165
- "desc": "A high-level, interpreted programming language.",
166
- "keywords": ["language", "interpreted", "high-level"],
167
- "syntax": {
168
- "if_statement": "if condition: ... else: ...",
169
- "loop": "for item in iterable: ..."
170
- }
171
- },
172
- "ast": {
173
- "desc": "Abstract Syntax Tree. Used for parsing code structure.",
174
- "keywords": ["parsing", "code", "structure", "tree"]
175
- },
176
- "fix_code": {
177
- "rule": "look for SyntaxError, especially missing colons or mismatched brackets",
178
- "keywords": ["fix", "error", "bug", "syntax"]
179
- }
180
- }
181
-
182
- # ==========================================================
183
- # PURE AI 'THINKING' ENGINE (Symbolic Fallback)
184
- # ==========================================================
185
- def _symbolic_engine(self, prompt: str, file_data: Optional[str]) -> str:
186
- """
187
- Fallback logic: Simulates 'thinking' using only internal rules and AST.
188
- """
189
- output = ["[Local/SymbolicEngine] Processing request..."]
190
- lower_prompt = prompt.lower()
191
-
192
- # --- Stage 1: File Analysis (Real Python AST) ---
193
- if file_data and not file_data.startswith("[CrystalAI]"):
194
- output.append("\n[Stage 1: Code Parsing]")
195
- try:
196
- ast.parse(file_data)
197
- output.append("✅ **No Syntax Errors Detected** (via AST).")
198
- output.append("The code is structurally sound. Ask for refactoring or explanation.")
199
- return "\n".join(output)
200
- except SyntaxError as se:
201
- fix_rule = self.knowledge_graph["fix_code"]["rule"]
202
- lineno = se.lineno or 0
203
- msg = (
204
- f"❌ **SyntaxError Detected** (via AST):\n"
205
- f"• Message: {se.msg}\n"
206
- f"• Line: {lineno}\n"
207
- f"• Rule suggestion: {fix_rule}"
208
- )
209
- output.append(msg)
210
- output.append(self._snippet(file_data, lineno))
211
- return "\n".join(output)
212
-
213
- # --- Stage 2: Knowledge Graph Lookup (Rule-Based Reasoning) ---
214
- output.append("\n[Stage 2: Symbolic Lookup]")
215
-
216
- found_concept = False
217
- for key, knowledge in self.knowledge_graph.items():
218
- if key in lower_prompt or any(k in lower_prompt for k in knowledge.get("keywords", [])):
219
- if key == "fix_code": continue
220
-
221
- output.append(f"🧠 Found Concept: **{key.upper()}**")
222
- output.append(f"Description: {knowledge.get('desc', 'No detailed description.')}")
223
-
224
- if 'syntax' in knowledge:
225
- output.append("Related Syntax:")
226
- for syn, code in knowledge['syntax'].items():
227
- output.append(f" - {syn.replace('_', ' ')}: `{code}`")
228
-
229
- found_concept = True
230
- break
231
-
232
- if not found_concept:
233
- output.append("❓ Concept Unknown: I am currently offline and limited to my internal knowledge base (Python, AST, Fix Code).")
234
- output.append("Please provide a file for AST analysis or try again later for a full Groq response.")
235
-
236
- return "\n".join(output)
237
-
238
-
239
- # ==========================================================
240
- # ASK (Groq → Symbolic Fallback)
241
- # ==========================================================
242
- def ask(self, text, file=None):
243
- file_data = self._read_file(file)
244
- prompt = self._build_prompt(text, file_data)
245
-
246
- resp = None
247
-
248
- # --- Attempt 1: Call External API (Groq) ---
249
- try:
250
- url = "https://api.groq.com/openai/v1/chat/completions"
251
- headers = {
252
- "Authorization": f"Bearer {self.key}",
253
- "Content-Type": "application/json"
254
- }
255
-
256
- payload = {
257
- "model": self.model,
258
- "messages": [
259
- {"role": "system", "content": self.personality},
260
- {"role": "user", "content": prompt}
261
- ],
262
- "temperature": 0.3
263
- }
264
-
265
- r = requests.post(url, json=payload, headers=headers, timeout=10)
266
- data = r.json()
267
-
268
- if "error" in data:
269
- # If the API returns an error (e.g., bad key), we force the fallback
270
- raise RuntimeError(f"API Error: {data['error'].get('message', 'Unknown API Error')}")
271
-
272
- resp = data["choices"][0]["message"]["content"]
273
-
274
- except Exception as e:
275
- # This block handles API connection failures (timeout) or API-side errors (bad key)
276
- print(f"[CrystalAI] Groq connection failed or returned error: {e.__class__.__name__}")
277
- print("[CrystalAI] Falling back to self-contained Symbolic Engine...")
278
-
279
- # --- Attempt 2: Fallback to Symbolic Engine ---
280
- resp = self._symbolic_engine(prompt, file_data)
281
-
282
-
283
- self._save_memory(text, resp)
284
- return CrystalAIResponse(resp)
285
-
286
- # ==========================================================
287
- # ASK (terminal)
288
- # ==========================================================
289
- def ask_t(self, text, file=None):
290
- return self.ask(f"[TERMINAL] {text}", file)
291
-
292
- # ==========================================================
293
- # AUTO FIX CODE (v0.4)
294
- # ==========================================================
295
- def fix_code(self, file_path):
296
- orig = self._read_file(file_path)
297
-
298
- if not orig or orig.startswith("[CrystalAI]"):
299
- return CrystalAIResponse(orig or "[CrystalAI] file missing")
300
-
301
- try:
302
- ast.parse(orig)
303
- return CrystalAIResponse("[AI] No syntax errors found.")
304
- except SyntaxError as se:
305
- fixed, notes = self._simple_fix(orig, se)
306
- diff = self._make_diff(orig, fixed)
307
- pretty = "[AI] Auto-fix result:\n" + "\n".join(notes) + "\n\n" + diff
308
- return CrystalAIResponse(pretty, {"diff": diff, "notes": notes})
309
-
310
- # ==========================================================
311
- # SIMPLE AUTO-FIX ENGINE
312
- # ==========================================================
313
- def _simple_fix(self, src, syntax_error):
314
- notes = []
315
- lines = src.splitlines()
316
- msg = getattr(syntax_error, "msg", "")
317
- lineno = syntax_error.lineno or 0
318
-
319
- # missing colon fix
320
- if "expected" in msg and ":" in msg:
321
- if 1 <= lineno <= len(lines):
322
- line = lines[lineno - 1].rstrip()
323
- if not line.endswith(":"):
324
- lines[lineno - 1] = line + ":"
325
- notes.append("[fix] added missing ':'")
326
- candidate = "\n".join(lines)
327
- try:
328
- ast.parse(candidate)
329
- return candidate, notes
330
- except Exception:
331
- pass
332
-
333
- # fallback
334
- notes.append("[info] auto-fix could not fix everything")
335
- return src, notes
336
-
337
- # ==========================================================
338
- # DIFF UTIL
339
- # ==========================================================
340
- def _make_diff(self, old, new):
341
- return "\n".join(
342
- difflib.unified_diff(
343
- old.splitlines(),
344
- new.splitlines(),
345
- fromfile="old",
346
- tofile="new",
347
- lineterm=""
348
- )
349
- )
350
-
351
- # ==========================================================
352
- # SNIPPET HELPER
353
- # ==========================================================
354
- def _snippet(self, src, lineno, ctx=2):
355
- lines = src.splitlines()
356
- start = max(0, lineno - ctx - 1)
357
- end = min(len(lines), lineno + ctx)
358
- out = []
359
- for i in range(start, end):
360
- mark = "->" if (i + 1) == lineno else " "
361
- out.append(f"{mark} {i+1:<4}: {lines[i]}")
362
- return "\n".join(out)
363
-
364
- # ==========================================================
365
- # END OF HYBRID ENGINE
366
- # ==========================================================
File without changes
File without changes
File without changes
File without changes