ساخت دستیار GraphQL با هوش مصنوعی؛ تبدیل متن فارسی به Query و Mutation معتبر
در این آموزش یک دستیار واقعی میسازیم که درخواست فارسی را به GraphQL Query و Variables تبدیل میکند، خروجی را با Schema اعتبارسنجی و سپس با کنترل کامل اجرا میکند.
GraphQL به توسعهدهنده اجازه میدهد دقیقاً فیلدهایی را درخواست کند که برنامه به آنها نیاز دارد. این انعطافپذیری یکی از مهمترین مزایای GraphQL است؛ اما نوشتن Queryهای پیچیده، تعریف Variableها، انتخاب فیلدهای درست، استفاده از Fragmentها و هماهنگماندن با Schema همیشه ساده نیست.
فرض کنید کاربر یا یکی از اعضای تیم میخواهد چنین درخواستی را اجرا کند:
پنج محصول موجود در دسته لپتاپ را با نام، قیمت و نام برند نمایش بده.
یک دستیار GraphQL مبتنی بر هوش مصنوعی میتواند آن را به چنین خروجی تبدیل کند:
query SearchAvailableLaptops(
$category: String!
$limit: Int!
$available: Boolean!
) {
products(
category: $category
limit: $limit
available: $available
) {
id
name
price
brand {
name
}
}
}
Variables:
{
"category": "laptop",
"limit": 5,
"available": true
}
اما تولید متن Query فقط بخش ساده مسئله است. یک سیستم واقعی باید بررسی کند:
- آیا فیلد
productsدر Schema وجود دارد؟ - آیا آرگومان
categoryمعتبر است؟ - آیا نوع
limitواقعاًIntاست؟ - آیا فیلد
brandدارای زیرمجموعهnameاست؟ - آیا Query از نظر Syntax معتبر است؟
- آیا Variableهای لازم تعریف شدهاند؟
- آیا مدل یک فیلد یا Mutation خیالی ساخته است؟
- آیا درخواست بیشازحد عمیق یا بزرگ نیست؟
- آیا کاربر اجازه اجرای Mutation را داده است؟
- آیا قبل از ارسال به API، Query بهصورت قطعی اعتبارسنجی شده است؟
در این مقاله، یک دستیار کامل Text-to-GraphQL میسازیم که درخواست فارسی را دریافت میکند، از طریق API هوش مصنوعی درواره Query تولید میکند، آن را با Schema واقعی اعتبارسنجی میکند و فقط پس از عبور از کنترلهای برنامه اجرا میکند.
GraphQL چیست؟
GraphQL یک زبان Query و مدل اجرایی برای API است. در GraphQL، API بهجای مجموعهای از Endpointهای ثابت، یک Schema شامل Typeها، Fieldها، Queryها و Mutationها ارائه میکند.
طبق وبسایت رسمی GraphQL، GraphQL دارای سیستم نوع قوی است و ابزارها میتوانند Query را پیش از اجرا با Schema اعتبارسنجی کنند.
نمونه Schema:
type Product {
id: ID!
name: String!
price: Float!
available: Boolean!
brand: Brand!
}
type Brand {
id: ID!
name: String!
}
type Query {
product(id: ID!): Product
products(
category: String
available: Boolean
limit: Int = 20
): [Product!]!
}
Query:
query GetProduct($productId: ID!) {
product(id: $productId) {
id
name
price
brand {
name
}
}
}
Variables:
{
"productId": "prd_123"
}
پاسخ API نیز معمولاً ساختاری مشابه شکل Query دارد:
{
"data": {
"product": {
"id": "prd_123",
"name": "Developer Laptop",
"price": 72000000,
"brand": {
"name": "Example"
}
}
}
}
تفاوت Text-to-GraphQL با Text-to-SQL
در Text-to-SQL مدل زبان طبیعی را به Query پایگاه داده تبدیل میکند. در Text-to-GraphQL، مدل براساس Schema عمومی API یک عملیات GraphQL میسازد.
| ویژگی | Text-to-SQL | Text-to-GraphQL |
|---|---|---|
| منبع ساختار | Database Schema | GraphQL Schema |
| خروجی | SQL Query | Query یا Mutation |
| محل اجرا | پایگاه داده | GraphQL API |
| مدل نوع | نوعهای دیتابیس | GraphQL Type System |
| پارامترها | Placeholder یا Bind Parameter | GraphQL Variables |
| کنترل اعتبار | SQL Parser و Database | GraphQL Parser و Schema Validation |
| انتخاب خروجی | ستونها | Selection Set |
در هر دو روش، خروجی مدل نباید بدون Parse و Validation اجرا شود.
کاربردهای دستیار GraphQL
یک GraphQL Assistant میتواند در بخشهای مختلف محصول استفاده شود:
- تولید Query از توضیح فارسی
- ساخت Query برای تیم Frontend
- تولید نمونه مستندات API
- کمک به تیم پشتیبانی فنی
- ساخت ابزار داخلی گزارشگیری
- تولید Query برای پنل مدیریت
- تبدیل درخواست محصول به Prototype
- توضیح Queryهای پیچیده
- اصلاح Query نامعتبر
- تولید Variableهای نمونه
- شناسایی Fieldهای Deprecated
- ساخت تست برای عملیات GraphQL
دستیار میتواند داخل ابزارهای زیر قرار بگیرد:
- پنل داخلی شرکت
- افزونه ویرایشگر کد
- داشبورد توسعهدهندگان
- ابزار مستندسازی API
- ربات تیم فنی
- CI/CD
- محیط تست GraphQL
- چتبات متصل به دادههای سازمانی
چرا Prompt ساده کافی نیست؟
یک Prompt ساده ممکن است چنین باشد:
برای دریافت محصولات، GraphQL Query بنویس.
مدل اطلاعات کافی ندارد و ممکن است براساس الگوهای رایج حدس بزند:
query {
getProducts {
title
cost
}
}
درحالیکه Schema واقعی شاید از فیلدهای products، name و price استفاده کند.
مدل زمانی میتواند Query دقیقتری بسازد که حداقل این اطلاعات را داشته باشد:
- Schema مرتبط
- نوع عملیات مجاز
- فیلدهای موردنیاز
- محدودیت عمق
- نامگذاری عملیات
- قواعد Variables
- فهرست Root Fieldهای مجاز
- شکل دقیق خروجی مورد انتظار
بااینحال، حتی با Prompt دقیق نیز Validation قطعی ضروری است.
معماری مناسب Text-to-GraphQL
معماری پیشنهادی:
درخواست فارسی کاربر
↓
تشخیص نوع عملیات
↓
انتخاب بخش مرتبط Schema
↓
ساخت Prompt محدود
↓
تولید Query و Variables
↓
Parse خروجی JSON
↓
Parse سند GraphQL
↓
Schema Validation
↓
کنترل نوع عملیات
↓
کنترل Root Fieldها
↓
کنترل عمق و اندازه Query
↓
نمایش پیشنمایش
↓
اجرای GraphQL API
↓
نمایش Data و Error
هوش مصنوعی فقط مسئول پیشنهاد عملیات است. اجازه نهایی اجرا را کد برنامه صادر میکند.
پروژهای که میسازیم
ساختار پروژه:
ai-graphql-assistant/
├── schema.graphql
├── requirements.txt
├── .env
├── graphql_assistant/
│ ├── __init__.py
│ ├── models.py
│ ├── generator.py
│ ├── validator.py
│ ├── executor.py
│ └── main.py
└── tests/
├── test_validator.py
└── test_examples.py
پوشهها را بسازید:
mkdir ai-graphql-assistant
cd ai-graphql-assistant
mkdir graphql_assistant
mkdir tests
محیط مجازی:
python -m venv .venv
فعالسازی در Linux و macOS:
source .venv/bin/activate
فعالسازی در Windows PowerShell:
.venv\Scripts\Activate.ps1
نصب Dependencyها
فایل requirements.txt:
openai
python-dotenv
pydantic
graphql-core
httpx
pytest
نصب:
pip install -r requirements.txt
ساخت GraphQL Schema نمونه
فایل schema.graphql:
enum ProductSort {
PRICE_ASC
PRICE_DESC
NAME_ASC
NAME_DESC
}
enum OrderStatus {
PENDING
CONFIRMED
PROCESSING
COMPLETED
CANCELLED
}
type Brand {
id: ID!
name: String!
}
type Product {
id: ID!
name: String!
description: String
price: Float!
available: Boolean!
category: String!
brand: Brand!
}
type ProductConnection {
items: [Product!]!
totalCount: Int!
nextCursor: String
}
type OrderItem {
product: Product!
quantity: Int!
unitPrice: Float!
}
type Order {
id: ID!
status: OrderStatus!
items: [OrderItem!]!
totalAmount: Float!
createdAt: String!
}
input CreateOrderItemInput {
productId: ID!
quantity: Int!
}
input CreateOrderInput {
customerId: ID!
items: [CreateOrderItemInput!]!
}
type Query {
product(id: ID!): Product
products(
search: String
category: String
available: Boolean
sort: ProductSort = NAME_ASC
limit: Int = 20
cursor: String
): ProductConnection!
order(id: ID!): Order
orders(
customerId: ID
status: OrderStatus
limit: Int = 20
): [Order!]!
}
type Mutation {
createOrder(input: CreateOrderInput!): Order!
cancelOrder(orderId: ID!): Order!
}
این Schema منبع حقیقت سیستم است. مدل فقط میتواند از فیلدها و نوعهایی استفاده کند که در این فایل وجود دارند.
تعریف مدل خروجی هوش مصنوعی
مدل نباید فقط یک رشته GraphQL برگرداند. خروجی ساختاریافته مدیریت و اعتبارسنجی را سادهتر میکند.
فایل graphql_assistant/models.py:
from __future__ import annotations
from typing import Any, Literal
from pydantic import BaseModel, Field
class GeneratedGraphQLOperation(BaseModel):
operation_name: str
operation_type: Literal["query", "mutation"]
query: str
variables: dict[str, Any] = Field(
default_factory=dict
)
explanation: str
assumptions: list[str] = Field(
default_factory=list
)
needs_human_review: bool = False
class ValidationResult(BaseModel):
valid: bool
operation_name: str | None = None
operation_type: str | None = None
root_fields: list[str] = Field(
default_factory=list
)
depth: int = 0
field_count: int = 0
errors: list[str] = Field(
default_factory=list
)
دریافت کلید API درواره
برای فراخوانی مدل:
- در درواره ثبتنام کنید.
- کلید API بگیرید.
- مدل مناسب تولید کد و خروجی ساختاریافته را انتخاب کنید.
- اطلاعات را در
.envقرار دهید.
فایل .env:
DARVAREH_API_KEY=YOUR_DARVAREH_API_KEY
DARVAREH_MODEL=MODEL_ID_DARVAREH
GRAPHQL_ENDPOINT=https://api.example.com/graphql
GRAPHQL_ACCESS_TOKEN=YOUR_GRAPHQL_ACCESS_TOKEN
فهرست مدلها، Model ID و قیمت بهروز را در صفحه مدلهای درواره مشاهده کنید.
آدرس پایه API درواره:
https://api.darvareh.ir/v1
ساخت مولد GraphQL Query
فایل graphql_assistant/generator.py:
from __future__ import annotations
import json
import os
from pathlib import Path
from dotenv import load_dotenv
from openai import OpenAI
from graphql_assistant.models import (
GeneratedGraphQLOperation,
)
load_dotenv()
def load_schema() -> str:
return Path("schema.graphql").read_text(
encoding="utf-8"
)
def strip_code_fence(value: str) -> str:
text = value.strip()
if not text.startswith("```"):
return text
lines = text.splitlines()
if len(lines) < 3:
return text
content = "\n".join(lines[1:-1]).strip()
if content.startswith("json"):
content = content[4:].lstrip()
return content
class GraphQLGenerator:
def __init__(self) -> None:
self.model_id = os.environ.get(
"DARVAREH_MODEL",
"MODEL_ID_DARVAREH",
)
self.client = OpenAI(
api_key=os.environ[
"DARVAREH_API_KEY"
],
base_url="https://api.darvareh.ir/v1",
)
self.schema = load_schema()
def generate(
self,
user_request: str,
allow_mutation: bool = False,
) -> GeneratedGraphQLOperation:
allowed_operation_types = (
["query", "mutation"]
if allow_mutation
else ["query"]
)
request_payload = {
"task": (
"Convert the Persian user request into one "
"GraphQL operation."
),
"userRequest": user_request,
"allowedOperationTypes": (
allowed_operation_types
),
"rules": [
(
"Use only types, fields, arguments, "
"and enum values present in the schema."
),
(
"Create exactly one named GraphQL "
"operation."
),
(
"Put user-supplied values in GraphQL "
"variables instead of embedding them "
"inside the query."
),
(
"Do not use introspection fields such "
"as __schema or __type."
),
(
"Do not invent fields when the request "
"cannot be represented."
),
(
"Set needs_human_review to true when "
"the request is ambiguous."
),
(
"Return valid JSON only and do not use "
"Markdown code fences."
),
],
"requiredOutput": {
"operation_name": "string",
"operation_type": "query | mutation",
"query": "string",
"variables": "object",
"explanation": "string",
"assumptions": ["string"],
"needs_human_review": "boolean",
},
"schema": self.schema,
}
response = self.client.chat.completions.create(
model=self.model_id,
temperature=0.0,
messages=[
{
"role": "system",
"content": (
"You are a GraphQL operation "
"generator. The supplied schema is "
"the only source of truth. Never "
"invent schema fields, arguments, "
"types, enum values, or operations."
),
},
{
"role": "user",
"content": json.dumps(
request_payload,
ensure_ascii=False,
),
},
],
)
content = response.choices[0].message.content
if not content:
raise RuntimeError(
"Model returned an empty response"
)
parsed = json.loads(
strip_code_fence(content)
)
generated = (
GeneratedGraphQLOperation.model_validate(
parsed
)
)
if (
generated.operation_type == "mutation"
and not allow_mutation
):
raise ValueError(
"The model returned a mutation while "
"mutations are disabled"
)
return generated
در این مرحله فقط ساختار JSON بررسی شده است. هنوز مشخص نیست که Query با Schema سازگار است یا خیر.
اعتبارسنجی GraphQL با Schema
یکی از مزایای GraphQL، امکان Validation عملیات براساس Type System است. یعنی میتوان پیش از ارسال Query به سرور بررسی کرد که فیلد، آرگومان، نوع Variable و Selection Set معتبر هستند.
فایل graphql_assistant/validator.py:
from __future__ import annotations
from pathlib import Path
from typing import Any
from graphql import (
GraphQLSchema,
build_schema,
get_operation_ast,
parse,
validate,
)
from graphql.language.ast import (
DocumentNode,
FieldNode,
FragmentDefinitionNode,
FragmentSpreadNode,
InlineFragmentNode,
OperationDefinitionNode,
SelectionSetNode,
)
from graphql_assistant.models import (
GeneratedGraphQLOperation,
ValidationResult,
)
ALLOWED_QUERY_FIELDS = {
"product",
"products",
"order",
"orders",
}
ALLOWED_MUTATION_FIELDS = {
"createOrder",
"cancelOrder",
}
MAX_QUERY_DEPTH = 6
MAX_FIELD_COUNT = 40
def load_graphql_schema() -> GraphQLSchema:
schema_text = Path("schema.graphql").read_text(
encoding="utf-8"
)
return build_schema(schema_text)
def get_fragments(
document: DocumentNode,
) -> dict[str, FragmentDefinitionNode]:
fragments: dict[
str,
FragmentDefinitionNode,
] = {}
for definition in document.definitions:
if isinstance(
definition,
FragmentDefinitionNode,
):
fragments[
definition.name.value
] = definition
return fragments
def inspect_selection_set(
selection_set: SelectionSetNode,
fragments: dict[str, FragmentDefinitionNode],
current_depth: int = 1,
visited_fragments: set[str] | None = None,
) -> tuple[int, int]:
if visited_fragments is None:
visited_fragments = set()
max_depth = current_depth
field_count = 0
for selection in selection_set.selections:
if isinstance(selection, FieldNode):
field_count += 1
if selection.selection_set:
child_depth, child_count = (
inspect_selection_set(
selection.selection_set,
fragments,
current_depth + 1,
visited_fragments.copy(),
)
)
max_depth = max(
max_depth,
child_depth,
)
field_count += child_count
elif isinstance(
selection,
InlineFragmentNode,
):
child_depth, child_count = (
inspect_selection_set(
selection.selection_set,
fragments,
current_depth,
visited_fragments.copy(),
)
)
max_depth = max(
max_depth,
child_depth,
)
field_count += child_count
elif isinstance(
selection,
FragmentSpreadNode,
):
fragment_name = selection.name.value
if fragment_name in visited_fragments:
continue
fragment = fragments.get(
fragment_name
)
if fragment:
next_visited = (
visited_fragments.copy()
)
next_visited.add(fragment_name)
child_depth, child_count = (
inspect_selection_set(
fragment.selection_set,
fragments,
current_depth,
next_visited,
)
)
max_depth = max(
max_depth,
child_depth,
)
field_count += child_count
return max_depth, field_count
def get_root_fields(
operation: OperationDefinitionNode,
) -> list[str]:
fields: list[str] = []
for selection in (
operation.selection_set.selections
):
if isinstance(selection, FieldNode):
fields.append(selection.name.value)
return fields
def contains_introspection_field(
document: DocumentNode,
) -> bool:
def check(
selection_set: SelectionSetNode,
) -> bool:
for selection in (
selection_set.selections
):
if isinstance(selection, FieldNode):
if selection.name.value.startswith(
"__"
):
return True
if (
selection.selection_set
and check(
selection.selection_set
)
):
return True
elif isinstance(
selection,
InlineFragmentNode,
):
if check(selection.selection_set):
return True
return False
for definition in document.definitions:
if isinstance(
definition,
(
OperationDefinitionNode,
FragmentDefinitionNode,
),
):
if check(definition.selection_set):
return True
return False
class GraphQLValidator:
def __init__(self) -> None:
self.schema = load_graphql_schema()
def validate_generated_operation(
self,
generated: GeneratedGraphQLOperation,
allow_mutation: bool = False,
) -> ValidationResult:
errors: list[str] = []
try:
document = parse(generated.query)
except Exception as error:
return ValidationResult(
valid=False,
errors=[
f"GraphQL syntax error: {error}"
],
)
schema_errors = validate(
self.schema,
document,
)
errors.extend(
str(error)
for error in schema_errors
)
operation = get_operation_ast(
document,
generated.operation_name,
)
if operation is None:
errors.append(
"Named operation was not found"
)
return ValidationResult(
valid=False,
errors=errors,
)
operation_type = (
operation.operation.value
)
if (
operation_type
!= generated.operation_type
):
errors.append(
"Declared operation_type does not "
"match the GraphQL document"
)
if (
operation.name is None
or operation.name.value
!= generated.operation_name
):
errors.append(
"Operation name does not match "
"operation_name"
)
if (
operation_type == "mutation"
and not allow_mutation
):
errors.append(
"Mutations are not enabled"
)
root_fields = get_root_fields(
operation
)
allowed_root_fields = (
ALLOWED_MUTATION_FIELDS
if operation_type == "mutation"
else ALLOWED_QUERY_FIELDS
)
unknown_root_fields = sorted(
set(root_fields)
- allowed_root_fields
)
if unknown_root_fields:
errors.append(
"Root fields are not allowed: "
+ ", ".join(
unknown_root_fields
)
)
if contains_introspection_field(
document
):
errors.append(
"Introspection fields are not allowed "
"in generated operations"
)
fragments = get_fragments(document)
depth, field_count = inspect_selection_set(
operation.selection_set,
fragments,
)
if depth > MAX_QUERY_DEPTH:
errors.append(
f"Query depth {depth} exceeds "
f"limit {MAX_QUERY_DEPTH}"
)
if field_count > MAX_FIELD_COUNT:
errors.append(
f"Field count {field_count} exceeds "
f"limit {MAX_FIELD_COUNT}"
)
return ValidationResult(
valid=not errors,
operation_name=(
operation.name.value
if operation.name
else None
),
operation_type=operation_type,
root_fields=root_fields,
depth=depth,
field_count=field_count,
errors=errors,
)
Validation چه خطاهایی را تشخیص میدهد؟
فرض کنید مدل این Query را بسازد:
query FindProducts {
products {
items {
id
title
discountPrice
}
}
}
اما Product در Schema فقط فیلدهای name و price دارد. Validation خطایی مشابه موارد زیر میدهد:
Cannot query field 'title' on type 'Product'.
Cannot query field 'discountPrice' on type 'Product'.
این همان کنترل قطعیای است که نباید به مدل زبانی سپرده شود.
چرا Root Field Allowlist لازم است؟
ممکن است یک Schema بزرگ شامل Queryهای زیادی باشد؛ اما دستیار شما فقط باید برای گزارش محصولات استفاده شود.
در این حالت حتی اگر Query از نظر Schema معتبر باشد، نباید هر Root Field قابل اجرا باشد.
مثلاً:
ALLOWED_QUERY_FIELDS = {
"product",
"products",
}
این Allowlist دامنه عملکرد دستیار را براساس کاربرد محصول محدود میکند.
ساخت اجراکننده GraphQL
فایل graphql_assistant/executor.py:
from __future__ import annotations
import os
from typing import Any
import httpx
from dotenv import load_dotenv
from graphql_assistant.models import (
GeneratedGraphQLOperation,
)
load_dotenv()
class GraphQLExecutionError(Exception):
def __init__(
self,
message: str,
errors: list[dict[str, Any]] | None = None,
) -> None:
super().__init__(message)
self.errors = errors or []
class GraphQLExecutor:
def __init__(self) -> None:
self.endpoint = os.environ[
"GRAPHQL_ENDPOINT"
]
self.access_token = os.environ.get(
"GRAPHQL_ACCESS_TOKEN"
)
def execute(
self,
generated: GeneratedGraphQLOperation,
) -> dict[str, Any]:
headers = {
"Accept": "application/json",
"Content-Type": "application/json",
}
if self.access_token:
headers["Authorization"] = (
f"Bearer {self.access_token}"
)
payload = {
"query": generated.query,
"variables": generated.variables,
"operationName": (
generated.operation_name
),
}
with httpx.Client(
timeout=30,
) as client:
response = client.post(
self.endpoint,
headers=headers,
json=payload,
)
response.raise_for_status()
body = response.json()
if not isinstance(body, dict):
raise GraphQLExecutionError(
"GraphQL response must be an object"
)
graphql_errors = body.get("errors")
if graphql_errors:
raise GraphQLExecutionError(
"GraphQL API returned errors",
errors=graphql_errors,
)
return body
نکته مهم این است که پاسخ HTTP موفق لزوماً به معنی اجرای موفق GraphQL نیست. بعضی پاسخها Status Code موفق دارند اما دارای کلید errors هستند.
بنابراین باید هر دو سطح بررسی شوند:
- وضعیت HTTP
- فیلد
errorsدر پاسخ GraphQL
ساخت برنامه اصلی
فایل graphql_assistant/main.py:
from __future__ import annotations
import argparse
import json
from graphql_assistant.executor import (
GraphQLExecutionError,
GraphQLExecutor,
)
from graphql_assistant.generator import (
GraphQLGenerator,
)
from graphql_assistant.validator import (
GraphQLValidator,
)
def main() -> None:
parser = argparse.ArgumentParser(
description=(
"Convert Persian requests to validated "
"GraphQL operations"
)
)
parser.add_argument(
"request",
help="Persian natural-language request",
)
parser.add_argument(
"--allow-mutation",
action="store_true",
help="Allow mutation generation",
)
parser.add_argument(
"--execute",
action="store_true",
help="Execute after validation",
)
args = parser.parse_args()
generator = GraphQLGenerator()
validator = GraphQLValidator()
generated = generator.generate(
user_request=args.request,
allow_mutation=args.allow_mutation,
)
validation = (
validator.validate_generated_operation(
generated,
allow_mutation=args.allow_mutation,
)
)
output = {
"generated": generated.model_dump(),
"validation": validation.model_dump(),
}
print(
json.dumps(
output,
ensure_ascii=False,
indent=2,
)
)
if not validation.valid:
raise SystemExit(1)
if generated.needs_human_review:
print(
"Operation requires human review "
"and will not be executed."
)
raise SystemExit(2)
if not args.execute:
return
if generated.operation_type == "mutation":
confirmation = input(
"This operation changes data. "
"Type EXECUTE to continue: "
)
if confirmation != "EXECUTE":
print("Execution cancelled.")
return
executor = GraphQLExecutor()
try:
result = executor.execute(generated)
except GraphQLExecutionError as error:
print(
json.dumps(
{
"success": False,
"message": str(error),
"errors": error.errors,
},
ensure_ascii=False,
indent=2,
)
)
raise SystemExit(3) from error
print(
json.dumps(
{
"success": True,
"response": result,
},
ensure_ascii=False,
indent=2,
)
)
if __name__ == "__main__":
main()
فایل graphql_assistant/__init__.py را خالی بگذارید.
اجرای دستیار
تولید Query بدون اجرا:
python -m graphql_assistant.main \
"پنج محصول موجود در دسته لپتاپ را بر اساس کمترین قیمت با نام، قیمت و برند نمایش بده"
خروجی احتمالی:
{
"generated": {
"operation_name": "FindAvailableLaptops",
"operation_type": "query",
"query": "query FindAvailableLaptops($category: String!, $available: Boolean!, $sort: ProductSort!, $limit: Int!) { products(category: $category, available: $available, sort: $sort, limit: $limit) { items { id name price brand { name } } totalCount nextCursor } }",
"variables": {
"category": "laptop",
"available": true,
"sort": "PRICE_ASC",
"limit": 5
},
"explanation": "Searches available laptop products ordered by ascending price.",
"assumptions": [],
"needs_human_review": false
},
"validation": {
"valid": true,
"operation_name": "FindAvailableLaptops",
"operation_type": "query",
"root_fields": [
"products"
],
"depth": 4,
"field_count": 8,
"errors": []
}
}
برای اجرای Query:
python -m graphql_assistant.main \
"پنج محصول موجود در دسته لپتاپ را نمایش بده" \
--execute
اجرای Mutation
Mutationها داده را تغییر میدهند؛ بنابراین در نسخه اولیه بهتر است بهصورت پیشفرض غیرفعال باشند.
درخواست:
python -m graphql_assistant.main \
"برای مشتری cus_123 یک سفارش شامل دو عدد از محصول prd_456 بساز" \
--allow-mutation
خروجی پیشنهادی:
mutation CreateCustomerOrder(
$input: CreateOrderInput!
) {
createOrder(input: $input) {
id
status
totalAmount
createdAt
items {
quantity
unitPrice
product {
id
name
}
}
}
}
Variables:
{
"input": {
"customerId": "cus_123",
"items": [
{
"productId": "prd_456",
"quantity": 2
}
]
}
}
برای اجرای واقعی:
python -m graphql_assistant.main \
"برای مشتری cus_123 یک سفارش شامل دو عدد از محصول prd_456 بساز" \
--allow-mutation \
--execute
برنامه پیش از اجرا تأیید صریح میخواهد:
This operation changes data. Type EXECUTE to continue:
چرا مقادیر باید در Variables قرار بگیرند؟
این Query از مقدار ثابت استفاده میکند:
query {
product(id: "prd_123") {
id
name
}
}
نسخه بهتر:
query GetProduct($productId: ID!) {
product(id: $productId) {
id
name
}
}
Variables:
{
"productId": "prd_123"
}
مزایای استفاده از Variables:
- Query قابلاستفاده مجدد میشود.
- کد خواناتر است.
- مدیریت نوع داده بهتر میشود.
- Log و Cache عملیات قابلکنترلتر میشود.
- داده کاربر از متن عملیات جدا میشود.
- تست Query آسانتر میشود.
تست Validator
فایل tests/test_validator.py:
from graphql_assistant.models import (
GeneratedGraphQLOperation,
)
from graphql_assistant.validator import (
GraphQLValidator,
)
def test_valid_product_query() -> None:
generated = GeneratedGraphQLOperation(
operation_name="GetProduct",
operation_type="query",
query="""
query GetProduct($productId: ID!) {
product(id: $productId) {
id
name
price
brand {
name
}
}
}
""",
variables={
"productId": "prd_123"
},
explanation="Gets one product",
)
result = (
GraphQLValidator()
.validate_generated_operation(
generated
)
)
assert result.valid is True
assert result.operation_type == "query"
assert result.root_fields == ["product"]
def test_unknown_field_is_rejected() -> None:
generated = GeneratedGraphQLOperation(
operation_name="GetProduct",
operation_type="query",
query="""
query GetProduct($productId: ID!) {
product(id: $productId) {
id
imaginaryField
}
}
""",
variables={
"productId": "prd_123"
},
explanation="Invalid query",
)
result = (
GraphQLValidator()
.validate_generated_operation(
generated
)
)
assert result.valid is False
assert any(
"imaginaryField" in error
for error in result.errors
)
def test_mutation_disabled_by_default() -> None:
generated = GeneratedGraphQLOperation(
operation_name="CancelOrder",
operation_type="mutation",
query="""
mutation CancelOrder($orderId: ID!) {
cancelOrder(orderId: $orderId) {
id
status
}
}
""",
variables={
"orderId": "ord_123"
},
explanation="Cancels an order",
)
result = (
GraphQLValidator()
.validate_generated_operation(
generated,
allow_mutation=False,
)
)
assert result.valid is False
assert "Mutations are not enabled" in (
result.errors
)
def test_introspection_is_rejected() -> None:
generated = GeneratedGraphQLOperation(
operation_name="InspectSchema",
operation_type="query",
query="""
query InspectSchema {
__schema {
types {
name
}
}
}
""",
variables={},
explanation="Introspection",
)
result = (
GraphQLValidator()
.validate_generated_operation(
generated
)
)
assert result.valid is False
اجرای تستها:
pytest -q
بررسی هماهنگی Variables با تعریف Query
Validation استاندارد GraphQL نوع استفاده از Variableها را بررسی میکند؛ اما مقادیر JSON نیز باید پیش از اجرا کنترل شوند.
برای نمونه، اگر Query این Variableها را تعریف کند:
query FindProducts(
$limit: Int!
$available: Boolean!
) {
products(
limit: $limit
available: $available
) {
totalCount
}
}
این Variables درست است:
{
"limit": 5,
"available": true
}
این مقدار اشتباه است:
{
"limit": "پنج",
"available": "بله"
}
سرور GraphQL معمولاً Coercion را بررسی میکند؛ اما برای تجربه بهتر میتوان نوع Variableها را از AST و Schema استخراج و پیش از ارسال اعتبارسنجی کرد.
در نسخه Production بهتر است موارد زیر بررسی شوند:
- وجود Variableهای Non-null
- نبود Variable ناشناخته
- نوع Scalar
- مقدار Enum
- ساختار Input Object
- نوع اعضای List
- محدودیتهای تجاری خارج از Schema
گرفتن Schema از GraphQL API
GraphQL از Introspection پشتیبانی میکند و ابزارها میتوانند اطلاعات Type System را از خود API دریافت کنند. مشخصات GraphQL نیز Introspection را بخشی از قابلیتهای GraphQL تعریف میکند.
بااینحال، در یک سیستم Production بهتر است Schema مورد استفاده دستیار از یک فرایند کنترلشده دریافت شود:
- Export در CI
- Schema Registry
- فایل نسخهبندیشده
- Artifact هر Release
- خروجی Build سرور GraphQL
مزیت فایل نسخهبندیشده این است که Query تولیدشده دقیقاً با نسخه مورد انتظار برنامه اعتبارسنجی میشود.
مدیریت Schemaهای بزرگ
ارسال یک Schema بسیار بزرگ در هر درخواست باعث افزایش مصرف توکن، هزینه و احتمال سردرگمی مدل میشود.
روش بهتر:
- Root Fieldهای مرتبط با درخواست را پیدا کنید.
- Typeهای وابسته را استخراج کنید.
- Input Typeها و Enumهای لازم را اضافه کنید.
- فقط زیرمجموعه مرتبط Schema را برای مدل ارسال کنید.
- Validation نهایی را همچنان با Schema کامل انجام دهید.
مثلاً برای درخواست محصولات فقط این بخشها لازماند:
Query.productsProductConnectionProductBrandProductSort
فیلدهای مربوط به سفارش ضرورتی ندارند.
انتخاب بخش مرتبط Schema با Retrieval
برای Schemaهای بسیار بزرگ میتوان هر بخش را به یک سند کوچک تبدیل کرد:
{
"kind": "root_field",
"operationType": "query",
"name": "products",
"signature": "products(search: String, category: String, available: Boolean, sort: ProductSort, limit: Int, cursor: String): ProductConnection!",
"relatedTypes": [
"ProductConnection",
"Product",
"Brand",
"ProductSort"
]
}
سپس درخواست کاربر Embedding میشود و نزدیکترین Root Fieldها بازیابی میشوند. بعد فقط بخش مرتبط Schema وارد Prompt میشود.
اما انتخاب Retrieval نباید اجازه اجرای خودکار ایجاد کند. عملیات نهایی همچنان باید با Schema کامل و Allowlist اعتبارسنجی شود.
استفاده از Fragment
مدل ممکن است برای Queryهای تکراری از Fragment استفاده کند:
fragment ProductSummary on Product {
id
name
price
available
brand {
name
}
}
query FindProducts(
$category: String!
$limit: Int!
) {
products(
category: $category
limit: $limit
) {
items {
...ProductSummary
}
totalCount
}
}
Validator نوشتهشده Fragmentها را هنگام محاسبه عمق و تعداد فیلدها دنبال میکند.
در سیستم واقعی باید مراقب Fragmentهای تودرتو و چرخهای نیز بود. Validation استاندارد GraphQL بسیاری از خطاهای Fragment را پیش از اجرا شناسایی میکند.
محدودکردن عمق Query
Query زیر از نظر Schema ممکن است معتبر باشد، اما بسیار عمیق شود:
query {
orders {
items {
product {
brand {
products {
items {
brand {
name
}
}
}
}
}
}
}
}
برای کنترل چنین عملیاتهایی، دو معیار ساده تعریف کردیم:
MAX_QUERY_DEPTH = 6
MAX_FIELD_COUNT = 40
این اعداد باید براساس Schema، Resolverها، حجم پاسخ و کاربرد محصول تنظیم شوند. عدد مناسب برای همه APIها یکسان نیست.
کنترل Complexity
تعداد فیلد و عمق همیشه هزینه واقعی Query را نشان نمیدهند.
ممکن است این Query عمق کمی داشته باشد اما داده زیادی دریافت کند:
query {
products(limit: 1000) {
items {
id
name
description
}
}
}
میتوان برای Fieldها وزن تعریف کرد:
هزینه فیلد ساده = 1
هزینه رابطه تکی = 2
هزینه فهرست = 10
هزینه فهرست × limit = هزینه نهایی تقریبی
فرمول ساده و سازگار با Ghost:
total_cost = sum(field_cost × expected_list_size)
برای مثال:
products = 10 × limit
items.id = 1 × limit
items.name = 1 × limit
اگر limit برابر 100 باشد:
total_cost = 1000 + 100 + 100 = 1200
در پروژه واقعی بهتر است Complexity براساس رفتار Resolverها و منابع داده اندازهگیری شود.
اصلاح خودکار Query نامعتبر
اگر Validation خطا برگرداند، میتوان یک بار Query را برای اصلاح به مدل بازگرداند.
ورودی مرحله Repair:
{
"originalRequest": "محصول را با عنوان و قیمت تخفیف نمایش بده",
"generatedQuery": "query { product(id: \"prd_1\") { title discountPrice } }",
"validationErrors": [
"Cannot query field 'title' on type 'Product'.",
"Cannot query field 'discountPrice' on type 'Product'."
],
"relevantSchema": "type Product { id: ID!, name: String!, price: Float! }"
}
قواعد مرحله اصلاح:
- فقط خطاهای Validation را اصلاح کن.
- هدف اصلی درخواست را تغییر نده.
- Field جدید نساز.
- حداکثر یک یا دو بار تلاش کن.
- خروجی اصلاحشده را دوباره از کل Pipeline عبور بده.
- اگر درخواست با Schema قابل انجام نیست، آن را صریح اعلام کن.
Loop نامحدود بین مدل و Validator ایجاد نکنید.
نگهداری حافظه مکالمه
اگر دستیار حالت گفتوگویی دارد، کاربر ممکن است بگوید:
فقط محصولات موجود را نشان بده.
سپس:
حالا بر اساس بیشترین قیمت مرتب کن.
درخواست دوم بدون Context ناقص است. حافظه موردنیاز بهتر است ساختاریافته باشد:
{
"currentIntent": "search_products",
"filters": {
"available": true
},
"sort": "PRICE_DESC",
"selectedFields": [
"id",
"name",
"price"
],
"limit": 20
}
در هر پیام، ابتدا State بهروزرسانی و سپس GraphQL تولید شود. ارسال کل تاریخچه گفتوگو در هر مرحله همیشه بهترین روش نیست.
تولید Query در برابر اجرای Query
این دو قابلیت را از هم جدا نگه دارید:
حالت Generate
- Query تولید میشود.
- Validation اجرا میشود.
- Preview نمایش داده میشود.
- هیچ درخواست GraphQL ارسال نمیشود.
حالت Execute
- Query قبلاً معتبر است.
- مجوز اجرای نوع عملیات بررسی میشود.
- Variables نهایی مشخصاند.
- Mutation تأیید میشود.
- سپس درخواست ارسال میشود.
این جداسازی برای IDE، مستندسازی و محیطهای توسعه بسیار مفید است.
کشکردن Queryهای معتبر
درخواستهای مشابه ممکن است بارها تکرار شوند:
محصول با شناسه مشخص را نشان بده
اطلاعات یک محصول را بگیر
جزئیات محصول را نمایش بده
میتوان Template معتبر Query را Cache کرد:
query GetProduct($productId: ID!) {
product(id: $productId) {
id
name
price
available
}
}
در درخواست بعدی فقط Variables تغییر میکنند. این روش:
- هزینه فراخوانی مدل را کاهش میدهد.
- Latency را کم میکند.
- رفتار سیستم را پایدارتر میکند.
- نرخ خطای تولید Query را کاهش میدهد.
Cache Key میتواند ترکیبی از موارد زیر باشد:
schema_version + normalized_intent + selected_fields
ثبت Log مناسب
برای هر عملیات اطلاعات زیر را ثبت کنید:
- شناسه درخواست
- نسخه Schema
- مدل استفادهشده
- Intent تشخیصدادهشده
- نوع عملیات
- نام Operation
- Root Fieldها
- نتیجه Validation
- عمق Query
- تعداد فیلدها
- زمان تولید مدل
- زمان اجرای API
- وجود GraphQL Error
- تعداد تلاشهای Repair
- تعداد توکن مصرفی در صورت دسترسی
Variables ممکن است شامل اطلاعات حساس یا شخصی باشند. بنابراین نباید بدون سیاست مشخص بهطور کامل در Log ذخیره شوند.
ارزیابی کیفیت سیستم
برای Evals یک Dataset بسازید:
[
{
"id": "products-001",
"request": "پنج محصول موجود را بر اساس کمترین قیمت نمایش بده",
"expectedOperationType": "query",
"expectedRootFields": [
"products"
],
"expectedVariables": {
"available": true,
"sort": "PRICE_ASC",
"limit": 5
},
"forbiddenFields": [
"orders",
"createOrder"
]
},
{
"id": "order-001",
"request": "سفارش ord_123 را با اقلام آن نمایش بده",
"expectedOperationType": "query",
"expectedRootFields": [
"order"
],
"forbiddenFields": [
"createOrder",
"cancelOrder"
]
}
]
معیارهای مناسب:
- درصد Queryهای دارای Syntax معتبر
- درصد Queryهای معتبر نسبت به Schema
- دقت انتخاب Root Field
- دقت Variableها
- نرخ تولید Field خیالی
- نرخ انتخاب اشتباه Mutation
- میانگین عمق Query
- نرخ نیاز به Repair
- نرخ موفقیت اجرای API
- هزینه متوسط هر درخواست
- Latency تولید
- رضایت کاربر
استفاده در Frontend
در Frontend بهتر است درخواست طبیعی به Backend شما ارسال شود:
const response = await fetch(
"/api/graphql-assistant/generate",
{
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
request: userInput
})
}
);
const result = await response.json();
Backend:
- API درواره را فراخوانی میکند.
- Query را اعتبارسنجی میکند.
- Preview را برمیگرداند.
- در صورت درخواست جداگانه کاربر، عملیات را اجرا میکند.
کلید API درواره و Token سرویس GraphQL نباید در کد عمومی مرورگر قرار بگیرند.
خطاهای رایج در ساخت AI GraphQL Assistant
اعتماد به JSON معتبر
JSON معتبر به معنی GraphQL معتبر نیست. هر دو سطح باید جداگانه بررسی شوند.
ارسال Schema بدون محدودیت
مدل ممکن است از هر Root Field موجود استفاده کند. علاوه بر Schema، Allowlist کاربردی تعریف کنید.
استفاده از Query بدون نام
این عملیات Anonymous است:
query {
products {
totalCount
}
}
نسخه بهتر:
query CountProducts {
products {
totalCount
}
}
Operation Name برای Log، Debug، Cache و Monitoring مفید است.
قراردادن مقادیر داخل Query
از Variables استفاده کنید و مقادیر کاربر را در رشته GraphQL قرار ندهید.
اجازه Mutation بهصورت پیشفرض
نسخه اولیه را Query-only بسازید. Mutation را فقط در کاربردهای مشخص و با تأیید فعال کنید.
اجرای Query قبل از Validation
ترتیب صحیح:
Generate → Parse → Validate → Policy Check → Execute
نادیدهگرفتن GraphQL Errors
فقط Status Code را بررسی نکنید. پاسخ GraphQL میتواند کلیدهای data و errors را همزمان داشته باشد.
ارسال Schema قدیمی
نسخه Schema را در هر گزارش و Cache Key ثبت کنید. Query معتبر برای نسخه قبلی ممکن است با نسخه فعلی ناسازگار باشد.
تلاش نامحدود برای Repair
تعداد تلاش اصلاح را محدود کنید. اگر Query پس از یک یا دو بار همچنان نامعتبر است، خطا را برای بررسی برگردانید.
چکلیست آمادهسازی برای Production
- Schema منبع حقیقت مشخصی دارد.
- نسخه Schema ثبت میشود.
- مدل فقط JSON ساختاریافته برمیگرداند.
- JSON با Pydantic یا JSON Schema بررسی میشود.
- Query با Parser واقعی Parse میشود.
- Query با Schema کامل Validation میشود.
- Operation Name اجباری است.
- فقط یک Operation پذیرفته میشود.
- Root Fieldها Allowlist دارند.
- نوع Operation بررسی میشود.
- Mutation بهصورت پیشفرض غیرفعال است.
- عمق Query محدود شده است.
- تعداد فیلدها محدود شده است.
- Complexity تخمین زده میشود.
- Variableها جدا از Query هستند.
- خروجی مدل مستقیماً اجرا نمیشود.
- خطاهای HTTP و GraphQL جدا بررسی میشوند.
- تعداد Repairها محدود است.
- Queryهای پرتکرار Cache میشوند.
- Dataset ارزیابی وجود دارد.
- کلیدها فقط در Backend نگهداری میشوند.
- Logها شامل Schema Version هستند.
انتخاب مدل مناسب
مدل مناسب Text-to-GraphQL باید تواناییهای زیر را داشته باشد:
- پیروی دقیق از Schema
- تولید JSON معتبر
- درک Type System
- تولید GraphQL Variables
- درک درخواست فارسی
- تشخیص Query و Mutation
- عملکرد مناسب در تولید کد
- رعایت محدودیتهای Prompt
- هزینه مناسب برای استفاده پرتکرار
برای Schema کوچک و Queryهای ساده، یک مدل سریع و اقتصادی کافی است. برای Schema بزرگ، درخواستهای چندمرحلهای یا تحلیل پیچیده، مدلی با Context Window و توان استدلال بیشتر مناسبتر خواهد بود.
مدلها، Model ID و قیمت بهروز را در صفحه مدلهای درواره ببینید.
چرا API درواره برای این پروژه مناسب است؟
Text-to-GraphQL یک قابلیت Backend است و باید از داخل برنامه، Agent، داشبورد یا Pipeline فراخوانی شود.
با API درواره میتوانید:
- درخواست فارسی را از Backend به مدل ارسال کنید.
- مدل مناسب کدنویسی را انتخاب کنید.
- خروجی JSON دریافت کنید.
- مدل را بدون تغییر معماری اصلی تعویض کنید.
- Query Generator را وارد محصول خود کنید.
- برای تولید، Repair و توضیح Query از مدلهای مختلف استفاده کنید.
- هزینه مدلها را پیش از انتخاب مقایسه کنید.
برای شروع، در درواره ثبتنام کنید، کلید API بگیرید و Base URL را روی مقدار زیر قرار دهید:
https://api.darvareh.ir/v1
پرسشهای متداول
آیا هوش مصنوعی میتواند GraphQL Query معتبر بسازد؟
بله، اما خروجی باید با Schema واقعی اعتبارسنجی شود. مدل ممکن است فیلد، آرگومان یا نوعی تولید کند که در Schema وجود ندارد.
آیا میتوان درخواست فارسی را به GraphQL تبدیل کرد؟
بله. مدل درخواست فارسی را تحلیل و به Query، Mutation و Variables تبدیل میکند. کیفیت خروجی به Schema، Prompt و Validation بستگی دارد.
آیا GraphQL Query قبل از اجرا قابل اعتبارسنجی است؟
بله. GraphQL دارای Type System است و Query را میتوان پیش از اجرا با Schema بررسی کرد.
آیا باید Schema کامل را به مدل بدهیم؟
برای Schema کوچک امکانپذیر است. در Schema بزرگ بهتر است ابتدا بخش مرتبط انتخاب شود؛ اما Validation نهایی باید با Schema کامل انجام شود.
آیا Mutation را هم میتوان خودکار تولید کرد؟
بله، ولی بهتر است Mutation پیشفرض غیرفعال باشد و پیش از اجرای واقعی Preview و تأیید دریافت شود.
تفاوت Query و Mutation چیست؟
Query معمولاً برای دریافت داده و Mutation برای ایجاد، ویرایش یا تغییر داده استفاده میشود.
آیا میتوان GraphQL Assistant را به React یا Next.js متصل کرد؟
بله. Frontend درخواست کاربر را به Backend میفرستد و Backend تولید، اعتبارسنجی و اجرای GraphQL را مدیریت میکند.
چگونه از ساختهشدن Field خیالی جلوگیری کنیم؟
Schema را به مدل بدهید و سپس Query خروجی را با GraphQL Validator واقعی بررسی کنید. Prompt بهتنهایی کافی نیست.
آیا درواره خودش GraphQL API میسازد؟
درواره زیرساخت دسترسی به مدلهای هوش مصنوعی را از طریق API فراهم میکند. Schema، Validator، GraphQL Endpoint و منطق اجرای دستیار را در برنامه خود پیادهسازی میکنید.
Model ID درواره را از کجا دریافت کنیم؟
Model ID و مشخصات مدلها را در صفحه مدلهای درواره ببینید و مقدار انتخابی را جایگزین MODEL_ID_DARVAREH کنید.
جمعبندی
ساخت GraphQL Query با هوش مصنوعی فقط به معنی تولید یک رشته متنی نیست. یک سیستم قابلاعتماد باید مدل را درون یک Pipeline کنترلشده قرار دهد.
در این معماری:
- Schema منبع حقیقت است.
- مدل Query و Variables را پیشنهاد میدهد.
- Pydantic ساختار JSON را بررسی میکند.
- GraphQL Parser صحت Syntax را کنترل میکند.
- Schema Validator فیلدها، نوعها و آرگومانها را بررسی میکند.
- Allowlist دامنه دسترسی دستیار را محدود میکند.
- عمق و اندازه Query کنترل میشود.
- Mutation تنها با مجوز و تأیید اجرا میشود.
- پاسخ HTTP و GraphQL جداگانه بررسی میشوند.
- کیفیت سیستم با Dataset و Evals اندازهگیری میشود.
نتیجه، دستیاری است که میتواند درخواست فارسی را به یک GraphQL Operation واقعی تبدیل کند؛ بدون اینکه تصمیم نهایی اجرا صرفاً به مدل زبانی سپرده شود.
برای ساخت این قابلیت، در درواره ثبتنام کنید، کلید API بگیرید و مدل مناسب پروژه را از صفحه مدلها انتخاب کنید.
مقالات مرتبط
- API هوش مصنوعی چیست؟
- آموزش اتصال API هوش مصنوعی به اپلیکیشن
- خروجی JSON ساختاریافته با Structured Outputs
- راهنمای Function Calling در هوش مصنوعی
- راهنمای Tool Calling در هوش مصنوعی
- ساخت API هوش مصنوعی آماده Production
- آموزش ارزیابی مدلهای هوش مصنوعی و Evals
- مهندسی Context برای ایجنتهای هوش مصنوعی
- آموزش تست API با Postman
- آموزش API درواره با cURL
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.