PraisonAI 0.0.59rc2__cp312-cp312-manylinux_2_35_x86_64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of PraisonAI might be problematic. Click here for more details.
- praisonai/__init__.py +6 -0
- praisonai/__main__.py +10 -0
- praisonai/agents_generator.py +381 -0
- praisonai/auto.py +190 -0
- praisonai/chainlit_ui.py +304 -0
- praisonai/cli.py +337 -0
- praisonai/deploy.py +138 -0
- praisonai/inbuilt_tools/__init__.py +2 -0
- praisonai/inbuilt_tools/autogen_tools.py +209 -0
- praisonai/inc/__init__.py +2 -0
- praisonai/inc/models.py +128 -0
- praisonai/public/android-chrome-192x192.png +0 -0
- praisonai/public/android-chrome-512x512.png +0 -0
- praisonai/public/apple-touch-icon.png +0 -0
- praisonai/public/fantasy.svg +3 -0
- praisonai/public/favicon-16x16.png +0 -0
- praisonai/public/favicon-32x32.png +0 -0
- praisonai/public/favicon.ico +0 -0
- praisonai/public/game.svg +3 -0
- praisonai/public/logo_dark.png +0 -0
- praisonai/public/logo_light.png +0 -0
- praisonai/public/movie.svg +3 -0
- praisonai/public/thriller.svg +3 -0
- praisonai/test.py +105 -0
- praisonai/train.py +232 -0
- praisonai/ui/chat.py +304 -0
- praisonai/ui/code.py +318 -0
- praisonai/ui/context.py +283 -0
- praisonai/ui/public/fantasy.svg +3 -0
- praisonai/ui/public/game.svg +3 -0
- praisonai/ui/public/logo_dark.png +0 -0
- praisonai/ui/public/logo_light.png +0 -0
- praisonai/ui/public/movie.svg +3 -0
- praisonai/ui/public/thriller.svg +3 -0
- praisonai/ui/sql_alchemy.py +638 -0
- praisonai/version.py +1 -0
- praisonai-0.0.59rc2.dist-info/LICENSE +20 -0
- praisonai-0.0.59rc2.dist-info/METADATA +344 -0
- praisonai-0.0.59rc2.dist-info/RECORD +41 -0
- praisonai-0.0.59rc2.dist-info/WHEEL +4 -0
- praisonai-0.0.59rc2.dist-info/entry_points.txt +5 -0
praisonai/ui/code.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import chainlit as cl
|
|
2
|
+
from chainlit.input_widget import TextInput
|
|
3
|
+
from chainlit.types import ThreadDict
|
|
4
|
+
from litellm import acompletion
|
|
5
|
+
import os
|
|
6
|
+
import sqlite3
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
from dotenv import load_dotenv
|
|
10
|
+
load_dotenv()
|
|
11
|
+
import chainlit.data as cl_data
|
|
12
|
+
from chainlit.step import StepDict
|
|
13
|
+
from literalai.helper import utc_now
|
|
14
|
+
import logging
|
|
15
|
+
import json
|
|
16
|
+
from sql_alchemy import SQLAlchemyDataLayer
|
|
17
|
+
from context import ContextGatherer
|
|
18
|
+
|
|
19
|
+
# Set up logging
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
log_level = os.getenv("LOGLEVEL", "INFO").upper()
|
|
22
|
+
logger.handlers = []
|
|
23
|
+
|
|
24
|
+
# Set up logging to console
|
|
25
|
+
console_handler = logging.StreamHandler()
|
|
26
|
+
console_handler.setLevel(log_level)
|
|
27
|
+
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
28
|
+
console_handler.setFormatter(console_formatter)
|
|
29
|
+
logger.addHandler(console_handler)
|
|
30
|
+
|
|
31
|
+
# Set the logging level for the logger
|
|
32
|
+
logger.setLevel(log_level)
|
|
33
|
+
|
|
34
|
+
CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
|
|
35
|
+
|
|
36
|
+
if not CHAINLIT_AUTH_SECRET:
|
|
37
|
+
os.environ["CHAINLIT_AUTH_SECRET"] = "p8BPhQChpg@J>jBz$wGxqLX2V>yTVgP*7Ky9H$aV:axW~ANNX-7_T:o@lnyCBu^U"
|
|
38
|
+
CHAINLIT_AUTH_SECRET = os.getenv("CHAINLIT_AUTH_SECRET")
|
|
39
|
+
|
|
40
|
+
now = utc_now()
|
|
41
|
+
|
|
42
|
+
create_step_counter = 0
|
|
43
|
+
|
|
44
|
+
DB_PATH = os.path.expanduser("~/.praison/database.sqlite")
|
|
45
|
+
|
|
46
|
+
def initialize_db():
|
|
47
|
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
48
|
+
conn = sqlite3.connect(DB_PATH)
|
|
49
|
+
cursor = conn.cursor()
|
|
50
|
+
cursor.execute('''
|
|
51
|
+
CREATE TABLE IF NOT EXISTS users (
|
|
52
|
+
id UUID PRIMARY KEY,
|
|
53
|
+
identifier TEXT NOT NULL UNIQUE,
|
|
54
|
+
metadata JSONB NOT NULL,
|
|
55
|
+
createdAt TEXT
|
|
56
|
+
)
|
|
57
|
+
''')
|
|
58
|
+
cursor.execute('''
|
|
59
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
60
|
+
id UUID PRIMARY KEY,
|
|
61
|
+
createdAt TEXT,
|
|
62
|
+
name TEXT,
|
|
63
|
+
userId UUID,
|
|
64
|
+
userIdentifier TEXT,
|
|
65
|
+
tags TEXT[],
|
|
66
|
+
metadata JSONB NOT NULL DEFAULT '{}',
|
|
67
|
+
FOREIGN KEY (userId) REFERENCES users(id) ON DELETE CASCADE
|
|
68
|
+
)
|
|
69
|
+
''')
|
|
70
|
+
cursor.execute('''
|
|
71
|
+
CREATE TABLE IF NOT EXISTS steps (
|
|
72
|
+
id UUID PRIMARY KEY,
|
|
73
|
+
name TEXT NOT NULL,
|
|
74
|
+
type TEXT NOT NULL,
|
|
75
|
+
threadId UUID NOT NULL,
|
|
76
|
+
parentId UUID,
|
|
77
|
+
disableFeedback BOOLEAN NOT NULL,
|
|
78
|
+
streaming BOOLEAN NOT NULL,
|
|
79
|
+
waitForAnswer BOOLEAN,
|
|
80
|
+
isError BOOLEAN,
|
|
81
|
+
metadata JSONB,
|
|
82
|
+
tags TEXT[],
|
|
83
|
+
input TEXT,
|
|
84
|
+
output TEXT,
|
|
85
|
+
createdAt TEXT,
|
|
86
|
+
start TEXT,
|
|
87
|
+
end TEXT,
|
|
88
|
+
generation JSONB,
|
|
89
|
+
showInput TEXT,
|
|
90
|
+
language TEXT,
|
|
91
|
+
indent INT,
|
|
92
|
+
FOREIGN KEY (threadId) REFERENCES threads (id) ON DELETE CASCADE
|
|
93
|
+
)
|
|
94
|
+
''')
|
|
95
|
+
cursor.execute('''
|
|
96
|
+
CREATE TABLE IF NOT EXISTS elements (
|
|
97
|
+
id UUID PRIMARY KEY,
|
|
98
|
+
threadId UUID,
|
|
99
|
+
type TEXT,
|
|
100
|
+
url TEXT,
|
|
101
|
+
chainlitKey TEXT,
|
|
102
|
+
name TEXT NOT NULL,
|
|
103
|
+
display TEXT,
|
|
104
|
+
objectKey TEXT,
|
|
105
|
+
size TEXT,
|
|
106
|
+
page INT,
|
|
107
|
+
language TEXT,
|
|
108
|
+
forId UUID,
|
|
109
|
+
mime TEXT,
|
|
110
|
+
FOREIGN KEY (threadId) REFERENCES threads (id) ON DELETE CASCADE
|
|
111
|
+
)
|
|
112
|
+
''')
|
|
113
|
+
cursor.execute('''
|
|
114
|
+
CREATE TABLE IF NOT EXISTS feedbacks (
|
|
115
|
+
id UUID PRIMARY KEY,
|
|
116
|
+
forId UUID NOT NULL,
|
|
117
|
+
value INT NOT NULL,
|
|
118
|
+
threadId UUID,
|
|
119
|
+
comment TEXT
|
|
120
|
+
)
|
|
121
|
+
''')
|
|
122
|
+
cursor.execute('''
|
|
123
|
+
CREATE TABLE IF NOT EXISTS settings (
|
|
124
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
125
|
+
key TEXT UNIQUE,
|
|
126
|
+
value TEXT
|
|
127
|
+
)
|
|
128
|
+
''')
|
|
129
|
+
conn.commit()
|
|
130
|
+
conn.close()
|
|
131
|
+
|
|
132
|
+
def save_setting(key: str, value: str):
|
|
133
|
+
"""Saves a setting to the database.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
key: The setting key.
|
|
137
|
+
value: The setting value.
|
|
138
|
+
"""
|
|
139
|
+
conn = sqlite3.connect(DB_PATH)
|
|
140
|
+
cursor = conn.cursor()
|
|
141
|
+
cursor.execute(
|
|
142
|
+
"""
|
|
143
|
+
INSERT OR REPLACE INTO settings (id, key, value)
|
|
144
|
+
VALUES ((SELECT id FROM settings WHERE key = ?), ?, ?)
|
|
145
|
+
""",
|
|
146
|
+
(key, key, value),
|
|
147
|
+
)
|
|
148
|
+
conn.commit()
|
|
149
|
+
conn.close()
|
|
150
|
+
|
|
151
|
+
def load_setting(key: str) -> str:
|
|
152
|
+
"""Loads a setting from the database.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
key: The setting key.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The setting value, or None if the key is not found.
|
|
159
|
+
"""
|
|
160
|
+
conn = sqlite3.connect(DB_PATH)
|
|
161
|
+
cursor = conn.cursor()
|
|
162
|
+
cursor.execute('SELECT value FROM settings WHERE key = ?', (key,))
|
|
163
|
+
result = cursor.fetchone()
|
|
164
|
+
conn.close()
|
|
165
|
+
return result[0] if result else None
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
# Initialize the database
|
|
169
|
+
initialize_db()
|
|
170
|
+
|
|
171
|
+
deleted_thread_ids = [] # type: List[str]
|
|
172
|
+
|
|
173
|
+
cl_data._data_layer = SQLAlchemyDataLayer(conninfo=f"sqlite+aiosqlite:///{DB_PATH}")
|
|
174
|
+
|
|
175
|
+
@cl.on_chat_start
|
|
176
|
+
async def start():
|
|
177
|
+
initialize_db()
|
|
178
|
+
model_name = load_setting("model_name")
|
|
179
|
+
|
|
180
|
+
if model_name:
|
|
181
|
+
cl.user_session.set("model_name", model_name)
|
|
182
|
+
else:
|
|
183
|
+
# If no setting found, use default or environment variable
|
|
184
|
+
model_name = os.getenv("MODEL_NAME", "gpt-4o-mini")
|
|
185
|
+
cl.user_session.set("model_name", model_name)
|
|
186
|
+
logger.debug(f"Model name: {model_name}")
|
|
187
|
+
settings = cl.ChatSettings(
|
|
188
|
+
[
|
|
189
|
+
TextInput(
|
|
190
|
+
id="model_name",
|
|
191
|
+
label="Enter the Model Name",
|
|
192
|
+
placeholder="e.g., gpt-4o-mini",
|
|
193
|
+
initial=model_name
|
|
194
|
+
)
|
|
195
|
+
]
|
|
196
|
+
)
|
|
197
|
+
cl.user_session.set("settings", settings)
|
|
198
|
+
await settings.send()
|
|
199
|
+
gatherer = ContextGatherer()
|
|
200
|
+
context, token_count, context_tree = gatherer.run()
|
|
201
|
+
msg = cl.Message(content="""Token Count: {token_count},
|
|
202
|
+
Files include: \n```bash\n{context_tree}\n"""
|
|
203
|
+
.format(token_count=token_count, context_tree=context_tree))
|
|
204
|
+
await msg.send()
|
|
205
|
+
|
|
206
|
+
@cl.on_settings_update
|
|
207
|
+
async def setup_agent(settings):
|
|
208
|
+
logger.debug(settings)
|
|
209
|
+
cl.user_session.set("settings", settings)
|
|
210
|
+
model_name = settings["model_name"]
|
|
211
|
+
cl.user_session.set("model_name", model_name)
|
|
212
|
+
|
|
213
|
+
# Save in settings table
|
|
214
|
+
save_setting("model_name", model_name)
|
|
215
|
+
|
|
216
|
+
# Save in thread metadata
|
|
217
|
+
thread_id = cl.user_session.get("thread_id")
|
|
218
|
+
if thread_id:
|
|
219
|
+
thread = await cl_data.get_thread(thread_id)
|
|
220
|
+
if thread:
|
|
221
|
+
metadata = thread.get("metadata", {})
|
|
222
|
+
metadata["model_name"] = model_name
|
|
223
|
+
|
|
224
|
+
# Always store metadata as a JSON string
|
|
225
|
+
await cl_data.update_thread(thread_id, metadata=json.dumps(metadata))
|
|
226
|
+
|
|
227
|
+
# Update the user session with the new metadata
|
|
228
|
+
cl.user_session.set("metadata", metadata)
|
|
229
|
+
|
|
230
|
+
@cl.on_message
|
|
231
|
+
async def main(message: cl.Message):
|
|
232
|
+
model_name = load_setting("model_name") or os.getenv("MODEL_NAME") or "gpt-4o-mini"
|
|
233
|
+
message_history = cl.user_session.get("message_history", [])
|
|
234
|
+
message_history.append({"role": "user", "content": message.content})
|
|
235
|
+
gatherer = ContextGatherer()
|
|
236
|
+
context, token_count, context_tree = gatherer.run()
|
|
237
|
+
prompt_history = message_history
|
|
238
|
+
prompt_history.append({"role": "user", "content": """
|
|
239
|
+
Answer the question:\n{question}.\n\n
|
|
240
|
+
Below is the Context:\n{context}\n\n"""
|
|
241
|
+
.format(context=context, question=message.content)})
|
|
242
|
+
|
|
243
|
+
msg = cl.Message(content="")
|
|
244
|
+
await msg.send()
|
|
245
|
+
|
|
246
|
+
response = await acompletion(
|
|
247
|
+
model=model_name,
|
|
248
|
+
messages=prompt_history,
|
|
249
|
+
stream=True,
|
|
250
|
+
# temperature=0.7,
|
|
251
|
+
# max_tokens=500,
|
|
252
|
+
# top_p=1
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
full_response = ""
|
|
256
|
+
async for part in response:
|
|
257
|
+
if token := part['choices'][0]['delta']['content']:
|
|
258
|
+
await msg.stream_token(token)
|
|
259
|
+
full_response += token
|
|
260
|
+
logger.debug(f"Full response: {full_response}")
|
|
261
|
+
message_history.append({"role": "assistant", "content": full_response})
|
|
262
|
+
logger.debug(f"Message history: {message_history}")
|
|
263
|
+
cl.user_session.set("message_history", message_history)
|
|
264
|
+
await msg.update()
|
|
265
|
+
|
|
266
|
+
username = os.getenv("CHAINLIT_USERNAME", "admin") # Default to "admin" if not found
|
|
267
|
+
password = os.getenv("CHAINLIT_PASSWORD", "admin") # Default to "admin" if not found
|
|
268
|
+
|
|
269
|
+
@cl.password_auth_callback
|
|
270
|
+
def auth_callback(username: str, password: str):
|
|
271
|
+
if (username, password) == (username, password):
|
|
272
|
+
return cl.User(
|
|
273
|
+
identifier=username, metadata={"role": "ADMIN", "provider": "credentials"}
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
return None
|
|
277
|
+
|
|
278
|
+
async def send_count():
|
|
279
|
+
await cl.Message(
|
|
280
|
+
f"Create step counter: {create_step_counter}", disable_feedback=True
|
|
281
|
+
).send()
|
|
282
|
+
|
|
283
|
+
@cl.on_chat_resume
|
|
284
|
+
async def on_chat_resume(thread: cl_data.ThreadDict):
|
|
285
|
+
logger.info(f"Resuming chat: {thread['id']}")
|
|
286
|
+
model_name = load_setting("model_name") or os.getenv("MODEL_NAME") or "gpt-4o-mini"
|
|
287
|
+
logger.debug(f"Model name: {model_name}")
|
|
288
|
+
settings = cl.ChatSettings(
|
|
289
|
+
[
|
|
290
|
+
TextInput(
|
|
291
|
+
id="model_name",
|
|
292
|
+
label="Enter the Model Name",
|
|
293
|
+
placeholder="e.g., gpt-4o-mini",
|
|
294
|
+
initial=model_name
|
|
295
|
+
)
|
|
296
|
+
]
|
|
297
|
+
)
|
|
298
|
+
await settings.send()
|
|
299
|
+
thread_id = thread["id"]
|
|
300
|
+
cl.user_session.set("thread_id", thread["id"])
|
|
301
|
+
|
|
302
|
+
# The metadata should now already be a dictionary
|
|
303
|
+
metadata = thread.get("metadata", {})
|
|
304
|
+
cl.user_session.set("metadata", metadata)
|
|
305
|
+
|
|
306
|
+
message_history = cl.user_session.get("message_history", [])
|
|
307
|
+
steps = thread["steps"]
|
|
308
|
+
|
|
309
|
+
for message in steps:
|
|
310
|
+
msg_type = message.get("type")
|
|
311
|
+
if msg_type == "user_message":
|
|
312
|
+
message_history.append({"role": "user", "content": message.get("output", "")})
|
|
313
|
+
elif msg_type == "assistant_message":
|
|
314
|
+
message_history.append({"role": "assistant", "content": message.get("output", "")})
|
|
315
|
+
else:
|
|
316
|
+
logger.warning(f"Message without type: {message}")
|
|
317
|
+
|
|
318
|
+
cl.user_session.set("message_history", message_history)
|
praisonai/ui/context.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import fnmatch
|
|
3
|
+
import yaml
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
# Set up logging
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
log_level = os.getenv("LOGLEVEL", "INFO").upper()
|
|
10
|
+
logger.handlers = []
|
|
11
|
+
|
|
12
|
+
# Set up logging to console
|
|
13
|
+
console_handler = logging.StreamHandler()
|
|
14
|
+
console_handler.setLevel(log_level)
|
|
15
|
+
console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
16
|
+
console_handler.setFormatter(console_formatter)
|
|
17
|
+
logger.addHandler(console_handler)
|
|
18
|
+
|
|
19
|
+
# Set the logging level for the logger
|
|
20
|
+
logger.setLevel(log_level)
|
|
21
|
+
|
|
22
|
+
class ContextGatherer:
|
|
23
|
+
def __init__(self, directory='.', output_file='context.txt',
|
|
24
|
+
relevant_extensions=None, max_file_size=1_000_000, max_tokens=900000):
|
|
25
|
+
self.directory = directory
|
|
26
|
+
self.output_file = output_file
|
|
27
|
+
self.relevant_extensions = relevant_extensions or [
|
|
28
|
+
'.py', '.js', '.ts', '.java', '.rb', '.php', '.pl', '.pm', '.c', '.h',
|
|
29
|
+
'.cpp', '.hpp', '.cs', '.vb', '.swift', '.kt', '.m', '.mm', '.go', '.rs',
|
|
30
|
+
'.hs', '.r', '.lua', '.sh', '.bat', '.clj', '.scala', '.erl', '.ex',
|
|
31
|
+
'.ml', '.fs', '.groovy', '.jsm', '.jsx', '.tsx', '.yaml'
|
|
32
|
+
]
|
|
33
|
+
self.max_file_size = max_file_size
|
|
34
|
+
self.max_tokens = int(os.getenv("PRAISONAI_MAX_TOKENS", max_tokens))
|
|
35
|
+
self.ignore_patterns = self.get_ignore_patterns()
|
|
36
|
+
self.include_paths = self.get_include_paths()
|
|
37
|
+
self.included_files = []
|
|
38
|
+
|
|
39
|
+
def get_ignore_patterns(self):
|
|
40
|
+
"""
|
|
41
|
+
Loads ignore patterns from various sources, prioritizing them in
|
|
42
|
+
the following order:
|
|
43
|
+
1. .praisonignore
|
|
44
|
+
2. settings.yaml (under code.ignore_files)
|
|
45
|
+
3. PRAISONAI_IGNORE_FILES environment variable
|
|
46
|
+
4. .gitignore
|
|
47
|
+
5. Default patterns
|
|
48
|
+
"""
|
|
49
|
+
ignore_patterns = []
|
|
50
|
+
|
|
51
|
+
def load_from_file(filepath):
|
|
52
|
+
if os.path.exists(filepath):
|
|
53
|
+
with open(filepath, 'r') as f:
|
|
54
|
+
ignore_patterns.extend(
|
|
55
|
+
line.strip() for line in f
|
|
56
|
+
if line.strip() and not line.startswith('#')
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# 1. Load from .praisonignore
|
|
60
|
+
load_from_file(os.path.join(self.directory, '.praisonignore'))
|
|
61
|
+
|
|
62
|
+
# 2. Load from settings.yaml
|
|
63
|
+
settings_path = os.path.join(self.directory, 'settings.yaml')
|
|
64
|
+
if os.path.exists(settings_path):
|
|
65
|
+
with open(settings_path, 'r') as f:
|
|
66
|
+
settings = yaml.safe_load(f)
|
|
67
|
+
if 'code' in settings and 'ignore_files' in settings['code']:
|
|
68
|
+
ignore_patterns.extend(settings['code']['ignore_files'])
|
|
69
|
+
|
|
70
|
+
# 3. Load from environment variable
|
|
71
|
+
ignore_files_env = os.getenv("PRAISONAI_IGNORE_FILES")
|
|
72
|
+
if ignore_files_env:
|
|
73
|
+
ignore_patterns.extend(ignore_files_env.split(","))
|
|
74
|
+
|
|
75
|
+
# 4. Load from .gitignore
|
|
76
|
+
load_from_file(os.path.join(self.directory, '.gitignore'))
|
|
77
|
+
|
|
78
|
+
# 5. Default patterns (only if no patterns loaded from above sources)
|
|
79
|
+
if not ignore_patterns:
|
|
80
|
+
ignore_patterns = [
|
|
81
|
+
".*", "*.pyc", "__pycache__", ".git", ".gitignore", ".vscode",
|
|
82
|
+
".idea", ".DS_Store", "*.lock", "*.pyc", ".env", "docs", "tests",
|
|
83
|
+
"test", "tmp", "temp", "*.txt", "*.md", "*.json", "*.csv", "*.tsv",
|
|
84
|
+
"public", "*.sql", "*.sqlite", "*.db", "*.db3", "*.sqlite3",
|
|
85
|
+
"*.log", "*.zip", "*.gz", "*.tar", "*.rar", "*.7z", "*.pdf",
|
|
86
|
+
"*.jpg", "*.jpeg", "*.png", "*.gif", "*.svg", "cookbooks",
|
|
87
|
+
"assets", "__pycache__", "dist", "build", "node_modules", "venv"
|
|
88
|
+
]
|
|
89
|
+
logger.debug(f"Using default ignore patterns: {ignore_patterns}")
|
|
90
|
+
|
|
91
|
+
# Modify patterns to match directories and add leading '*' if necessary
|
|
92
|
+
modified_ignore_patterns = [
|
|
93
|
+
'*' + pattern if not pattern.startswith('.') and not pattern.startswith('*') else pattern
|
|
94
|
+
for pattern in ignore_patterns
|
|
95
|
+
]
|
|
96
|
+
logger.debug(f"Final ignore patterns: {modified_ignore_patterns}")
|
|
97
|
+
return modified_ignore_patterns
|
|
98
|
+
|
|
99
|
+
def get_include_paths(self):
|
|
100
|
+
"""
|
|
101
|
+
Loads include paths from:
|
|
102
|
+
1. .praisoninclude (includes ONLY files/directories listed)
|
|
103
|
+
2. .praisoncontext (if .praisoninclude doesn't exist, this is used
|
|
104
|
+
to include all other relevant files, excluding ignore patterns)
|
|
105
|
+
"""
|
|
106
|
+
include_paths = []
|
|
107
|
+
include_all = False # Flag to indicate if we need to include all files
|
|
108
|
+
|
|
109
|
+
include_file = os.path.join(self.directory, '.praisoncontext')
|
|
110
|
+
if os.path.exists(include_file):
|
|
111
|
+
with open(include_file, 'r') as f:
|
|
112
|
+
include_paths.extend(
|
|
113
|
+
line.strip() for line in f
|
|
114
|
+
if line.strip() and not line.startswith('#')
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# If .praisoncontext doesn't exist, fall back to .praisoninclude
|
|
118
|
+
# for including all relevant files
|
|
119
|
+
if not include_paths:
|
|
120
|
+
include_file = os.path.join(self.directory, '.praisoninclude')
|
|
121
|
+
if os.path.exists(include_file):
|
|
122
|
+
with open(include_file, 'r') as f:
|
|
123
|
+
include_paths.extend(
|
|
124
|
+
line.strip() for line in f
|
|
125
|
+
if line.strip() and not line.startswith('#')
|
|
126
|
+
)
|
|
127
|
+
include_all = True # Include all files along with specified paths
|
|
128
|
+
|
|
129
|
+
return include_paths, include_all
|
|
130
|
+
|
|
131
|
+
def should_ignore(self, file_path):
|
|
132
|
+
"""
|
|
133
|
+
Check if a file or directory should be ignored based on patterns.
|
|
134
|
+
Handles both file names and directory names for more comprehensive filtering.
|
|
135
|
+
"""
|
|
136
|
+
relative_path = os.path.relpath(file_path, self.directory)
|
|
137
|
+
if relative_path.startswith('.'):
|
|
138
|
+
return True
|
|
139
|
+
for pattern in self.ignore_patterns:
|
|
140
|
+
if fnmatch.fnmatch(relative_path, pattern) or \
|
|
141
|
+
fnmatch.fnmatch(os.path.basename(file_path), pattern):
|
|
142
|
+
return True
|
|
143
|
+
return False
|
|
144
|
+
|
|
145
|
+
def is_relevant_file(self, file_path):
|
|
146
|
+
"""Determine if a file is relevant for the context."""
|
|
147
|
+
return os.path.isfile(file_path) and \
|
|
148
|
+
os.path.getsize(file_path) <= self.max_file_size and \
|
|
149
|
+
any(file_path.endswith(ext) for ext in self.relevant_extensions)
|
|
150
|
+
|
|
151
|
+
def gather_context(self):
|
|
152
|
+
"""
|
|
153
|
+
Gather context from relevant files, respecting ignore patterns
|
|
154
|
+
and include options from .praisoninclude and .praisoncontext.
|
|
155
|
+
"""
|
|
156
|
+
context = []
|
|
157
|
+
total_files = 0
|
|
158
|
+
processed_files = 0
|
|
159
|
+
self.include_paths, include_all = self.get_include_paths()
|
|
160
|
+
|
|
161
|
+
def add_file_content(file_path):
|
|
162
|
+
"""Helper function to add file content to context."""
|
|
163
|
+
try:
|
|
164
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
165
|
+
content = f.read()
|
|
166
|
+
context.append(
|
|
167
|
+
f"File: {file_path}\n\n{content}\n\n{'=' * 50}\n"
|
|
168
|
+
)
|
|
169
|
+
self.included_files.append(
|
|
170
|
+
Path(file_path).relative_to(self.directory)
|
|
171
|
+
)
|
|
172
|
+
except Exception as e:
|
|
173
|
+
logger.error(f"Error reading {file_path}: {e}")
|
|
174
|
+
|
|
175
|
+
def process_path(path):
|
|
176
|
+
"""Helper function to process a single path (file or directory)."""
|
|
177
|
+
nonlocal total_files, processed_files
|
|
178
|
+
if os.path.isdir(path):
|
|
179
|
+
for root, dirs, files in os.walk(path):
|
|
180
|
+
total_files += len(files)
|
|
181
|
+
dirs[:] = [
|
|
182
|
+
d
|
|
183
|
+
for d in dirs
|
|
184
|
+
if not self.should_ignore(os.path.join(root, d))
|
|
185
|
+
]
|
|
186
|
+
for file in files:
|
|
187
|
+
file_path = os.path.join(root, file)
|
|
188
|
+
if not self.should_ignore(file_path) and self.is_relevant_file(file_path):
|
|
189
|
+
add_file_content(file_path)
|
|
190
|
+
processed_files += 1
|
|
191
|
+
print(
|
|
192
|
+
f"\rProcessed {processed_files}/{total_files} files",
|
|
193
|
+
end="",
|
|
194
|
+
flush=True,
|
|
195
|
+
)
|
|
196
|
+
elif os.path.isfile(path) and self.is_relevant_file(path):
|
|
197
|
+
add_file_content(path)
|
|
198
|
+
processed_files += 1
|
|
199
|
+
print(
|
|
200
|
+
f"\rProcessed {processed_files}/1 files",
|
|
201
|
+
end="",
|
|
202
|
+
flush=True,
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
if include_all:
|
|
206
|
+
# Include ALL relevant files from the entire directory
|
|
207
|
+
process_path(self.directory)
|
|
208
|
+
|
|
209
|
+
# Include files from .praisoninclude specifically
|
|
210
|
+
for include_path in self.include_paths:
|
|
211
|
+
full_path = os.path.join(self.directory, include_path)
|
|
212
|
+
process_path(full_path)
|
|
213
|
+
elif self.include_paths:
|
|
214
|
+
# Include only files specified in .praisoncontext
|
|
215
|
+
for include_path in self.include_paths:
|
|
216
|
+
full_path = os.path.join(self.directory, include_path)
|
|
217
|
+
process_path(full_path)
|
|
218
|
+
else:
|
|
219
|
+
# No include options, process the entire directory
|
|
220
|
+
process_path(self.directory)
|
|
221
|
+
|
|
222
|
+
print() # New line after progress indicator
|
|
223
|
+
return "\n".join(context)
|
|
224
|
+
|
|
225
|
+
def count_tokens(self, text):
|
|
226
|
+
"""Count tokens using a simple whitespace-based tokenizer."""
|
|
227
|
+
return len(text.split())
|
|
228
|
+
|
|
229
|
+
def truncate_context(self, context):
|
|
230
|
+
"""Truncate context to stay within the token limit."""
|
|
231
|
+
tokens = context.split()
|
|
232
|
+
if len(tokens) > self.max_tokens:
|
|
233
|
+
truncated_context = ' '.join(tokens[:self.max_tokens])
|
|
234
|
+
logger.warning("Context truncated due to token limit.")
|
|
235
|
+
return truncated_context
|
|
236
|
+
return context
|
|
237
|
+
|
|
238
|
+
def save_context(self, context):
|
|
239
|
+
"""Save the gathered context to a file."""
|
|
240
|
+
with open(self.output_file, 'w', encoding='utf-8') as f:
|
|
241
|
+
f.write(context)
|
|
242
|
+
|
|
243
|
+
def get_context_tree(self):
|
|
244
|
+
"""Generate a formatted tree structure of included files and folders."""
|
|
245
|
+
tree = []
|
|
246
|
+
start_dir = Path(self.directory)
|
|
247
|
+
|
|
248
|
+
def add_to_tree(path, prefix=''):
|
|
249
|
+
contents = sorted(path.iterdir())
|
|
250
|
+
pointers = [('└── ' if i == len(contents) - 1 else '├── ') for i in range(len(contents))]
|
|
251
|
+
for pointer, item in zip(pointers, contents):
|
|
252
|
+
rel_path = item.relative_to(start_dir)
|
|
253
|
+
if rel_path in self.included_files:
|
|
254
|
+
tree.append(f"{prefix}{pointer}{rel_path}")
|
|
255
|
+
|
|
256
|
+
if item.is_dir():
|
|
257
|
+
add_to_tree(item, prefix + (' ' if pointer == '└── ' else '│ '))
|
|
258
|
+
|
|
259
|
+
add_to_tree(start_dir)
|
|
260
|
+
return '\n'.join(tree)
|
|
261
|
+
|
|
262
|
+
def run(self):
|
|
263
|
+
"""Execute the context gathering, truncation, and reporting."""
|
|
264
|
+
context = self.gather_context()
|
|
265
|
+
context = self.truncate_context(context)
|
|
266
|
+
token_count = self.count_tokens(context)
|
|
267
|
+
print(f"Context gathered successfully.")
|
|
268
|
+
print(f"Total number of tokens (estimated): {token_count}")
|
|
269
|
+
# self.save_context(context)
|
|
270
|
+
context_tree = self.get_context_tree()
|
|
271
|
+
logger.debug(f"Context tree:\n{context_tree}")
|
|
272
|
+
return context, token_count, context_tree
|
|
273
|
+
|
|
274
|
+
def main():
|
|
275
|
+
gatherer = ContextGatherer()
|
|
276
|
+
context, token_count, context_tree = gatherer.run()
|
|
277
|
+
print(context_tree)
|
|
278
|
+
print(f"\nThe context contains approximately {token_count} tokens.")
|
|
279
|
+
print("First 500 characters of context:")
|
|
280
|
+
print(context[:500] + "...")
|
|
281
|
+
|
|
282
|
+
if __name__ == "__main__":
|
|
283
|
+
main()
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
|
+
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M840.5 798.2L662.3 599.5l-151 173.7-173.7-173.7-167.7 201c-21 30.4 0.9 71.8 37.9 71.6l594.7-3.3c36.2-0.1 57.8-40.3 38-70.6z" fill="#FFB89A" /><path d="M741.6 647.3l-52.3-47.7c-12.2-11.2-31.2-10.3-42.4 1.9s-10.3 31.2 1.9 42.4l52.3 47.7c5.8 5.3 13 7.8 20.2 7.8 8.1 0 16.2-3.3 22.2-9.8 11.2-12.1 10.3-31.1-1.9-42.3zM631.2 546.5c-12.4-11-31.4-9.8-42.3 2.6l-98.8 111.7-171-165.7L87.9 724.7c-11.8 11.7-11.8 30.7-0.1 42.4 5.9 5.9 13.6 8.9 21.3 8.9 7.6 0 15.3-2.9 21.1-8.7l189.4-188.1 173.8 168.5L633.8 589c11-12.5 9.8-31.5-2.6-42.5z" fill="#33CC99" /><path d="M721.3 342.8m-35.1 0a35.1 35.1 0 1 0 70.2 0 35.1 35.1 0 1 0-70.2 0Z" fill="#33CC99" /><path d="M743.2 175.1H191.6c-70.6 0-128.3 57.7-128.3 128.3v499.2c0 70.6 57.7 128.3 128.3 128.3h551.5c70.6 0 128.3-57.7 128.3-128.3V303.5c0.1-70.6-57.7-128.4-128.2-128.4z m68.3 627.6c0 18.1-7.1 35.2-20.1 48.2-13 13-30.1 20.1-48.2 20.1H191.6c-18.1 0-35.2-7.1-48.2-20.1-13-13-20.1-30.1-20.1-48.2V303.5c0-18.1 7.1-35.2 20.1-48.2 13-13 30.1-20.1 48.2-20.1h551.5c18.1 0 35.2 7.1 48.2 20.1 13 13 20.1 30.1 20.1 48.2v499.2z" fill="#45484C" /><path d="M799.7 90.9H237.2c-16.6 0-30 13.4-30 30s13.4 30 30 30h562.4c26.1 0 50.8 10.3 69.4 28.9 18.6 18.6 28.9 43.3 28.9 69.4v482.4c0 16.6 13.4 30 30 30s30-13.4 30-30V249.2C958 161.9 887 90.9 799.7 90.9z" fill="#45484C" /></svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
|
+
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M570.2 842c-50.6 0-278.7-180-278.7-401.9 0-58.8-2.9-133.1-1-183.9-50.8 3.2-91.4 45.7-91.4 97.3v272.1c37.4 194.7 137.5 334 255.2 334 69.5 0 132.9-48.6 180.9-128.5-20.8 7.1-42.6 10.9-65 10.9z" fill="#FFB89A" /><path d="M926.1 191.8C900.5 74.1 817.9 62.1 704.9 62.1c-29.1 0-60.3 0.8-93 0.8-36 0-70.5-1.1-102.5-1.1-109.7 0-189.8 12.5-201.3 123.7-20.4 198.3 30 617.1 306.1 617.1S939 414.3 926.1 191.8z m-76.9 268.5c-9.5 47.9-22.3 90.8-38.1 127.7-16.8 39.2-37 71.4-60 95.8-37.3 39.5-82.1 58.7-137 58.7-53.4 0-97.6-20.1-134.9-61.6-45.5-50.5-79.8-131.5-99-234.2-15.6-83.5-20.3-178.9-12.4-255.2 1.8-17.3 5.7-30.7 11.6-39.8 4.4-6.8 10.1-11.7 18.7-15.8 25.8-12.5 70.8-14.2 111.4-14.2 15 0 30.7 0.2 47.3 0.5 17.8 0.3 36.2 0.6 55.2 0.6 17.2 0 33.9-0.2 50-0.4 15.1-0.2 29.3-0.4 43.1-0.4 44.5 0 89.5 1.8 118 15.1 15.9 7.4 33.4 20.8 43.6 63 2.6 53.3 3.6 153.5-17.5 260.2z" fill="#4E5155" /><path d="M532 841.7c-32.5 22.3-70.6 33.7-113.2 33.7-29.7 0-57.3-6-82.1-17.7-23.2-11-44.7-27.4-63.9-48.7-46-50.9-80.3-131.3-99.2-232.4-15.1-80.6-19.6-172.9-12-246.8 3-29.5 12-50.2 27.5-63.2 14.2-12 35.1-19.2 65.8-22.9 16.5-2 28.2-16.9 26.3-33.3-2-16.5-16.9-28.2-33.3-26.3-42.9 5.1-73.8 16.7-97.4 36.5-27.9 23.5-43.8 57.2-48.5 103-8.2 79.3-3.4 178.1 12.7 264 9.7 51.9 23.4 99.4 40.6 141.2 19.8 48.1 44.4 88.6 73 120.4 51.6 57.2 115.7 86.2 190.6 86.2 55 0 104.5-14.9 147.2-44.2 13.7-9.4 17.1-28.1 7.7-41.7-9.4-13.7-28.1-17.2-41.8-7.8z" fill="#4E5155" /><path d="M519.7 248.5c-16.6 0-30 13.4-30 30v91.3c0 16.6 13.4 30 30 30s30-13.4 30-30v-91.3c0-16.6-13.5-30-30-30zM299.5 385.5c0-16.6-13.4-30-30-30s-30 13.4-30 30v91.3c0 16.6 13.4 30 30 30s30-13.4 30-30v-91.3zM754.6 248.5c-16.6 0-30 13.4-30 30v91.3c0 16.6 13.4 30 30 30s30-13.4 30-30v-91.3c0-16.6-13.4-30-30-30zM716.7 554.5c0-16.6-13.4-30-30-30H551v30c0 58.5 38.1 123.7 92.8 123.7 22.9 0 45-11.9 62.2-33.6 10.3-13 8.1-31.9-4.9-42.1-13-10.3-31.9-8.1-42.1 4.9-5.3 6.7-11.1 10.9-15.1 10.9-4.3 0-11.9-5.1-19.1-16.4-3.3-5.3-6.2-11.2-8.4-17.4h70.4c16.4 0 29.9-13.4 29.9-30zM401.6 704c-25.4 0-46.1-24.2-46.1-53.9 0-16.6-13.4-30-30-30s-30 13.4-30 30c0 62.8 47.6 113.9 106.1 113.9 16.6 0 30-13.4 30-30s-13.5-30-30-30z" fill="#33CC99" /></svg>
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
|
+
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M861.9 383.8H218.1c-36.4 0-66.1-29.8-66.1-66.1V288c0-36.4 29.8-66.1 66.1-66.1h643.8c36.4 0 66.1 29.8 66.1 66.1v29.7c0 36.3-29.8 66.1-66.1 66.1z" fill="#FFB89A" /><path d="M822.9 129.2H199.8c-77.2 0-140.4 63.2-140.4 140.4v487.2c0 77.2 63.2 140.4 140.4 140.4h623.1c77.2 0 140.4-63.2 140.4-140.4V269.6c0-77.2-63.2-140.4-140.4-140.4z m80.4 177H760.4L864.6 201c5.4 3.3 10.4 7.3 15 11.8 15.3 15.3 23.7 35.4 23.7 56.8v36.6z m-673.3 0l104-117h61.3l-109.1 117H230z m247.4-117h169.2L532 306.2H368.3l109.1-117z m248.8 0h65.6L676 306.2h-60l112.5-114.8-2.3-2.2zM143 212.9c15.3-15.3 35.4-23.7 56.8-23.7h53.9l-104 117h-30.4v-36.5c0.1-21.4 8.5-41.5 23.7-56.8z m736.6 600.7c-15.3 15.3-35.4 23.7-56.8 23.7h-623c-21.3 0-41.5-8.4-56.8-23.7-15.3-15.3-23.7-35.4-23.7-56.8V366.2h783.9v390.6c0.1 21.3-8.3 41.5-23.6 56.8z" fill="#45484C" /><path d="M400.5 770.6V430.9L534.1 508c14.3 8.3 19.3 26.6 11 41-8.3 14.3-26.6 19.3-41 11l-43.6-25.2v131.8l114.1-65.9-7.5-4.3c-14.3-8.3-19.3-26.6-11-41 8.3-14.3 26.6-19.3 41-11l97.5 56.3-294.1 169.9z" fill="#33CC99" /></svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
|
3
|
+
<svg width="800px" height="800px" viewBox="0 0 1024 1024" class="icon" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M188.3 766.5a94.4 135.8 0 1 0 188.8 0 94.4 135.8 0 1 0-188.8 0Z" fill="#FFB89A" /><path d="M931.5 397s0-0.1 0 0c-34.2-82.6-119.3-141-218.8-141-129.7 0-234.9 99.3-234.9 221.9 0 52.1 19.1 100.1 50.9 138 1 14.5 1.8 29.1 1.8 43.6 0 148.5 98.1 269 219.2 269 121 0 219.2-120.4 219.2-269 0-70.1-1.7-214.7-37.4-262.5z m-36.6 347.5c-8.7 25.3-21.1 47.9-36.8 67.1-29.8 36.5-68.3 56.7-108.5 56.7s-78.7-20.1-108.5-56.7c-15.7-19.2-28-41.8-36.8-67.1-9.3-26.9-13.9-55.5-13.9-85.1 0-16.8-1-33.5-2-47.7l-1.3-19.5-12.6-15c-24.1-28.6-36.8-63-36.8-99.3 0-89.3 78.5-161.9 174.9-161.9 36.4 0 71.4 10.3 101 29.7 28.4 18.7 65.5 81.7 65.5 81.7s17.9 27.5 24.7 98.2c4.5 46.5 5 95.9 5 133.8 0.1 29.6-4.6 58.2-13.9 85.1zM377.1 219.9c-51.8 0-93.8 42-93.8 93.8s42 93.8 93.8 93.8 93.8-42 93.8-93.8-42-93.8-93.8-93.8z m0 127.5c-18.6 0-33.8-15.2-33.8-33.8 0-18.6 15.2-33.8 33.8-33.8 18.6 0 33.8 15.2 33.8 33.8 0 18.7-15.1 33.8-33.8 33.8z" fill="#45484C" /><path d="M521.2 206.7m-50.3 0a50.3 50.3 0 1 0 100.6 0 50.3 50.3 0 1 0-100.6 0Z" fill="#45484C" /><path d="M653 156.4m-50.3 0a50.3 50.3 0 1 0 100.6 0 50.3 50.3 0 1 0-100.6 0Z" fill="#45484C" /><path d="M781.9 158.4m-50.3 0a50.3 50.3 0 1 0 100.6 0 50.3 50.3 0 1 0-100.6 0Z" fill="#45484C" /><path d="M909 206.7m-50.3 0a50.3 50.3 0 1 0 100.6 0 50.3 50.3 0 1 0-100.6 0Z" fill="#45484C" /><path d="M263.9 602.7c44.7 0 81 31.5 81 70.3 0 20.9-10.2 35.9-18.7 44.8l-15.9 19.7-0.5 27.2c0.7 7.2 0.6 16.9 0.6 24.7v4.8c0 33.7-27.4 61.2-61.2 61.2-14.9 0-33.3-9.6-48.1-25-15.2-15.9-24.6-35.9-24.6-52.3v-3.2c0-12.7 0-36.2 1-60.2 1.4-33 7.4-57.3 7.4-57.3 3.9-14.7 13.4-28.2 26.8-38 14.8-11 32.8-16.7 52.2-16.7m0-60c-66.4 0-122 42.4-137 99.4-10.9 23-10.4 112.6-10.4 135.9 0 66.9 65.8 137.3 132.7 137.3 66.9 0 121.2-54.3 121.2-121.2 0-9.2 0.3-23-0.8-34.9 22-23 35.4-53.2 35.4-86.3-0.1-71.9-63.2-130.2-141.1-130.2zM444.4 559.9c-26.4 0-47.8 21.4-47.8 47.8s21.4 47.8 47.8 47.8 47.8-21.4 47.8-47.8-21.4-47.8-47.8-47.8zM377.1 494.5c-15.2 0-27.5 12.3-27.5 27.5s12.3 27.5 27.5 27.5 27.5-12.3 27.5-27.5c0-15.3-12.3-27.5-27.5-27.5zM288.1 471.5c-15.2 0-27.5 12.3-27.5 27.5s12.3 27.5 27.5 27.5 27.5-12.3 27.5-27.5-12.4-27.5-27.5-27.5zM188.3 477.9c-15.2 0-27.5 12.3-27.5 27.5s12.3 27.5 27.5 27.5 27.5-12.3 27.5-27.5-12.3-27.5-27.5-27.5zM100.6 538.4c-15.2 0-27.5 12.3-27.5 27.5s12.3 27.5 27.5 27.5 27.5-12.3 27.5-27.5c-0.1-15.2-12.4-27.5-27.5-27.5z" fill="#45484C" /><path d="M670.1 584.6c-41.4 0-80.2-20.3-103.9-54.3-9.5-13.6-6.2-32.3 7.4-41.8 13.6-9.5 32.3-6.2 41.8 7.4 12.5 17.9 33 28.6 54.7 28.6 36.8 0 66.7-29.9 66.7-66.7 0-19.8-8.7-38.4-23.9-51.2-12.7-10.6-14.4-29.6-3.7-42.3s29.6-14.4 42.3-3.7c28.9 24.2 45.4 59.6 45.4 97.2-0.1 70-56.9 126.8-126.8 126.8z" fill="#33CC99" /><path d="M853 556.4c-26 0-49.6-14.5-60.1-36.9-7-15-0.6-32.9 14.4-39.9s32.9-0.6 39.9 14.4c0.3 0.6 2.2 2.4 5.8 2.4 1.2 0 2.3-0.2 3.3-0.6 15.5-5.9 32.8 1.8 38.7 17.3 5.9 15.5-1.8 32.8-17.3 38.7-7.9 3.1-16.2 4.6-24.7 4.6z" fill="#33CC99" /></svg>
|