{"id":352,"date":"2026-08-23T16:46:09","date_gmt":"2026-08-23T20:46:09","guid":{"rendered":"https:\/\/juredev.com\/blog\/?p=352"},"modified":"2026-08-23T17:15:18","modified_gmt":"2026-08-23T21:15:18","slug":"symfony-ai-en-la-practica-migracion-a-v0-12","status":"publish","type":"post","link":"https:\/\/juredev.com\/blog\/2026\/08\/symfony-ai-en-la-practica-migracion-a-v0-12\/","title":{"rendered":"Symfony AI en la pr\u00e1ctica: Migraci\u00f3n a v0.12, resiliencia con excepciones granulares y selecci\u00f3n de modelos"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Cuando trabajamos con herramientas en fase experimental, como <strong>Symfony AI<\/strong>, la velocidad de evoluci\u00f3n es tanto una ventaja como un desaf\u00edo. Entre la versi\u00f3n <code>v0.9.0<\/code> y la <code>v0.12.0<\/code>, el componente no solo refin\u00f3 sus firmas de API y su arquitectura interna, sino que introdujo herramientas esenciales para construir agentes verdaderamente resilientes y preparados para los imprevistos del mundo real.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En los art\u00edculos anteriores construimos un <a href=\"https:\/\/juredev.com\/blog\/2026\/04\/symfony-ai-agentes-tools-php\/\" data-type=\"link\" data-id=\"https:\/\/juredev.com\/blog\/2026\/04\/symfony-ai-agentes-tools-php\/\">asistente<\/a> de tienda con <a href=\"https:\/\/juredev.com\/blog\/2026\/04\/symfony-ai-practica-chat-con-historial-tools-y-vanilla-js\/\">Vanilla JS<\/a> y <a href=\"https:\/\/juredev.com\/blog\/2026\/04\/symfony-ai-practica-iii-streaming-con-sse-gestion-de-tools-anidadas\/\">streaming SSE con Tools anidadas<\/a>. Sin embargo, cuando un proveedor externo sufre una sobrecarga, un modelo es retirado de un cat\u00e1logo o la conversaci\u00f3n excede la ventana de contexto, un bloque <code>catch (\\Throwable $e)<\/code> gen\u00e9rico no es suficiente para ofrecer una buena experiencia de usuario.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En este art\u00edculo, antes de continuar con la Serie II, abordamos:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>La auditor\u00eda de cambios y actualizaci\u00f3n segura a Symfony AI <code>v0.12.0<\/code><\/strong>.<\/li>\n\n\n\n<li><strong>Protecci\u00f3n contra mutaciones in-place<\/strong> en la gesti\u00f3n del <code>MessageBag<\/code>.<\/li>\n\n\n\n<li><strong>Manejo granular de excepciones<\/strong> (<code>ExceedContextSizeException<\/code>, <code>ModelNotFoundException<\/code>, <code>ServerException<\/code>) para recuperaci\u00f3n autom\u00e1tica.<\/li>\n\n\n\n<li><strong>La importancia del modelo en la obediencia de reglas y paginaci\u00f3n<\/strong>, y c\u00f3mo evitar inflar el <em>system prompt<\/em>.<\/li>\n<\/ol>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p class=\"wp-block-paragraph\"><strong>Recordatorio:<\/strong> Symfony AI se encuentra en constante evoluci\u00f3n (actualmente en <code>v0.12.0<\/code>). Aunque los contratos principales ya est\u00e1n maduros, actualizar peri\u00f3dicamente con un enfoque por fases permite incorporar mejoras de robustez sin comprometer el c\u00f3digo existente.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\">Arquitectura resultante<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Antes de entrar en detalle, este es el estado final de la arquitectura tras la migraci\u00f3n:<\/p>\n\n\n\n<script type=\"module\">\n  import mermaid from 'https:\/\/cdn.jsdelivr.net\/npm\/mermaid@10\/dist\/mermaid.esm.min.mjs';\n  mermaid.initialize({\n    startOnLoad: true,\n    theme: 'dark',\n    themeVariables: {\n      fontSize: '13px'\n    }\n  });\n<\/script>\n\n<div style=\"display: flex; justify-content: center; align-items: center; width: 100%; margin: 20px 0; overflow-x: auto;\">\n  <pre class=\"mermaid\" style=\"background: transparent; border: none; max-width: 850px; width: 100%;\">flowchart TD\n    A[\"Usuario \/ Frontend JS\"] --&gt;|POST \/api\/chat SSE| B[ProductChatController]\n    \n    subgraph Controller [Controlador y Resiliencia]\n        direction TB\n        B --&gt;|Captura| E1[ExceedContextSizeException]\n        B --&gt;|Captura| E2[ModelNotFoundException]\n        B --&gt;|Captura| E3[ServerException]\n    end\n\n    B --&gt; C[ProductChatService]\n    C --&gt;|clona MessageBag| D[\"AgentInterface (v0.12.0)\"]\n    \n    D &lt;--&gt;|chat\/completions| G[\"Groq API (GPT OSS 120B)\"]\n    D --&gt;|Tool calls| Tools\n    \n    subgraph Tools [\"Herramientas #[AsTool]\"]\n        direction TB\n        T1[ProductSearchTool]\n        T2[ProductPriceTool]\n        T3[ProductCategoryTool]\n    end\n    \n    Tools --&gt; R[ProductRepository]\n    D --&gt;|TextDelta SSE| B\n    B --&gt; A\n  <\/pre>\n<\/div>\n\n\n\n<h2 class=\"wp-block-heading\">1. Qu\u00e9 cambi\u00f3 entre Symfony AI v0.9 y v0.12<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Al tratarse de saltos entre versiones menores de un componente experimental, es vital revisar los cambios arquitect\u00f3nicos en el ciclo de vida del agente. Estos tres cambios son los que m\u00e1s probablemente van a romper tu c\u00f3digo existente:<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">A. Mutaci\u00f3n in-place del <code>MessageBag<\/code> (desde v0.10)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">En versiones tempranas, los procesadores internos del agente clonaban el saco de mensajes. A partir de <code>v0.10<\/code>, el agente muta el <code>MessageBag<\/code> <em>in-place<\/em> para inyectar llamadas a herramientas (<code>ToolCallMessage<\/code>) y respuestas intermedias. Si nuestro servicio reutiliza la instancia o la inspecciona posteriormente, debemos ser conscientes de este comportamiento.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">B. L\u00edmite autom\u00e1tico de iteraciones de herramientas (<code>maxToolCalls: 50<\/code>)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Antes de <code>v0.10<\/code>, si un modelo entraba en un bucle alucinatorio invocando herramientas sin fin, el proceso se ejecutaba indefinidamente hasta agotar el tiempo de PHP. Ahora viene acotado por defecto a 50 iteraciones, lanzando una excepci\u00f3n si se sobrepasa.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">C. Flexibilidad en la firma de <code>AgentInterface::call()<\/code> (v0.12)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">La firma se ampli\u00f3 para aceptar <code>string|MessageBag|UserMessage<\/code>, permitiendo invocar agentes simples directamente con texto (<code>$agent-&gt;call('Hola')<\/code>), manteniendo total compatibilidad cuando enviamos un <code>MessageBag<\/code> estructurado con historial.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">2. Protegiendo el servicio conversacional (<code>ProductChatService<\/code>)<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">En nuestro servicio, construimos el historial a partir de la sesi\u00f3n de Symfony y lo convertimos en un <code>MessageBag<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Para evitar que la ejecuci\u00f3n interna del agente altere el objeto de mensajes que acabamos de estructurar, aplicamos una pr\u00e1ctica defensiva expl\u00edcita: <strong>clonar el <code>MessageBag<\/code> al invocar <code>call()<\/code><\/strong>. Sin este clone, el agente mutar\u00eda la instancia original in-place, lo que puede corromper el historial que acabas de construir o provocar que mensajes internos del ciclo de tool calls aparezcan mezclados con los mensajes del usuario en iteraciones posteriores.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/AI\/ProductChatService.php\n\npublic function streamAsk(string $userQuestion, \\Closure $onChunk): void\n{\n    $session = $this-&gt;requestStack-&gt;getSession();\n    $history = $session-&gt;get('chat_history', &#91;]);\n    $history&#91;] = &#91;'role' =&gt; 'user', 'content' =&gt; $userQuestion];\n\n    $messages = $this-&gt;buildMessageBag($history);\n\n    \/\/ Invocamos el stream protegiendo el MessageBag original contra mutaciones\n    $result = $this-&gt;defaultAgent-&gt;call(clone $messages, &#91;'stream' =&gt; true]);\n    $stream = $result-&gt;getContent();\n    $fullAnswer = '';\n\n    foreach ($stream as $delta) {\n        if ($delta instanceof \\Symfony\\AI\\Platform\\Result\\Stream\\Delta\\TextDelta) {\n            $content = $delta-&gt;getText();\n            if ($content !== '') {\n                $fullAnswer .= $content;\n                $onChunk($content);\n            }\n        }\n    }\n\n    \/\/ Guardas de seguridad e inserci\u00f3n en historial...\n    $history&#91;] = &#91;'role' =&gt; 'assistant', 'content' =&gt; $fullAnswer];\n    \n    \/\/ Mantenemos solo los \u00faltimos 10 mensajes (sliding window)\n    if (count($history) &gt; 10) {\n        $history = array_slice($history, -10);\n    }\n    \n    $session-&gt;set('chat_history', $history);\n}<\/code><\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">3. Resiliencia en el Controlador con Excepciones Granulares<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Uno de los mayores avances en Symfony AI v0.10+ es la jerarqu\u00eda tipada de excepciones en el namespace <code>Symfony\\AI\\Platform\\Exception<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">En lugar de capturar un error ciego, ahora podemos reaccionar seg\u00fan el tipo de fallo:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\/\/ src\/Controller\/ProductChatController.php\n\nnamespace App\\Controller;\n\nuse App\\AI\\ProductChatService;\nuse Symfony\\AI\\Platform\\Exception\\ExceedContextSizeException;\nuse Symfony\\AI\\Platform\\Exception\\ModelNotFoundException;\nuse Symfony\\AI\\Platform\\Exception\\ServerException;\nuse Symfony\\Bundle\\FrameworkBundle\\Controller\\AbstractController;\nuse Symfony\\Component\\HttpFoundation\\Request;\nuse Symfony\\Component\\HttpFoundation\\Response;\nuse Symfony\\Component\\HttpFoundation\\StreamedResponse;\nuse Symfony\\Component\\Routing\\Attribute\\Route;\n\nclass ProductChatController extends AbstractController\n{\n    #&#91;Route('\/api\/chat', methods: &#91;'POST'])]\n    public function chat(Request $request, ProductChatService $chatService): Response\n    {\n        $body = json_decode($request-&gt;getContent(), true);\n        $question = trim(strip_tags($body&#91;'question'] ?? ''));\n\n        if (empty($question)) {\n            return $this-&gt;json(&#91;'error' =&gt; 'Question is required'], 400);\n        }\n\n        $response = new StreamedResponse(function () use ($chatService, $question, $request) {\n            try {\n                $chatService-&gt;streamAsk($question, function (string $token) {\n                    echo \"data: \" . json_encode(&#91;'text' =&gt; $token], JSON_THROW_ON_ERROR) . \"\\n\\n\";\n                    ob_flush();\n                    flush();\n                });\n                echo \"data: &#91;DONE]\\n\\n\";\n                ob_flush();\n                flush();\n            } catch (ExceedContextSizeException $e) {\n                \/\/ 1. Contexto saturado: purgamos la sesi\u00f3n para desbloquear al usuario\n                error_log(\"AI CONTEXT ERROR: \" . $e-&gt;getMessage());\n                $request-&gt;getSession()-&gt;remove('chat_history');\n                echo \"data: \" . json_encode(&#91;\n                    'error' =&gt; 'El historial es muy largo. Se ha reiniciado la conversaci\u00f3n, por favor intenta de nuevo.'\n                ]) . \"\\n\\n\";\n                ob_flush();\n                flush();\n            } catch (ModelNotFoundException $e) {\n                \/\/ 2. Modelo no disponible o retirado del cat\u00e1logo\n                error_log(\"AI MODEL ERROR: \" . $e-&gt;getMessage());\n                echo \"data: \" . json_encode(&#91;\n                    'error' =&gt; 'El modelo de IA configurado no est\u00e1 disponible en este momento.'\n                ]) . \"\\n\\n\";\n                ob_flush();\n                flush();\n            } catch (ServerException $e) {\n                \/\/ 3. Error 5xx transitorio del proveedor (Groq, OpenAI, etc.)\n                error_log(\"AI SERVER ERROR: \" . $e-&gt;getMessage());\n                echo \"data: \" . json_encode(&#91;\n                    'error' =&gt; 'El servicio de IA est\u00e1 temporalmente saturado. Intenta de nuevo en unos segundos.'\n                ]) . \"\\n\\n\";\n                ob_flush();\n                flush();\n            } catch (\\Throwable $e) {\n                \/\/ 4. \u00daltima l\u00ednea de defensa\n                error_log(\"AI ERROR: \" . $e-&gt;getMessage() . \"\\n\" . $e-&gt;getTraceAsString());\n                $errorMsg = \"Error inesperado: \" . $e-&gt;getMessage();\n                echo \"data: \" . json_encode(&#91;'error' =&gt; $errorMsg]) . \"\\n\\n\";\n                ob_flush();\n                flush();\n            }\n        });\n\n        $response-&gt;headers-&gt;set('Content-Type', 'text\/event-stream');\n        $response-&gt;headers-&gt;set('Cache-Control', 'no-cache');\n        $response-&gt;headers-&gt;set('X-Accel-Buffering', 'no');\n        $response-&gt;headers-&gt;set('Connection', 'keep-alive');\n\n        return $response;\n    }\n}<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">\u00bfQu\u00e9 ganamos con esto?<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Auto-recuperaci\u00f3n de sesi\u00f3n:<\/strong> Si un usuario env\u00eda textos masivos y desborda los tokens permitidos (<code>ExceedContextSizeException<\/code>), la sesi\u00f3n se limpia autom\u00e1ticamente en el backend sin dejar al usuario en un bucle de error irrecuperable.<\/li>\n\n\n\n<li><strong>Claridad diagn\u00f3stica:<\/strong> Distinguimos entre un fallo transitorio de infraestructura (503\/504 v\u00eda <code>ServerException<\/code>) y un problema de configuraci\u00f3n o la no disponibilidad del modelo (<code>ModelNotFoundException<\/code>).<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">4. Lecciones en Producci\u00f3n: Tama\u00f1o de Modelo vs. Prompt Engineering<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Durante las pruebas de migraci\u00f3n nos enfrentamos a un dilema habitual: \u00bfpor qu\u00e9 un modelo a veces omite categor\u00edas, ignora el l\u00edmite de paginaci\u00f3n o responde preguntas fuera de tema?<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">El experimento con modelos compactos<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Al probar con modelos m\u00e1s reducidos (como modelos de ~27B par\u00e1metros), observamos dos conductas frecuentes:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Volcado sin paginar:<\/strong> Al preguntar por \u00abtodos los productos\u00bb, en lugar de llamar primero a <code>categories<\/code> y esperar instrucciones, ejecutaba b\u00fasquedas y volcaba todos los resultados salt\u00e1ndose la instrucci\u00f3n de 3 elementos por p\u00e1gina.<\/li>\n\n\n\n<li>2. <strong>Deriva tem\u00e1tica (<em>Off-topic<\/em>):<\/strong> Respond\u00eda a preguntas de cultura general o geograf\u00eda en lugar de concentrarse en la tienda.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">La regla de <em>Stay on Topic<\/em><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Para corregir la deriva tem\u00e1tica, reforzamos las directrices del <em>System Prompt<\/em>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Message::forSystem(\n    \"You are a helpful online store assistant. Reply ONLY in Spanish.\\n\\n\" .\n    \"AVAILABLE TOOLS:\\n\" .\n    \"- 'categories': list available types.\\n\" .\n    \"- 'search': lists products by category. Pass exact slug (telefono, portatil, auriculares, reloj). Page defaults to '1' ('2' for more).\\n\" .\n    \"- 'price': gets the price of an SKU.\\n\\n\" .\n    \"RULES:\\n\" .\n    \"1. Map user synonyms to exact slugs (e.g., 'laptops'-&gt;'portatil', 'relojes'-&gt;'reloj' or 'celulares'-&gt;'telefono').\\n\" .\n    \"2. NO HALLUCINATION: DO NOT invoke unlisted tools (like order\/checkout). Never invent products or SKUs.\\n\" .\n    \"3. NO SALES: You cannot process orders or take payments; decline gracefully.\\n\" .\n    \"4. STAY ON TOPIC: Only answer questions about the store, its products and categories. For unrelated questions, politely redirect the user to the store's offerings.\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">El equilibrio: no inflar el prompt innecesariamente<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Es tentador agregar decenas de reglas al prompt para tapar cada fallo de un modelo peque\u00f1o. Sin embargo, en arquitecturas con herramientas, <strong>la capacidad de seguimiento instruccional (<em>instruction-following<\/em>) del modelo es determinante<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">l configurar un modelo con mayor capacidad de razonamiento en Groq, como <strong>GPT OSS 120B<\/strong> (<code>openai\/gpt-oss-120b<\/code>), el agente ejecut\u00f3 el flujo con precisi\u00f3n milim\u00e9trica:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code># config\/packages\/ai.yaml\nai:\n    platform:\n        generic:\n            groq:\n                base_url: 'https:\/\/api.groq.com\/openai'\n                api_key: '%env(GROQ_API_KEY)%'\n                model_catalog: 'ai.platform.model_catalog.generic.fallback'\n                supports_completions: true\n                supports_embeddings: true\n                completions_path: '\/v1\/chat\/completions'\n                embeddings_path: '\/v1\/embeddings'\n    agent:\n        default:\n            platform: 'ai.platform.generic.groq'\n            model: 'openai\/gpt-oss-120b'\n            tools:\n                services:\n                    - { service: 'App\\AI\\Tool\\ProductSearchTool' }\n                    - { service: 'App\\AI\\Tool\\ProductPriceTool' }\n                    - { service: 'App\\AI\\Tool\\ProductCategoryTool' }<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Resultado obtenido:<\/strong><\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>OK<\/strong> &#8211; Pregunta general &#8211;&gt; Consulta <code>categories<\/code> y presenta las 4 categor\u00edas disponibles.<\/li>\n\n\n\n<li><strong>OK<\/strong> &#8211; Petici\u00f3n de lista &#8211;&gt; Muestra exactamente 3 productos (P\u00e1gina 1) y ofrece la P\u00e1gina 2.<\/li>\n\n\n\n<li><strong>OK<\/strong> &#8211; Petici\u00f3n \u00abP\u00e1gina 2\u00bb &#8211;&gt; Recupera los elementos restantes mediante paginaci\u00f3n real.<\/li>\n\n\n\n<li><strong>OK<\/strong> &#8211; Deducci\u00f3n contextual &#8211;&gt; Identifica el producto m\u00e1s barato cruzando el cat\u00e1logo.<\/li>\n\n\n\n<li><strong>OK<\/strong> &#8211; Rechazo elegante &#8211;&gt; Declina pedidos de venta y preguntas fuera de tema.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">\u00bfQue beneficios nos trajo la actualizaci\u00f3n?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">La migraci\u00f3n a <strong>Symfony AI v0.12.0<\/strong> demuestra que el framework est\u00e1 madurando con paso firme:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Las <strong>excepciones tipadas<\/strong> permiten pasar de un manejo de errores tosco a una experiencia de usuario que se autorecupera ante situaciones como desbordamiento de contexto o ca\u00eddas del proveedor.<\/li>\n\n\n\n<li>El desacoplamiento de herramientas mediante <code>#[AsTool]<\/code> sigue siendo el pilar m\u00e1s s\u00f3lido y elegante: las herramientas no requirieron ni una sola l\u00ednea de modificaci\u00f3n en toda la migraci\u00f3n.<\/li>\n\n\n\n<li>Elegir el modelo adecuado para Tool Calling ahorra complejidad en el prompt y garantiza que las reglas de negocio (paginaci\u00f3n, idioma, l\u00edmites) se cumplan de manera determinista.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">En la siguiente serie exploraremos caracter\u00edsticas avanzadas: <strong>Structured Output con DTOs tipados<\/strong>, depuraci\u00f3n profunda con el <strong>Profiler de Symfony AI<\/strong> y estrategias de <strong>Failover entre proveedores<\/strong>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sigamos codificando.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Cuando trabajamos con herramientas en fase experimental, como Symfony AI, la velocidad de evoluci\u00f3n es tanto una ventaja como un desaf\u00edo. Entre la versi\u00f3n v0.9.0 y la v0.12.0, el componente no solo refin\u00f3 sus firmas de API y su arquitectura interna, sino que introdujo herramientas esenciales para construir agentes verdaderamente resilientes y preparados para los [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_piko_noindex":"","footnotes":""},"categories":[7],"tags":[134,138,15],"class_list":["post-352","post","type-post","status-publish","format-standard","hentry","category-desarrollo","tag-ia","tag-llm","tag-php"],"_links":{"self":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/352","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/comments?post=352"}],"version-history":[{"count":0,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/posts\/352\/revisions"}],"wp:attachment":[{"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/media?parent=352"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/categories?post=352"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/juredev.com\/blog\/wp-json\/wp\/v2\/tags?post=352"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}