تخيل أنك تتصفح مواقع التوثيق أو دروس البرمجة ولا تشعر بالوحدة أبدًا. بدلاً من التنقل في كل شيء بنفسك، لديك مساعد ذكاء اصطناعي يتبعك من صفحة إلى أخرى. إنه غير مرتبط بأي صفحة ويب واحدة، بل يكون دائمًا بجانبك جاهزًا للمساعدة. يمكنه تنفيذ الكود الذي تصادفه عبر الإنترنت بأمان، وتقديم الشروحات، وإعطاؤك رؤى في اللحظة التي تحتاجها فيها.
إذًا كيف تجلب هذه التجربة إلى الواقع؟ ببناء امتداد متصفح. سيتضمن هذا الامتداد وكيل ذكاء اصطناعي يمكن للمستخدم الدردشة معه، وسيكون للوكيل إمكانية الوصول إلى آلة حاوية رملية آمنة حيث يمكنه تشغيل الكود وأداء عمليات أخرى بأمان.
لإنشاء هذا النظام، سنقوم ببناء إضافة كروم، ونستخدم نموذج Novita قادر على استخدام الأدوات، وندمج Novita Sandbox كبيئة تنفيذ آمنة للوكيل. في هذا المقال، سنمر بالعملية الكاملة لبنائه.

بنهاية هذا البرنامج التعليمي، ستتعلم:
- كيفية بناء إضافة كروم تتكامل مع وكيل ذكاء اصطناعي
- كيفية الاستفادة من النماذج اللغوية الكبيرة الوكيلة لـ Novita
- كيفية إعداد Novita Sandbox كبيئة آمنة لوكيل المتصفح
- كيفية جعل الامتداد يتواصل في الوقت الفعلي مع الوكيل
الحاوية الرملية: الأداة الوحيدة التي تحتاجها
تعتمد إضافة كروم التي نقوم ببنائها على وكيل ذكاء اصطناعي لمساعدة المستخدم. وبما أن هذه الإضافة مخصصة للعمل كمساعد برمجة، يحتاج الوكيل إلى القدرة على تشغيل الكود، وإنشاء الملفات، وفحص المخرجات، وأداء جميع المهام النموذجية التي قد يقوم بها المطور. قد تتوقع أنك ستحتاج إلى قائمة طويلة من الأدوات لجعل هذا ممكنًا، ولكن في الواقع، تحتاج إلى أداة واحدة فقط: حاوية رملية.
تمنح الحاوية الرملية الوكيل إمكانية الوصول إلى بيئة لينكس حيث يمكنه تشغيل الأوامر، وإنشاء وتعديل الملفات، وأداء أي عملية تقوم بها عادة في الطرفية. لهذا المشروع، سنستخدم Novita Sandbox.
لإعدادها، أولاً قم بتثبيت حزمة Novita Sandbox:
pip install novita-sandbox
بعد ذلك، اضبط متغير البيئة NOVITA_API_KEY على مفتاح API الخاص بك. بمجرد الانتهاء من ذلك، يمكنك إنشاء حاوية رملية واستخدامها كما يلي:
from novita_sandbox.code_interpreter import Sandbox
sandbox = Sandbox.create()
result = sandbox.commands.run('ls -l')
print(result)
sandbox.kill()
ينشئ هذا المقتطف حاوية رملية، ويشغل أمر ls -l، ويطبع المخرجات، ثم يغلق الحاوية الرملية. هذا سير العمل البسيط هو الأساس الذي سيعتمد عليه مساعد المتصفح لدينا لاستخدام الحاوية الرملية لمساعدة المستخدمين.
الآن لنأخذ هذا المبدأ ونطبقه على الإضافة الكاملة.
بنية وكيل مساعد المتصفح
تتبع بنية هذا_project نموذج العميل والخادم. تعمل إضافة كروم كعميل، بينما يستضيف خادم خلفي مخصص كل من وكيل الذكاء الاصطناعي وبيئة الحاوية الرملية.
تتواصل الإضافة مع الخادم عبر اتصال WebSocket. يسمح هذا بالمراسلة ثنائية الاتجاه في الوقت الفعلي، بحيث تتدفق طلبات المستخدم واستجابات الوكيل فورًا دون تأخر ملحوظ. يتواصل الخادم بدوره مع واجهات برمجة تطبيقات Novita التي تتضمن نقطة نهاية النموذج وخدمة الحاوية الرملية.
معًا، تشكل الإضافة والخادم مساعد متصفح ذكي قادر على تشغيل الكود بأمان، ومعالجة المعلومات بسرعة، وتقديم شروحات مفيدة مباشرة داخل تجربة تصفح المستخدم.
بناء الإضافة
الآن بعد أن فهمنا البنية العامة، يمكننا البدء في تنفيذ الإضافة نفسها. سنبدأ بخادم الإضافة.
خادم الإضافة
خادم الإضافة هو خدمة WebSocket بسيطة مع نقطة نهاية واحدة /ws. تتلقى هذه النقطة النهائية رسائل من المستخدم وتعيد استجابات النموذج اللغوي الكبير في الوقت الفعلي. كما تتعامل مع استدعاءات الأدوات عن طريق استدعاء الحاوية الرملية كلما احتاج الوكيل إلى تنفيذ كود أو أداء عملية.
الاعتماديات
يعتمد الخادم على ثلاث مكتبات أساسية:
- FastAPI: إطار عمل HTTP الذي يوفر تنفيذ WebSocket
- OpenAI: حزمة تطوير البرمجيات (SDK) المستخدمة للتواصل مع نماذج Novita
- Novita Sandbox: البيئة الآمنة حيث يتم تنفيذ الكود بأمان
قم بتثبيتها باستخدام:
pip install novita-sandbox "fastapi[standard]" openai
اضبط مفتاح API الخاص بـ Novita كمتغير بيئة:
export NOVITA_API_KEY = sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
كتابة كود الخادم
ابدأ باستيراد الوحدات المطلوبة:
import os
import json
import uvicorn
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from novita_sandbox.code_interpreter import Sandbox
from openai import OpenAI
تهيئة عميل LLM
بعد ذلك، أنشئ عميل OpenAI يشير إلى واجهة برمجة تطبيقات Novita. في هذا المثال نستخدم نموذج llama-3.3-70b-instruct، ولكن أي نموذج Novita يدعم استدعاء الأدوات سيعمل.
client = OpenAI(
base_url="https://api.novita.ai/openai",
api_key=os.environ["NOVITA_API_KEY"],
)
model = "meta-llama/llama-3.3-70b-instruct"
تحديد مخطط الأدوات
سيستخدم الوكيل أربع أدوات، كل منها يتفاعل مع الحاوية الرملية:
- read_file: يقرأ محتوى ملف
- write_file: ينشئ ملفًا واحدًا ويكتب عليه
- write_files: ينشئ ملفات متعددة ويكتب عليها
- run_commands: ينفذ أوامر shell داخل دليل عمل الحاوية الرملية
إليك مخطط الأدوات الكامل:
tools = [
{
"type": "function",
"function": {
"name": "read_file",
"description": "Read contents of a file inside the sandbox",
"parameters": {
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"],
},
},
},
{
"type": "function",
"function": {
"name": "write_file",
"description": "Write a single file inside the sandbox",
"parameters": {
"type": "object",
"properties": {
"path": {"type": "string"},
"data": {"type": "string"},
},
"required": ["path", "data"],
},
},
},
{
"type": "function",
"function": {
"name": "write_files",
"description": "Write multiple files inside the sandbox",
"parameters": {
"type": "object",
"properties": {
"files": {
"type": "array",
"items": {
"type": "object",
"properties": {
"path": {"type": "string"},
"data": {"type": "string"},
},
"required": ["path", "data"],
},
}
},
"required": ["files"],
},
},
},
{
"type": "function",
"function": {
"name": "run_commands",
"description": "Run a shell command inside the sandbox working directory",
"parameters": {
"type": "object",
"properties": {
"command": {"type": "string"},
},
"required": ["command"],
},
},
},
]
تكوين خادم HTTP
قم بإعداد FastAPI وتمكين CORS حتى يتمكن امتداد كروم من إرسال طلبات إلى الخادم.
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_methods=["*"],
allow_headers=["*"],
)
تنفيذ الأدوات
حدد دالة معالجة تنفذ كل أداة وتوجه استدعاءات أدوات الوكيل إلى الحاوية الرملية:
def make_tool_handlers(sandbox):
def read_file(path: str):
print(f"[LOG] read_file called with path: {path}")
try:
content = sandbox.files.read(path)
print(f"[LOG] read_file result: {content}")
return content
except Exception as e:
return f"Error reading file: {e}"
def write_file(path: str, data: str):
print(f"[LOG] write_file called with path: {path}")
try:
sandbox.files.write(path, data)
return f"File created successfully at {path}"
except Exception as e:
return f"Error writing file: {e}"
def write_files(files: list):
print(f"[LOG] write_files called with {len(files)} files")
try:
sandbox.files.write_files(files)
return f"{len(files)} file(s) created successfully"
except Exception as e:
return f"Error writing multiple files: {e}"
def run_commands(command: str):
print(f"[LOG] run_commands called with command: {command}")
try:
result = sandbox.commands.run(command)
return result.stdout
except Exception as e:
return f"Error running command: {e}"
return {
"read_file": read_file,
"write_file": write_file,
"write_files": write_files,
"run_commands": run_commands,
}
إنشاء نقطة نهاية WebSocket
الآن لننفذ نقطة نهاية WebSocket. عندما يتصل المستخدم بالنقطة النهائية، يتم إنشاء مثيل حاوية رملية جديدة.
كل التواصل بين المستخدم والوكيل يمر عبر هذا الاتصال. إذا طلب المستخدم من الوكيل استخدام أداة، يختار الوكيل وينفذ الأداة المناسبة عبر دالة المعالجة.
عند إغلاق الاتصال، يتم إنهاء الحاوية الرملية.
@app.websocket("/ws")
async def websocket_endpoint(ws: WebSocket):
await ws.accept()
print("\
[WS] Client connected")
# Create sandbox per connection
sandbox = Sandbox.create(timeout=1200)
print("[WS] Sandbox created")
tools_exec = make_tool_handlers(sandbox)
messages = [] # persistent inside this websocket
try:
while True:
data = await ws.receive_text()
print(f"[WS] Received message: {data}")
# Add user message
messages.append({"role": "user", "content": data})
# LLM call
response = client.chat.completions.create(
model=model,
messages=messages,
tools=tools,
)
assistant_msg = response.choices[0].message
messages.append(assistant_msg)
# If LLM wants to call tools
if assistant_msg.tool_calls:
print(f"[WS] Assistant requested {len(assistant_msg.tool_calls)} tool call(s).")
results = []
for tool_call in assistant_msg.tool_calls:
fn_name = tool_call.function.name
fn_args = json.loads(tool_call.function.arguments)
print(f"[WS] Tool call: {fn_name} args={fn_args}")
if fn_name in tools_exec:
result = tools_exec[fn_name](**fn_args)
else:
result = f"Error: Unknown tool {fn_name}"
results.append(result)
messages.append({
"tool_call_id": tool_call.id,
"role": "tool",
"content": str(result),
})
# Follow-up model call
follow_up = client.chat.completions.create(
model=model,
messages=messages,
)
final_answer = follow_up.choices[0].message
messages.append(final_answer)
await ws.send_json({
"reply": final_answer.content,
"tool_output": results,
})
else:
# Simple model text output
await ws.send_json({"reply": assistant_msg.content})
except WebSocketDisconnect:
print("[WS] Client disconnected")
finally:
sandbox.kill()
print("[WS] Sandbox terminated")
تشغيل الخادم
أخيرًا، استخدم Uvicorn لتشغيل الخدمة:
if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8000)
هذا يكمل مكون الخادم.
الإضافة
الإضافة هي الواجهة التي يتفاعل معها المستخدم. تتكون من مجموعة صغيرة من الملفات التي تعمل معًا للعمل على أي صفحة ويب. بمجرد تفعيلها، يمكن للمستخدم التواصل مع خادم الإضافة في الوقت الفعلي مباشرة من الصفحة التي يتصفحها.
تتضمن الإضافة الملفات التالية:
- manifest.json: يحدد تكوين الإضافة وصلاحياتها
- background.js: يحتوي على منطق عامل الخدمة ويتعامل مع إجراءات قائمة السياق
- content.js: يدير التفاعلات داخل الصفحة ويعرض مربع حوار المساعد
- styles.css: يوفر التنسيق لنافذة المساعد الموجودة على الصفحة
كل ملف له مسؤولية منفصلة، ومعًا يشكلون امتدادًا كاملاً وعاملاً.
كيف تعمل الإضافة
قبل تنفيذ الملفات، لنلقي نظرة سريعة على كيفية عمل الإضافة من وجهة نظر المستخدم:

- ينقر المستخدم بزر الماوس الأيمن على أي صفحة ويب، وتظهر قائمة السياق.
- يختار “Agent Sandbox” من القائمة.
- تفتح الإضافة مربع حوار المساعد على الصفحة.
- ينقر المستخدم على Connect لإنشاء اتصال مع خادم الإضافة.
- بمجرد إنشاء الاتصال، يمكن للمستخدم البدء في الكتابة مباشرة في مربع الرسائل.
- بعد كتابة رسالتهم، ينقرون ببساطة على Send لإرسالها إلى الخادم.
- إذا أرادوا تزويد الوكيل بسياق الصفحة، يمكنهم النقر على Extract لالتقاط كل المحتوى المرئي من صفحة الويب.
- يمكنهم أيضًا إضافة سياق إضافي يدويًا قبل النقر على Send مرة أخرى.

الآن بعد أن فهمنا سير العمل، لنبدأ في تنفيذ ملفات الإضافة.
manifest.json
الملف الأول الذي ننشئه هو manifest.json، الذي يكوّن صلاحيات الإضافة ومنطق الخلفية ونصوص المحتوى.
{
"manifest_version": 3,
"name": "Agent Sandbox",
"version": "1.0",
"description": "Chat with your Novita AI sandbox agent over WebSocket.",
"permissions": [
"contextMenus",
"activeTab",
"scripting"
],
"host_permissions": [
"ws://localhost:8000/*",
"http://localhost:8000/*",
"https://localhost:8000/*"
],
"background": {
"service_worker": "background.js"
},
"action": {
"default_icon": "icon.png"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content.js"],
"css": ["styles.css"]
}
]
}
يخبر هذا الملف كروم بالنصوص التي يجب تحميلها، والصلاحيات التي نحتاجها، وأي ملف يعمل كعامل خدمة.
background.js
نص الخلفية هو عامل الخدمة الذي يعمل في الخلفية. إنه مسؤول عن إضافة امتدادنا إلى قائمة سياق كروم والاستماع إلى تفاعلات المستخدم. عندما يختار المستخدم خيار القائمة الخاص بنا، يرسل نص الخلفية رسالة إلى نص المحتوى، الذي يقوم بعد ذلك بتفعيل مربع حوار الامتداد.
chrome.runtime.onInstalled.addListener(() => {
chrome.contextMenus.create({
id: "ask-assistant",
title: "Agent Sandbox",
contexts: ["all"]
});
});
chrome.contextMenus.onClicked.addListener((info, tab) => {
chrome.scripting.executeScript(
{
target: { tabId: tab.id },
files: ["content.js"]
},
() => {
chrome.tabs.sendMessage(tab.id, {
type: "OPEN_PANEL"
});
}
);
});
content.js
نص المحتوى مسؤول عن عرض مربع حوار الامتداد داخل صفحة الويب. عندما يتلقى رسالة من نص الخلفية، يفتح المربع الحواري. هذا النص هو جافا سكريبت عادي. إنه يدير واجهة المستخدم من خلال عمليات DOM القياسية ويستخدم واجهة برمجة تطبيقات WebSocket للتواصل مع خادم الإضافة.
let socket = null;
chrome.runtime.onMessage.addListener((msg) => {
if (msg.type === "OPEN_PANEL") {
openPanel();
}
});
// -------------------------------------------------
// WEBPAGE TEXT EXTRACTOR
// -------------------------------------------------
function extractWebpageContent() {
const cloned = document.cloneNode(true);
cloned.querySelectorAll("script, style, iframe, noscript").forEach(e => e.remove());
let main =
cloned.querySelector("article") ||
cloned.querySelector("main") ||
cloned.querySelector("#content") ||
cloned.body;
const text = Array.from(main.querySelectorAll("h1, h2, h3, p"))
.map(el => el.innerText.trim())
.filter(Boolean)
.join("\
\
");
return text;
}
// -------------------------------------------------
// PANEL UI
// -------------------------------------------------
function openPanel() {
const old = document.getElementById("assistant-box");
if (old) old.remove();
const box = document.createElement("div");
box.id = "assistant-box";
box.innerHTML = `
<div id="assistant-container">
<div id="assistant-header">
<h3>Code Assistant</h3>
<button id="assistant-close">×</button>
</div>
<textarea id="assistant-input" placeholder="Message Agent..."></textarea>
<div class="btn-row">
<button id="connect-btn">Connect</button>
<button id="disconnect-btn">Disconnect</button>
<button id="send-btn">Send</button>
<button id="extract-btn">Extract</button>
</div>
<div id="assistant-result">Not connected.</div>
</div>
`;
document.body.appendChild(box);
document.getElementById("assistant-close").onclick = () => box.remove();
const resultBox = document.getElementById("assistant-result");
const inputBox = document.getElementById("assistant-input");
// -------------------------------------------------
// TEXT SELECTION LISTENER
// -------------------------------------------------
document.addEventListener("mouseup", () => {
const selection = window.getSelection().toString().trim();
if (!selection) return;
// Append selected text to message box
inputBox.value += (inputBox.value ? "\
\
" : "") + selection;
// Scroll text area to bottom
inputBox.scrollTop = inputBox.scrollHeight;
});
// -------------------------------------------------
// CONNECT
// -------------------------------------------------
document.getElementById("connect-btn").onclick = () => {
if (socket && socket.readyState === WebSocket.OPEN) {
resultBox.innerText = "Already connected.";
return;
}
socket = new WebSocket("ws://localhost:8000/ws");
socket.onopen = () => {
resultBox.innerText = "Connected to WebSocket.";
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
resultBox.innerText += "\
\
Assistant:\
" + data.reply;
resultBox.scrollTop = resultBox.scrollHeight;
};
socket.onerror = () => {
resultBox.innerText = "WebSocket error.";
};
socket.onclose = () => {
resultBox.innerText = "Disconnected.";
};
};
// -------------------------------------------------
// DISCONNECT
// -------------------------------------------------
document.getElementById("disconnect-btn").onclick = () => {
if (socket) socket.close();
};
// -------------------------------------------------
// SEND MESSAGE
// -------------------------------------------------
document.getElementById("send-btn").onclick = () => {
const context = inputBox.value;
if (!socket || socket.readyState !== WebSocket.OPEN) {
resultBox.innerText = "Not connected.";
return;
}
socket.send(JSON.stringify({ message: context }));
inputBox.value = "";
resultBox.innerText += "\
\
You:\
" + context;
};
// -------------------------------------------------
// EXTRACT PAGE → ADD TO MESSAGE BOX
// -------------------------------------------------
document.getElementById("extract-btn").onclick = () => {
const extracted = extractWebpageContent();
if (!extracted || extracted.length < 10) {
resultBox.innerText = "Could not extract useful content.";
return;
}
// Add extracted text to input box (not sent automatically)
inputBox.value += (inputBox.value ? "\
\
" : "") + extracted;
// Scroll text area
inputBox.scrollTop = inputBox.scrollHeight;
resultBox.innerText = "📄 Extracted content added to message box.";
};
}
styles.css
ثم نستخدم ملف styles.css لتنسيق الواجهة والتحكم في كيفية ظهور الإضافة على الصفحة.
#assistant-box {
position: fixed;
top: 10%;
right: 10%;
width: 350px;
background: #000;
border: 1px solid #00ff7f;
border-radius: 8px;
box-shadow: 0 0 10px rgba(0,255,127,0.5);
z-index: 999999;
font-family: monospace;
color: #00ff7f;
}
#assistant-container {
padding: 12px;
}
#assistant-header {
display: flex;
justify-content: space-between;
align-items: center;
}
#assistant-close {
background: transparent;
border: none;
font-size: 20px;
cursor: pointer;
padding: 0 5px;
font-weight: bold;
color: #00ff7f;
}
#assistant-container textarea {
width: 100%;
height: 60px;
margin-top: 8px;
background: #0d0d0d;
color: #00ff7f;
border: 1px solid #00ff7f;
border-radius: 4px;
padding: 6px;
resize: vertical;
outline: none;
}
#send-btn {
width: 100%;
margin-top: 10px;
background: #00ff7f;
border: none;
padding: 10px;
color: #000;
font-weight: bold;
cursor: pointer;
border-radius: 4px;
transition: 0.2s;
}
#send-btn:hover {
background: #00e66a;
}
#assistant-box pre {
background: #0a0a0a;
padding: 8px;
border-radius: 4px;
max-height: 120px;
overflow: auto;
white-space: pre-wrap;
word-break: break-word;
margin-top: 5px;
}
#assistant-result {
margin-top: 12px;
background: #0d0d0d;
padding: 8px;
border-radius: 5px;
white-space: pre-wrap;
max-height: 150px;
overflow: auto;
}
كيفية إعداد إضافة كروم
الآن بعد توفر جميع الملفات المطلوبة، الخطوة التالية هي تحميل الإضافة إلى كروم. اتبع هذه الخطوات:
- احفظ كود خادم الإضافة في ملف بايثون.
- قم بتثبيت كل الاعتماديات وإعداد متغيرات البيئة الضرورية للخادم.
- أنشئ مجلدًا جديدًا على جهازك باسم code-assistant-extension لعميل الإضافة.
- أضف الملفات التالية إلى المجلد:
- manifest.json
- background.js
- content.js
- styles.css
- افتح كروم واذهب إلى: chrome://extensions/
- فعّل Developer mode في الزاوية العلوية اليمنى.
- انقر على Load unpacked.
- اختر المجلد الذي يحتوي على ملفات الإضافة الخاصة بك.
- سيظهر الامتداد في شريط الأدوات.
- انقر بزر الماوس الأيمن على أي تحديد نص في صفحة ويب لرؤية Agent Sandbox في قائمة السياق.
امتدادك الآن جاهز للاستخدام.
المستودع - امتداد متصفح مساعد الكود بالذكاء الاصطناعي
الخاتمة
في هذا المقال، قمنا ببناء إضافة كروم تتصل بخلفية مدعومة من Novita Sandbox، مما يسمح لوكيل ذكاء اصطناعي بتنفيذ الكود بأمان ومساعدة المستخدمين أثناء تصفحهم. هذا النمط يتجاوز مساعدة البرمجة؛ يمكنه تشغيل أدوات التعلم التفاعلي، ومساعدات تصحيح الأخطاء، وأدوات تعزيز التوثيق، والمزيد.
البنية مستقلة عن المتصفح، مما يعني أنه يمكن تكييف نفس النهج مع أي متصفح حديث مع تغييرات طفيفة. من هنا يمكنك توسيع قدرات المساعد، أو تحسين واجهة المستخدم، أو إضافة أدوات حاوية رملية جديدة. تفتح هذه الأساس الباب لإنشاء مرافق متصفح ذكية وقوية.
Novita AI هي منصة سحابة ذكاء اصطناعي رائدة توفر للمطورين واجهات برمجة تطبيقات سهلة الاستخدام وبنية تحتية لـ GPU موثوقة وبأسعار معقولة لبناء وتوسيع نطاق تطبيقات الذكاء الاصطناعي
