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.
- {crystalwindow-4.4/crystalwindow.egg-info → crystalwindow-4.5}/PKG-INFO +1 -1
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/__init__.py +8 -1
- crystalwindow-4.5/crystalwindow/ai.py +314 -0
- crystalwindow-4.5/crystalwindow/cw_client.py +95 -0
- crystalwindow-4.5/crystalwindow/websearch.py +233 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/window.py +6 -1
- {crystalwindow-4.4 → crystalwindow-4.5/crystalwindow.egg-info}/PKG-INFO +1 -1
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow.egg-info/SOURCES.txt +2 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/setup.py +1 -1
- crystalwindow-4.4/crystalwindow/ai.py +0 -366
- {crystalwindow-4.4 → crystalwindow-4.5}/LICENSE +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/MANIFEST.in +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/README.md +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/FileHelper.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/Icons/default_icon.png +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/animation.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/assets.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/camera.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/clock.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/collision.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/color_handler.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/crystal3d.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/docs/getting_started.md +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/docs/index.md +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_helpers.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_rects.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_text_helper.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/draw_tool.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/fun_helpers.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/3dsquare.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/__init__.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/__main__.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/gravitytest.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/guitesting.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/sandbox.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/squaremove.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gametests/windowtesting.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gravity.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gui.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/gui_ext.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/math.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/player.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/sprites.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/tilemap.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow/ver_warner.py +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow.egg-info/dependency_links.txt +0 -0
- {crystalwindow-4.4 → crystalwindow-4.5}/crystalwindow.egg-info/top_level.txt +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|