<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: ZAKARIA KHCHICHE</title>
    <description>The latest articles on DEV Community by ZAKARIA KHCHICHE (@zakaria_khchiche_490919ed).</description>
    <link>https://dev.to/zakaria_khchiche_490919ed</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4154395%2F8560915b-8ac4-4591-9017-961aa77a1c4b.jpg</url>
      <title>DEV Community: ZAKARIA KHCHICHE</title>
      <link>https://dev.to/zakaria_khchiche_490919ed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/zakaria_khchiche_490919ed"/>
    <language>en</language>
    <item>
      <title>An OpenAI tool Mistral rejected: the SDK's strict: null</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Wed, 21 Oct 2026 14:14:35 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/an-openai-tool-mistral-rejected-the-sdks-strict-null-3oep</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/an-openai-tool-mistral-rejected-the-sdks-strict-null-3oep</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;: mistral-common's &lt;code&gt;Function.from_openai&lt;/code&gt; rejected any tool definition containing &lt;code&gt;"strict": null&lt;/code&gt;. That is exactly what the OpenAI SDK produces by default when you dump a tool. My fix (&lt;a href="https://github.com/mistralai/mistral-common/pull/396" rel="noopener noreferrer"&gt;PR #396&lt;/a&gt;) was merged by the Mistral AI maintainers on October 21, 2026, and will ship in the next release after 1.12.0. Until then, there is a one-line workaround (below).&lt;/p&gt;

&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;You are moving agents from OpenAI to Mistral, or running an OpenAI-compatible gateway in front of Mistral models. Your tools are typed with the OpenAI SDK, then converted by mistral-common:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;openai.types.shared&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;FunctionDefinition&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mistral_common.protocol.instruct.tool_calls&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Function&lt;/span&gt;

&lt;span class="n"&gt;tool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FunctionDefinition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;get_weather&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On mistral-common 1.12.0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pydantic_core._pydantic_core.ValidationError: 1 validation error for Function
strict
  Input should be a valid boolean [type=bool_type, input_value=None, input_type=NoneType]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tool is valid. &lt;code&gt;Tool.from_openai&lt;/code&gt; and &lt;code&gt;ChatCompletionRequest.from_openai(tools=...)&lt;/code&gt; fail the same way, because they call the same function.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause
&lt;/h2&gt;

&lt;p&gt;In the OpenAI schema, &lt;code&gt;strict&lt;/code&gt; is &lt;strong&gt;optional and nullable&lt;/strong&gt;, so the SDK dumps it as &lt;code&gt;None&lt;/code&gt; when you don't set it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;get_weather&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;parameters&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Function.from_openai&lt;/code&gt; already replaced &lt;code&gt;None&lt;/code&gt; with defaults for &lt;code&gt;parameters&lt;/code&gt; and &lt;code&gt;description&lt;/code&gt;, but not for &lt;code&gt;strict&lt;/code&gt;. On the Mistral side, &lt;code&gt;strict&lt;/code&gt; is a boolean that defaults to &lt;code&gt;False&lt;/code&gt;, so Pydantic rejected &lt;code&gt;None&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is a silent integration bug: each library is right on its own, and the failure only shows up where they meet.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Two lines in &lt;code&gt;Function.from_openai&lt;/code&gt;: when &lt;code&gt;strict&lt;/code&gt; is &lt;code&gt;None&lt;/code&gt;, drop the field so that the model falls back to its default (&lt;code&gt;False&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;filtered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;strict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;True&lt;/code&gt; and &lt;code&gt;False&lt;/code&gt; behave as before. Two tests cover &lt;code&gt;Function.from_openai&lt;/code&gt; and &lt;code&gt;Tool.from_openai&lt;/code&gt; with &lt;code&gt;"strict": None&lt;/code&gt;. The full suite (1,294 tests), ruff and mypy all pass.&lt;/p&gt;

&lt;p&gt;Timeline:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/mistralai/mistral-common/issues/393" rel="noopener noreferrer"&gt;issue #393&lt;/a&gt; on October 20 (minimal reproduction);&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mistralai/mistral-common/pull/396" rel="noopener noreferrer"&gt;PR #396&lt;/a&gt; the same day;&lt;/li&gt;
&lt;li&gt;merged on October 21.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  If you are on 1.12.0 today
&lt;/h2&gt;

&lt;p&gt;Until the next release, dump your tools without null fields:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_openai&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;model_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exclude_none&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your tools arrive as raw JSON (in a gateway, for example), drop &lt;code&gt;strict&lt;/code&gt; when it is &lt;code&gt;null&lt;/code&gt; before converting.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second report, fixed differently by Mistral
&lt;/h2&gt;

&lt;p&gt;The same day I reported an inconsistency in the tokenizers (&lt;a href="https://github.com/mistralai/mistral-common/issues/395" rel="noopener noreferrer"&gt;issue #395&lt;/a&gt;). &lt;code&gt;Tekkenizer.decode&lt;/code&gt; still accepted the legacy integer special token policies (0, 1, 2), but &lt;code&gt;id_to_byte_piece&lt;/code&gt; rejected them.&lt;/p&gt;

&lt;p&gt;I had proposed aligning &lt;code&gt;id_to_byte_piece&lt;/code&gt; with &lt;code&gt;decode&lt;/code&gt;. The maintainers confirmed the bug and chose the opposite approach: they removed the integer backward compatibility everywhere (&lt;a href="https://github.com/mistralai/mistral-common/pull/402" rel="noopener noreferrer"&gt;PR #402&lt;/a&gt;, October 21). It is their design call, and a consistent one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What it means for you&lt;/strong&gt;: if your code calls &lt;code&gt;decode(..., special_token_policy=1)&lt;/code&gt;, it will fail with the next release. Switch to the enum, or to its string value, now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;mistral_common.tokens.tokenizers.base&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SpecialTokenPolicy&lt;/span&gt;
&lt;span class="n"&gt;tokenizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;special_token_policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SpecialTokenPolicy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;KEEP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# or "keep"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A tool dumped by the OpenAI SDK contains &lt;code&gt;"strict": None&lt;/code&gt;. mistral-common 1.12.0 rejects it, and the next release accepts it.&lt;/li&gt;
&lt;li&gt;Workaround today: &lt;code&gt;model_dump(exclude_none=True)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Integer special token policies (0, 1, 2) are going away: use &lt;code&gt;SpecialTokenPolicy&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;When migrating from OpenAI to Mistral, test the conversion of your tools exactly as your code produces them, not only hand-written dicts.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Related
&lt;/h2&gt;

&lt;p&gt;This is the second contribution merged by Mistral AI, after &lt;a href="https://github.com/mistralai/mistral-common/pull/349" rel="noopener noreferrer"&gt;PR #349&lt;/a&gt; on offline tokenizers (&lt;a href="https://dev.to/zakaria_khchiche_490919ed/running-mistral-offline-the-tokenizer-is-on-disk-but-the-library-cant-find-it-5a2"&gt;article&lt;/a&gt;). Other fixes are awaiting review: &lt;a href="https://dev.to/zakaria_khchiche_490919ed/three-silent-bugs-in-mistral-ais-python-libraries-1gf"&gt;Three silent bugs in Mistral AI's Python libraries&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Zakaria Khchiche, freelance Data &amp;amp; AI Tech Lead: 200+ AI agents in production (Copilot Studio, Azure AI Foundry, Azure OpenAI), contributor to Microsoft Agent Framework and Mistral AI. &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want your team to build agents like these? I run a hands-on Copilot Studio training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-copilot-studio/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/formation-copilot-studio/&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/kit-ai-act/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>mistral</category>
      <category>openai</category>
      <category>python</category>
      <category>llm</category>
    </item>
    <item>
      <title>Taking a Copilot Studio agent to production: the 6 steps I use after 200+ agents</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Fri, 16 Oct 2026 08:05:36 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/taking-a-copilot-studio-agent-to-production-the-6-steps-i-use-after-200-agents-26mn</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/taking-a-copilot-studio-agent-to-production-the-6-steps-i-use-after-200-agents-26mn</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; a Copilot Studio agent is ready for production when six things are in place: a clear scope, testable instructions, a clean knowledge base, readable scanned documents, real connections to your systems, and security and lifecycle management agreed with IT. I'm a freelance Data &amp;amp; AI Tech Lead in Paris. At SUEZ I worked on 20 generative AI use cases and put more than 200 AI agents into production. This is the method I apply.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Scope the agent
&lt;/h2&gt;

&lt;p&gt;Define its role, its users and what it must refuse. Then choose between generative orchestration (you describe the agent and give it sources, tools and memory) and hand-designed topics. Generative orchestration is faster to build and less predictable, so keep designed topics for the paths that must never vary.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Write instructions you can test
&lt;/h2&gt;

&lt;p&gt;Instructions are the agent's contract. I write them in five blocks: who the agent is, what it does, what it refuses, how it answers, and what it does when it doesn't know. Each block comes with test questions, so a change to the instructions can be checked instead of guessed.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat the knowledge base like a database
&lt;/h2&gt;

&lt;p&gt;Most bad answers blamed on the model come from the documents: outdated versions, duplicates, conflicting files. Keep one up-to-date version of each document in SharePoint or your file sources before you tune anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make scanned documents readable
&lt;/h2&gt;

&lt;p&gt;For an agent, a PDF that is only an image is a blank page. Contracts and technical sheets often are. Add OCR and controlled preprocessing before indexing.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Connect the agent to your systems
&lt;/h2&gt;

&lt;p&gt;An agent that explains how to open a ticket helps. An agent that opens the ticket changes how people work. Use connectors, APIs, MCP servers and agent flows, and let flows handle what must be guaranteed: the model reasons, the flow executes.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Secure and industrialize
&lt;/h2&gt;

&lt;p&gt;Agree with IT and security on access rights, traceability, environments and ALM (dev, test, production). Then monitor answer quality with the same test set as in step 2, after every change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;p&gt;These steps don't make an agent infallible. They make its failures visible and fixable. When you need fine-grained model choice or cost control at scale, Azure AI Foundry is often a better fit than Copilot Studio, and in large groups the two coexist.&lt;/p&gt;

&lt;h2&gt;
  
  
  Going further
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Detailed method (French), with FAQ: &lt;a href="https://zakariakhchiche.github.io/copilot-studio/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/copilot-studio/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Answering only when a source supports it: &lt;a href="https://dev.to/zakaria_khchiche_490919ed/copilot-studio-over-thousands-of-documents-answer-only-when-a-source-supports-it-2p45"&gt;Copilot Studio over thousands of documents&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Zakaria Khchiche, freelance Data &amp;amp; AI Tech Lead (Paris). Contributor to Microsoft Agent Framework. &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Want your team to build agents like these? I run a hands-on Copilot Studio training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-copilot-studio/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/formation-copilot-studio/&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/kit-ai-act/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>copilotstudio</category>
      <category>ai</category>
      <category>microsoft</category>
      <category>powerplatform</category>
    </item>
    <item>
      <title>Locating a signature in a scanned form with Jev 1.14 (TypeSafe) and Microsoft Agent Framework</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Thu, 15 Oct 2026 12:16:19 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/locating-a-signature-in-a-scanned-form-with-jev-114-typesafe-and-microsoft-agent-framework-5214</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/locating-a-signature-in-a-scanned-form-with-jev-114-typesafe-and-microsoft-agent-framework-5214</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Jev 1.14 (TypeSafe AI, October 14, 2026) accepts images and a new &lt;code&gt;Box&lt;/code&gt; question type that returns the bounding box of one target in an image. You can use it today with the TypeSafe Python SDK, and I proposed support in the Microsoft Agent Framework TypeSafe connector (&lt;a href="https://github.com/microsoft/agent-framework/pull/9411" rel="noopener noreferrer"&gt;PR #9411&lt;/a&gt;, awaiting review). TypeSafe flags both features as experimental ("Danger Zone"): prototypes only.&lt;/p&gt;

&lt;h2&gt;
  
  
  The use case: is the form signed, and where?
&lt;/h2&gt;

&lt;p&gt;On document agents, scanned files cause a large share of failures: signed forms, stamps, handwritten fields. Before extracting anything, two simple questions often matter: &lt;em&gt;is the document signed?&lt;/em&gt; and &lt;em&gt;where is the signature?&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Jev type&lt;/th&gt;
&lt;th&gt;Answer&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Is the form signed?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Noul&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;probability from 0 to 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Where is the signature?&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Box&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;top&lt;/code&gt;, &lt;code&gt;bottom&lt;/code&gt;, &lt;code&gt;left&lt;/code&gt;, &lt;code&gt;right&lt;/code&gt; as fractions of the image, plus &lt;code&gt;confidence&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Requirements: model &lt;code&gt;jev-1.14.0&lt;/code&gt; or &lt;code&gt;jev-preview&lt;/code&gt;, header &lt;code&gt;TypeSafe-Danger-Zone-Images: true&lt;/code&gt;, and an image in the request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Today: with the TypeSafe SDK
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typesafe_sdk&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;AsyncTypeSafeClient&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Noul&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;AsyncTypeSafeClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;form.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
    &lt;span class="n"&gt;questions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Is the form signed?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the signature on the form&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jev-1.14.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;extra_headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;TypeSafe-Danger-Zone-Images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;answers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;box&lt;/span&gt;  &lt;span class="c1"&gt;# fractions of width and height
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Proposed: from a Microsoft Agent Framework agent
&lt;/h2&gt;

&lt;p&gt;The connector currently rejects any non-text content. PR #9411 maps inline image content to TypeSafe image assets and adds the header only when the request contains an image or a &lt;code&gt;Box&lt;/code&gt; question; text-only requests are unchanged.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;Message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;The scanned form:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;png_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;media_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image/png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])],&lt;/span&gt;
    &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;model&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;jev-1.14.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response_format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Noul&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Is the form signed?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;signature&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;Box&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instructions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;the signature on the form&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This API is a proposal under review and may change before it is merged.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three pitfalls
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No "not found" answer.&lt;/strong&gt; &lt;code&gt;Box&lt;/code&gt; always returns a box, even when the target is absent. Ask a &lt;code&gt;Noul&lt;/code&gt; presence question in the same request and ignore the box when it is low.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One box per question.&lt;/strong&gt; Two signatures means two questions, each described so they cannot be confused ("the signature at the bottom right").&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small targets in large images.&lt;/strong&gt; Crop the region first, then ask again on the crop. TypeSafe reports that checking each edge with a &lt;code&gt;Noul&lt;/code&gt; question caught 30 of 36 clipped boxes in its tests.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Jev 1.14 adds images and &lt;code&gt;Box&lt;/code&gt;, still experimental: keep them out of production for now.&lt;/li&gt;
&lt;li&gt;Always pair &lt;code&gt;Box&lt;/code&gt; with a presence &lt;code&gt;Noul&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Build a test set from your own scans before choosing a confidence threshold; TypeSafe recommends none.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sources: &lt;a href="https://typesafe.ai/blog/jev-1-14" rel="noopener noreferrer"&gt;TypeSafe blog, Jev 1.14&lt;/a&gt; · &lt;a href="https://docs.typesafe.ai/primitives/box" rel="noopener noreferrer"&gt;Box documentation&lt;/a&gt; · &lt;a href="https://github.com/microsoft/agent-framework/issues/9410" rel="noopener noreferrer"&gt;Issue #9410&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Zakaria Khchiche, freelance Tech Lead Data &amp;amp; AI, 200+ AI agents in production. &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Want your team to build agents like these? I run a hands-on Copilot Studio training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-copilot-studio/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/formation-copilot-studio/&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/kit-ai-act/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Can you deploy Hermes Agent in a Microsoft enterprise? Intune, Teams, Entra ID and Graph, checked against the code</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Fri, 09 Oct 2026 13:27:47 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/can-you-deploy-hermes-agent-in-a-microsoft-enterprise-intune-teams-entra-id-and-graph-checked-4i9n</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/can-you-deploy-hermes-agent-in-a-microsoft-enterprise-intune-teams-entra-id-and-graph-checked-4i9n</guid>
      <description>&lt;p&gt;&lt;strong&gt;Short answer:&lt;/strong&gt; yes for a scoped pilot, not yet for a company-wide rollout without groundwork. Now that it is listed on the Microsoft Store, Hermes Agent (Nous Research, open source, MIT) can be pushed through Intune like any other Store app. It already talks to Teams, Microsoft Graph and Microsoft Foundry through Entra ID. Governance (secrets, access control, audit) is still on IT.&lt;/p&gt;

&lt;p&gt;I'm a Data &amp;amp; AI Tech Lead who puts agents into production for large companies (200+ agents in production at SUEZ). I read the Microsoft-facing code of Hermes Agent and proposed two fixes, now under review by the maintainers. Here is what matters for a Microsoft 365 shop.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Microsoft Store listing changes
&lt;/h2&gt;

&lt;p&gt;Until now, installing Hermes Agent on Windows meant a PowerShell script or a terminal command. No IT department signs off on that for 500 laptops.&lt;/p&gt;

&lt;p&gt;A Store app deploys through Intune with the "Microsoft Store app (new)" type (winget-based):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;targeting by Entra ID group (a pilot group, not the whole company);&lt;/li&gt;
&lt;li&gt;"available" in Company Portal or "required";&lt;/li&gt;
&lt;li&gt;updates tracked automatically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two cautions: check the package publisher before approving it, and test Autopilot, where Store apps are only partially supported. Store certification checks for malware and store-policy compliance; it is neither a security audit nor a GDPR review.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Hermes Agent already does with Microsoft
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Microsoft piece&lt;/th&gt;
&lt;th&gt;What exists in Hermes Agent&lt;/th&gt;
&lt;th&gt;What you need to plan&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Teams (chat)&lt;/td&gt;
&lt;td&gt;Native Bot Framework adapter: personal chats, group chats, channels&lt;/td&gt;
&lt;td&gt;Entra ID app registration, public HTTPS endpoint, user allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams (approvals)&lt;/td&gt;
&lt;td&gt;Sensitive commands arrive as an Adaptive Card: approve once, for the session, always, or deny&lt;/td&gt;
&lt;td&gt;Default deny when no allowlist is set: keep it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Teams (meetings)&lt;/td&gt;
&lt;td&gt;Meeting pipeline over Microsoft Graph: subscriptions, transcripts, summary posted to a channel or chat&lt;/td&gt;
&lt;td&gt;Admin consent, regular Graph subscription renewal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Microsoft Foundry&lt;/td&gt;
&lt;td&gt;Keyless Entra ID auth (&lt;code&gt;DefaultAzureCredential&lt;/code&gt; chain: service principal, managed identity, Azure CLI…)&lt;/td&gt;
&lt;td&gt;Model and hosting region choice&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sovereign clouds (GCC High, DoD, 21Vianet)&lt;/td&gt;
&lt;td&gt;Configurable authority&lt;/td&gt;
&lt;td&gt;Fix proposed so Graph calls follow the right cloud (see below)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most mature part, in my view, is command approval in Teams. When the agent wants to run a command flagged as dangerous, it posts a card with four buttons, and only allowlisted users can click. Without an allowlist, the click is rejected. That is exactly what you want from a tool with terminal access.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is missing for a company-wide rollout
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No documented admin tooling:&lt;/strong&gt; the docs describe no central admin console, SSO or audit log.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets in a file:&lt;/strong&gt; Teams and Graph credentials are read from a &lt;code&gt;.env&lt;/code&gt; file on the gateway host.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ID-based access control:&lt;/strong&gt; access to the Teams bot is an allowlist of Entra ID object IDs, not a group.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demanding Teams setup:&lt;/strong&gt; app registration, admin consent, a stable public URL with a valid certificate, secret rotation. Plan a few hours with Azure experience.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An agent that acts on the machine:&lt;/strong&gt; it can run commands and scripts. Without Intune rules or an isolated backend (Docker, for example), that is shadow AI waiting to happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Two fixes I proposed
&lt;/h2&gt;

&lt;p&gt;Reading the Microsoft-facing code, I found two concrete defects and proposed a fix for each. Both are &lt;strong&gt;under review&lt;/strong&gt; by the maintainers, not merged yet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Microsoft Graph in sovereign clouds&lt;/strong&gt; (&lt;a href="https://github.com/NousResearch/hermes-agent/pull/135618" rel="noopener noreferrer"&gt;PR #135618&lt;/a&gt;). The docs explain how to point authentication at &lt;code&gt;login.microsoftonline.us&lt;/code&gt; for GCC High, but the Graph client always sent its requests to &lt;code&gt;graph.microsoft.com&lt;/code&gt;. A token issued for a national cloud is not accepted by the global service, so every request was rejected. The fix derives the Graph root from the configured scope, limited to the four Graph roots Microsoft publishes. No new setting, and the token is never sent anywhere else.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Localized meeting summaries&lt;/strong&gt; (&lt;a href="https://github.com/NousResearch/hermes-agent/pull/135619" rel="noopener noreferrer"&gt;PR #135619&lt;/a&gt;). In webhook mode, empty summary sections (decisions, action items, risks) printed an English "None", while Graph mode already used the translated value. The fix uses the same translation in both modes.&lt;/p&gt;

&lt;p&gt;Each fix comes with a test that fails without it and passes the project's checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  My pre-pilot checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; one use case, one Entra ID pilot group, an "available" (not "required") Intune assignment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model:&lt;/strong&gt; Microsoft Foundry with Entra ID auth (no API key to store), in a European region.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Teams:&lt;/strong&gt; a dedicated app registration, a user allowlist, never tenant-wide open access, mention-only replies in channels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execution:&lt;/strong&gt; command approval on, execution in an isolated backend rather than directly on the laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Secrets:&lt;/strong&gt; a &lt;code&gt;.env&lt;/code&gt; file with restricted permissions, scheduled rotation of Entra ID secrets.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Meetings:&lt;/strong&gt; documented admin consent, a scheduled job renewing Graph subscriptions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Training:&lt;/strong&gt; pilot users trained on uses and limits. In the EU, Article 4 of the AI Act, rewritten by Regulation (EU) 2026/1744 in force since 27 July 2026, now asks employers to "promote" AI literacy (not legal advice).&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The Microsoft Store listing makes Hermes Agent deployable through Intune: that is the real news for IT.&lt;/li&gt;
&lt;li&gt;The Teams, Graph and Foundry integration already exists and is serious, especially Adaptive Card command approval.&lt;/li&gt;
&lt;li&gt;Governance (secrets, access, audit) remains the company's job: run a scoped pilot before any rollout.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Zakaria Khchiche, Data &amp;amp; AI Tech Lead (Paris), AI trainer at Spar-x. 200+ agents in production at SUEZ; engagements for TotalEnergies, Groupe SNCF, La Banque Postale, Volvo Group. Open-source contributor (Microsoft Agent Framework, Mistral, OGX).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;LinkedIn: &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;https://www.linkedin.com/in/zakariakhchiche/&lt;/a&gt; · Website: &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Want your team to build agents like these? I run a hands-on generative AI and agents-in-production training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-ia-generative/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/formation-ia-generative/&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/kit-ai-act/&lt;/a&gt; · Quote request: &lt;a href="https://forms.gle/ARaTCq9ADgqvJfKL8" rel="noopener noreferrer"&gt;https://forms.gle/ARaTCq9ADgqvJfKL8&lt;/a&gt;&lt;/p&gt;

</description>
      <category>microsoft</category>
      <category>ai</category>
      <category>agents</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Microsoft Agent Framework behind a corporate proxy: the HTTP transport that ignored HTTPS_PROXY</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Tue, 06 Oct 2026 13:27:32 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/microsoft-agent-framework-behind-a-corporate-proxy-the-http-transport-that-ignored-httpsproxy-2od4</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/microsoft-agent-framework-behind-a-corporate-proxy-the-http-transport-that-ignored-httpsproxy-2od4</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published in French on &lt;a href="https://medium.com/@ZKHCHICHE/microsoft-agent-framework-derri%C3%A8re-un-proxy-dentreprise-le-transport-http-qui-ignorait-41d6de99876b" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An agent works on the developer's laptop, then can't reach its API once deployed at the client. No clear exception, no useful trace: the call just never gets through. In a large company, the first suspect is usually the same: the proxy.&lt;/p&gt;

&lt;p&gt;That's what happened to me with the TypeSafe (Jev) connector in Microsoft Agent Framework. The fix was merged by the Microsoft team on October 6, 2026: &lt;a href="https://github.com/microsoft/agent-framework/pull/8984" rel="noopener noreferrer"&gt;microsoft/agent-framework#8984&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context
&lt;/h2&gt;

&lt;p&gt;I use Jev, TypeSafe's model, to decide whether a passage retrieved from a large document library actually supports an answer. Microsoft Agent Framework ships a Python connector for TypeSafe. In corporate environments, servers reach the Internet through a proxy configured with &lt;code&gt;HTTPS_PROXY&lt;/code&gt;, &lt;code&gt;HTTP_PROXY&lt;/code&gt;, &lt;code&gt;ALL_PROXY&lt;/code&gt; and &lt;code&gt;NO_PROXY&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The symptom: the TypeSafe SDK on its own reached the API in that environment. The same call through the Agent Framework connector did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root cause
&lt;/h2&gt;

&lt;p&gt;When the connector created its own client (&lt;code&gt;AsyncTypeSafeClient&lt;/code&gt;), it passed a custom HTTP transport, &lt;code&gt;_ApiKeyTransport&lt;/code&gt;, whose only job was to add the &lt;code&gt;Authorization: Bearer &amp;lt;key&amp;gt;&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;httpx only reads the proxy environment variables when no custom transport is given. By replacing the transport, the connector silently disabled the company's whole network configuration.&lt;/p&gt;

&lt;p&gt;A textbook silent bug: no error when the client is created, no warning. The setting exists; it is just ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The custom transport was redundant: the TypeSafe SDK (0.7.1 and 0.7.2) already sends the &lt;code&gt;Authorization&lt;/code&gt; header itself. It redacts that header in log records and exception messages, not on the wire.&lt;/p&gt;

&lt;p&gt;The fix:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;removes &lt;code&gt;_ApiKeyTransport&lt;/code&gt;, so the SDK builds its default transport and proxy variables apply again;&lt;/li&gt;
&lt;li&gt;updates the connector's &lt;code&gt;AGENTS.md&lt;/code&gt; note: connector-owned clients rely on the SDK for the bearer header and keep its transport;&lt;/li&gt;
&lt;li&gt;keeps transport-level tests that check the configured key is actually sent in the outgoing request.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Less code, standard network behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  The test to add to your own integrations
&lt;/h2&gt;

&lt;p&gt;Whenever an HTTP client gets a custom transport, adapter or session, check two things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;the proxy&lt;/strong&gt;: run the integration with &lt;code&gt;HTTPS_PROXY&lt;/code&gt; pointing to a test proxy and check the request goes through it;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;the header actually sent&lt;/strong&gt;: test the request that leaves the client (with a mock transport that captures it), not just the object's configuration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A test on configuration says "the key is set". A test on the request says "the key is sent, through the right path".&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Replacing the httpx transport makes it ignore &lt;code&gt;HTTPS_PROXY&lt;/code&gt;, &lt;code&gt;NO_PROXY&lt;/code&gt; and the other proxy variables.&lt;/li&gt;
&lt;li&gt;Before adding a transport for a single header, check whether the SDK already sends it.&lt;/li&gt;
&lt;li&gt;Test what goes on the wire, not what the object thinks it sends.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is my first fix in Microsoft Agent Framework. I've also had fixes merged into Mistral AI (mistral-common) and OGX (formerly Llama Stack), with the same method: audit a component under real conditions before adopting it, and fix it at the source.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="https://dev.to/zakaria_khchiche_490919ed/locating-a-signature-in-a-scanned-form-with-jev-114-typesafe-and-microsoft-agent-framework-5214"&gt;Locating a signature in a scanned form with Jev 1.14 (TypeSafe) and Microsoft Agent Framework&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Zakaria Khchiche is a freelance Data &amp;amp; AI Tech Lead in Paris. He builds and runs AI agents in production for large companies and contributes to open-source agent frameworks. &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want your team to build agents like these? I run a hands-on Copilot Studio training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-copilot-studio/" rel="noopener noreferrer"&gt;training program&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;AI Act kit&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
    </item>
    <item>
      <title>Running Mistral offline: the tokenizer is on disk, but the library can't find it</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Mon, 05 Oct 2026 19:07:52 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/running-mistral-offline-the-tokenizer-is-on-disk-but-the-library-cant-find-it-5a2</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/running-mistral-offline-the-tokenizer-is-on-disk-but-the-library-cant-find-it-5a2</guid>
      <description>&lt;p&gt;An inference server cut off from the Internet. A dedicated disk for models. Mistral's tokenizer downloaded in advance, sitting in its folder. And at startup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;FileNotFoundError: No local files found for the repo ID mistralai/Mistral-7B-v0.1 and revision None.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file is there. The library looks somewhere else.&lt;/p&gt;

&lt;p&gt;I found this bug in &lt;a href="https://github.com/mistralai/mistral-common" rel="noopener noreferrer"&gt;mistral-common&lt;/a&gt;, Mistral AI's open-source library that prepares requests for its models. The fix was reviewed, approved and merged by a Mistral AI maintainer (&lt;a href="https://github.com/mistralai/mistral-common/pull/349" rel="noopener noreferrer"&gt;PR #349&lt;/a&gt;, &lt;a href="https://github.com/mistralai/mistral-common/issues/348" rel="noopener noreferrer"&gt;issue #348&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  Who is affected
&lt;/h2&gt;

&lt;p&gt;Teams running Mistral models &lt;strong&gt;without Internet access&lt;/strong&gt; that store their models &lt;strong&gt;outside the default Hugging Face cache&lt;/strong&gt;. That is exactly the enterprise setup: air-gapped network for security, corporate proxy, a dedicated model disk on the GPU server.&lt;/p&gt;

&lt;p&gt;Two situations trigger it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the caller forbids network access (&lt;code&gt;local_files_only=True&lt;/code&gt;);&lt;/li&gt;
&lt;li&gt;the Hub is unreachable (outage, firewall, &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt;) and the library falls back to local files on its own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With the default cache, or with network access, nothing happens. That is why the bug stays invisible in development and shows up on deployment day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cause: two steps, two folders
&lt;/h2&gt;

&lt;p&gt;Offline, loading the tokenizer chains two functions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;list_local_hf_repo_files&lt;/code&gt; lists the cached files to find the tokenizer (&lt;code&gt;tekken.json&lt;/code&gt;). It &lt;strong&gt;always&lt;/strong&gt; looked in the default cache, &lt;code&gt;HF_HUB_CACHE&lt;/code&gt;, and had no &lt;code&gt;cache_dir&lt;/code&gt; parameter.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;hf_hub_download&lt;/code&gt; then fetches the file. It does receive &lt;code&gt;cache_dir&lt;/code&gt; and looks in the custom folder.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before: the listing ignores the folder chosen by the user
&lt;/span&gt;&lt;span class="n"&gt;repo_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;huggingface_hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HF_HUB_CACHE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the tokenizer only lives in &lt;code&gt;cache_dir&lt;/code&gt;, the listing comes back empty and the function gives up before even trying to read the file. The reverse case fails too, one step later.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: 5 lines, the same rule as Hugging Face
&lt;/h2&gt;

&lt;p&gt;Both steps now read the same folder: &lt;code&gt;cache_dir&lt;/code&gt; if given, otherwise the default cache, exactly the rule &lt;code&gt;hf_hub_download&lt;/code&gt; follows.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cache_root&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cache_dir&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cache_dir&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;huggingface_hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HF_HUB_CACHE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;repo_cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache_root&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;huggingface_hub&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constants&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REPO_ID_SEPARATOR&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;models&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;repo_id&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Backward compatible&lt;/strong&gt;: a new optional parameter, unchanged behavior without it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 regression tests&lt;/strong&gt;, 5 of which fail without the fix: folder given as a string or a &lt;code&gt;Path&lt;/code&gt;, full offline load, fallback after a network error, &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimal scope&lt;/strong&gt;: no new feature slipped into the fix.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The maintainer asked for a single change and applied it himself: removing a code comment. The code itself did not change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Until you can upgrade
&lt;/h2&gt;

&lt;p&gt;If you are affected and cannot upgrade mistral-common yet: point &lt;code&gt;HF_HUB_CACHE&lt;/code&gt; at the same folder as your &lt;code&gt;cache_dir&lt;/code&gt;. Both steps then read the same place.&lt;/p&gt;

&lt;p&gt;From reading their source code, Transformers (&lt;code&gt;cache_dir&lt;/code&gt;) and vLLM (&lt;code&gt;--download-dir&lt;/code&gt; with &lt;code&gt;tokenizer_mode=mistral&lt;/code&gt;) pass that folder down to mistral-common, so they are probably affected offline too. I did not test that end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for teams shipping agents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test the real deployment, not the developer laptop.&lt;/strong&gt; This bug only exists offline with a custom model folder: exactly the production setup, never the laptop one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cut the network in your tests.&lt;/strong&gt; &lt;code&gt;HF_HUB_OFFLINE=1&lt;/code&gt; in one integration test would have caught it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An option that is not passed everywhere is a bug.&lt;/strong&gt; When a function accepts &lt;code&gt;cache_dir&lt;/code&gt;, check that every internal call receives it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute back.&lt;/strong&gt; Five lines and six tests is the price of a fix that protects everyone deploying Mistral offline.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Deploying Mistral models on an air-gapped network? Which other traps have you hit?&lt;/p&gt;

&lt;p&gt;Let's talk, or get your own agents audited: &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://www.malt.fr/profile/zakariakhchiche" rel="noopener noreferrer"&gt;Malt&lt;/a&gt; · &lt;a href="https://medium.com/@ZKHCHICHE" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; · &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Want your team to build agents like these? I run a hands-on Copilot Studio training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-copilot-studio/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/formation-copilot-studio/&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/kit-ai-act/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>mistral</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Three silent bugs in Mistral AI's Python libraries</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Sun, 04 Oct 2026 09:43:15 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/three-silent-bugs-in-mistral-ais-python-libraries-1gf</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/three-silent-bugs-in-mistral-ais-python-libraries-1gf</guid>
      <description>&lt;p&gt;A tool schema that looks right. A validator that says "valid". A structured output that parses. In each case the code runs without an error, and the result is still wrong.&lt;/p&gt;

&lt;p&gt;I audited the two Python libraries at the heart of Mistral AI's stack and found three bugs of that kind. All three are reproduced, fixed with regression tests and submitted upstream.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: the two libraries
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/mistralai/mistral-common" rel="noopener noreferrer"&gt;mistral-common&lt;/a&gt; validates chat requests and turns them into tokens. vLLM and Hugging Face Transformers rely on it to serve Mistral models.&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/mistralai/client-python" rel="noopener noreferrer"&gt;client-python&lt;/a&gt; is the official &lt;code&gt;mistralai&lt;/code&gt; SDK. Its &lt;code&gt;extra&lt;/code&gt; package turns Python functions into tools and Pydantic models into structured outputs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agents built on Mistral go through both on every call. A silent bug there spreads to every agent built on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: optional tool parameters become mandatory
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;create_tool_call&lt;/code&gt; turns a Python function into a tool the model can call. With the idiomatic Pydantic style, a default value is lost:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;City name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
    &lt;span class="n"&gt;unit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Annotated&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celsius or fahrenheit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;celsius&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="nf"&gt;create_tool_call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;get_weather&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# ['city', 'unit']   expected: ['city']
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default lives in the function signature, not in the &lt;code&gt;Field&lt;/code&gt; taken from &lt;code&gt;Annotated&lt;/code&gt;, so it never reaches the schema. Tools are sent with &lt;code&gt;strict=True&lt;/code&gt;: the model is forced to fill &lt;code&gt;unit&lt;/code&gt; every time, and the developer's default is never used.&lt;/p&gt;

&lt;p&gt;Reported in &lt;a href="https://github.com/mistralai/client-python/issues/632" rel="noopener noreferrer"&gt;issue #632&lt;/a&gt;, with the fix and two tests ready on my fork.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: a dict field that can never hold a key
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;chat.parse&lt;/code&gt; lets you ask for an answer that matches a Pydantic model. Before sending it, the SDK makes the schema strict by setting &lt;code&gt;additionalProperties: false&lt;/code&gt; on every object.&lt;/p&gt;

&lt;p&gt;The problem: for a &lt;code&gt;dict[str, int]&lt;/code&gt; field, Pydantic already uses &lt;code&gt;additionalProperties&lt;/code&gt; to describe the values. The SDK overwrites it.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;code&gt;stock: dict[str, int]&lt;/code&gt;&lt;/th&gt;
&lt;th&gt;Schema&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pydantic&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"additionalProperties": {"type": "integer"}}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sent to the API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;{"additionalProperties": false}&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With the second schema, &lt;code&gt;{"apples": 3}&lt;/code&gt; is invalid: the field can only be empty. The fix only adds the key when it is missing, which is what the OpenAI SDK already does.&lt;/p&gt;

&lt;p&gt;Reported in &lt;a href="https://github.com/mistralai/client-python/issues/633" rel="noopener noreferrer"&gt;issue #633&lt;/a&gt;, with the fix and a test. I could only check the client side: I did not test the API end to end.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: a newline slips past the validator
&lt;/h2&gt;

&lt;p&gt;mistral-common checks that function names match &lt;code&gt;^[a-zA-Z0-9_-]{1,64}$&lt;/code&gt; and that tool call ids have 9 alphanumeric characters. The check uses &lt;code&gt;re.match&lt;/code&gt;, and in Python &lt;code&gt;$&lt;/code&gt; also matches right before a final &lt;code&gt;\n&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;"get_weather\n"&lt;/code&gt; and &lt;code&gt;"abcDEF123\n"&lt;/code&gt; (10 characters) are accepted, and the invalid name ends up in the tokenized prompt. The fix is one word per call site: &lt;code&gt;fullmatch&lt;/code&gt; instead of &lt;code&gt;match&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Fixed in &lt;a href="https://github.com/mistralai/mistral-common/pull/354" rel="noopener noreferrer"&gt;pull request #354&lt;/a&gt; (&lt;a href="https://github.com/mistralai/mistral-common/issues/353" rel="noopener noreferrer"&gt;issue #353&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for teams shipping agents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check what you send, not just what you write.&lt;/strong&gt; Print the schema your SDK actually sends to the model at least once. Bugs 1 and 2 are invisible in the Python code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Strict mode raises the stakes.&lt;/strong&gt; A wrong schema in strict mode is not ignored: the model must follow it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;re.match&lt;/code&gt; + &lt;code&gt;$&lt;/code&gt; is not a full match.&lt;/strong&gt; Use &lt;code&gt;re.fullmatch&lt;/code&gt; for validation, in your code too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute back.&lt;/strong&gt; Each fix is a few lines. Sending it upstream protects everyone on the same stack.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Building agents on Mistral? I'd like to hear which surprises you've hit.&lt;/p&gt;

&lt;p&gt;Let's talk, or get your own agents audited: &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://www.malt.fr/profile/zakariakhchiche" rel="noopener noreferrer"&gt;Malt&lt;/a&gt; · &lt;a href="https://medium.com/@ZKHCHICHE" rel="noopener noreferrer"&gt;Medium&lt;/a&gt; · &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Want your team to build agents like these? I run a hands-on Copilot Studio training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-copilot-studio/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/formation-copilot-studio/&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;https://zakariakhchiche.github.io/kit-ai-act/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>opensource</category>
      <category>llm</category>
    </item>
    <item>
      <title>Copilot Studio over thousands of documents: answer only when a source supports it</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Sat, 03 Oct 2026 18:10:17 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/copilot-studio-over-thousands-of-documents-answer-only-when-a-source-supports-it-2p45</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/copilot-studio-over-thousands-of-documents-answer-only-when-a-source-supports-it-2p45</guid>
      <description>&lt;p&gt;A Copilot Studio agent connected to a large document library almost always answers. The problem is when it shouldn't: the retrieved procedure is for another equipment model, the cited revision is superseded, or no document covers the question. I built an MCP server that adds a decision step between search and the answer. For each retrieved passage, Jev, TypeSafe's "System One" model, answers five yes/no questions, and code decides: evidence, conflict, or dropped. The agent answers with citations, flags a false premise, or says it found nothing reliable.&lt;/p&gt;

&lt;p&gt;Code: &lt;a href="https://github.com/Zakariakhchiche/copilot-studio-jev" rel="noopener noreferrer"&gt;github.com/Zakariakhchiche/copilot-studio-jev&lt;/a&gt;. I also proposed it as a sample to Microsoft's official repo: &lt;a href="https://github.com/microsoft/CopilotStudioSamples/pull/539" rel="noopener noreferrer"&gt;microsoft/CopilotStudioSamples#539&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;How it works:&lt;/strong&gt; Azure AI Search proposes about a dozen passages; Jev returns calibrated probabilities for each; thresholds in code decide what reaches the agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three outcomes:&lt;/strong&gt; cited answer, false premise flagged, or abstention. Never an answer "from memory".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measured on the demo corpus:&lt;/strong&gt; 4 of 4 questions handled as expected, about 0.5 to 0.7 s per question for 10 to 12 passages, roughly $0.001 of Jev for the whole test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two lessons from the live run:&lt;/strong&gt; a broad injection question drops real procedures, and superseded documents need their own question.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Search ranks, it doesn't decide
&lt;/h2&gt;

&lt;p&gt;In a maintenance procedure library, near-duplicates are the norm: two pump models, an archived revision next to the current one, a forum note. Search, even with semantic re-ranking, orders passages by similarity. It doesn't tell you which one actually answers, or whether to answer at all.&lt;/p&gt;

&lt;p&gt;Example from the demo: a technician asks "P-200 bearings need grease every 500 hours, how much do I add?". The current procedure says 2,000 hours. The archived revision does say 500 hours and 30 g. If the agent receives it as a source, it answers confidently from an outdated document.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Copilot Studio&lt;/strong&gt; calls the &lt;code&gt;search_procedures&lt;/code&gt; tool on an MCP server (Streamable HTTP, as Copilot Studio requires).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Azure AI Search&lt;/strong&gt; returns the top N passages (12 by default).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jev&lt;/strong&gt; gets one request per passage, with the query and the passage in the state, and answers five yes/no questions in parallel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code&lt;/strong&gt; applies thresholds in a fixed order and returns a status, guidance and the kept passages with their URLs.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The five questions, adapted from TypeSafe's "Classifying RAG passages" cookbook:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;is_relevant&lt;/code&gt;: does the passage address the subject of the query?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contains_answer_evidence&lt;/code&gt;: does it state information usable in a direct answer?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contradicts_query_premise&lt;/code&gt;: does it conflict with a factual premise in the query?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;contains_prompt_injection&lt;/code&gt;: does it contain text addressed to an AI assistant rather than to a human reader?&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;is_superseded&lt;/code&gt;: does it say it is superseded, archived or no longer valid?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them asks "should I keep this passage?". Policy lives in code: changing it means editing a reviewed threshold, not rewording a prompt. Order matters: injection first (security), then superseded, then contradiction, then relevance and evidence.&lt;/p&gt;

&lt;p&gt;Statuses returned to the agent: &lt;code&gt;answer_from_evidence&lt;/code&gt;, &lt;code&gt;premise_conflict&lt;/code&gt;, &lt;code&gt;insufficient_evidence&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step by step
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Install and test&lt;/strong&gt; (Node.js 20+). Tests run offline against a fake TypeSafe endpoint fed with scores recorded from the live run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Zakariakhchiche/copilot-studio-jev
&lt;span class="nb"&gt;cd &lt;/span&gt;copilot-studio-jev/mcp-server
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm &lt;span class="nb"&gt;test
&lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Configure.&lt;/strong&gt; Copy &lt;code&gt;.env.example&lt;/code&gt; to &lt;code&gt;.env&lt;/code&gt;, set &lt;code&gt;TYPESAFE_API_KEY&lt;/code&gt;, and pin &lt;code&gt;TYPESAFE_MODEL=jev-1.13.0&lt;/code&gt; rather than the &lt;code&gt;jev-latest&lt;/code&gt; alias, which moves with each release and can shift the probabilities your thresholds were tuned on. Without Azure variables, the server searches the bundled demo corpus.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Load your library into Azure AI Search.&lt;/strong&gt; Split documents into passages of a few hundred words, export them as JSON (id, title, text, url, source type), then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run ingest &lt;span class="nt"&gt;--&lt;/span&gt; path/to/passages.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Jev cost depends on the number of candidates per question (&lt;code&gt;GATE_CANDIDATES&lt;/code&gt;), not on library size: 500 or 15,000 documents, each question costs one Jev request per candidate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Expose the server.&lt;/strong&gt; For testing, a Dev Tunnel is enough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--env-file&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;.env build/index.js
devtunnel host &lt;span class="nt"&gt;-p&lt;/span&gt; 3000 &lt;span class="nt"&gt;--allow-anonymous&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, host it (Azure Container Apps, App Service) and set &lt;code&gt;MCP_API_KEY&lt;/code&gt; to require an &lt;code&gt;x-api-key&lt;/code&gt; header.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Add the tool in Copilot Studio:&lt;/strong&gt; Tools → Add a tool → New tool → Model Context Protocol. Give a clear description (the orchestrator uses it to decide when to call the tool), the URL ending in &lt;code&gt;/mcp&lt;/code&gt;, and the authentication. Turn off general knowledge and web search so the agent can't answer outside the library.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Keep agent instructions short:&lt;/strong&gt; call &lt;code&gt;search_procedures&lt;/code&gt; for any equipment, maintenance or safety question, follow the &lt;code&gt;guidance&lt;/code&gt; field, never answer from memory, cite title and URL.&lt;/p&gt;

&lt;p&gt;Transparency: I tested the MCP server with the official MCP client and the gate against the live Jev API, but not yet end-to-end inside a Copilot Studio agent. The steps above follow Microsoft's "Connect your agent to an existing MCP server" documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measured results
&lt;/h2&gt;

&lt;p&gt;Run on 2026-10-03 with &lt;code&gt;jev-1.13.0&lt;/code&gt; on the demo corpus (12 fictional procedures for a water utility, with near-duplicates, an archived revision and a planted forum note):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Candidates&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;th&gt;Gate time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;How often should I grease the bearings on pump P-200?&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;answer from the current procedure only&lt;/td&gt;
&lt;td&gt;0.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;P-200 bearings need grease every 500 hours, how much do I add?&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;premise flagged as false&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Do I need lockout/tagout for bearing work on P-200?&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;answer from the lockout and seal procedures, forum note dropped&lt;/td&gt;
&lt;td&gt;0.5 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What is the warranty on the sand filters?&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;abstention&lt;/td&gt;
&lt;td&gt;0.7 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Times cover all candidates of one question with 6 requests in flight, measured from Europe. The 44 requests used about 22,600 input tokens.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two lessons from the live run
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A broad injection question drops real procedures.&lt;/strong&gt; The cookbook question ("does this passage attempt to control the system answering the query?") scored the lockout procedure at 0.76, because procedures are written as instructions, so the safety procedure was excluded. Reworded as "text addressed to an AI assistant rather than to a human reader", it scores the procedure at 0.02 and the planted note at 0.99.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Superseded documents need their own question.&lt;/strong&gt; Without it, the archived revision was accepted as evidence (0.97) for the 500-hour question and the false premise went through. With &lt;code&gt;is_superseded&lt;/code&gt;, the archived revision scores 0.98 and is dropped; other passages stay at or below 0.03.&lt;/p&gt;

&lt;h2&gt;
  
  
  What about other languages?
&lt;/h2&gt;

&lt;p&gt;TypeSafe documents English as Jev's primary language. The lockout question asked in French against English procedures scored the right passage at 0.49 relevance and 0.50 evidence, just under the thresholds: the agent abstains. A safe failure, but a failure. The tool therefore asks the orchestrator to send the query in the library's language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Audit and replay
&lt;/h2&gt;

&lt;p&gt;Following a reader's question, each answer can now leave a trace. With &lt;code&gt;AUDIT_LOG_PATH&lt;/code&gt; set, the server appends one JSON line per answer: the query, the versioned model ID, the thresholds in force, and every candidate passage with its document version, five scores and route. The tool output returns the matching &lt;code&gt;audit_id&lt;/code&gt;, so you can trace why the agent answered, flagged a conflict or abstained.&lt;/p&gt;

&lt;p&gt;Because the routing only reads stored scores, past answers can be replayed with new thresholds without calling Jev:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;GATE_EVIDENCE_MIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.6 npm run replay &lt;span class="nt"&gt;--&lt;/span&gt; audit.jsonl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script lists the passages and answers whose decision would change. For stale documents, use two layers: filter archived revisions out at retrieval with index metadata (a version or status field), and keep &lt;code&gt;is_superseded&lt;/code&gt; as the safety net. The records contain user questions: in production, send them to your logging pipeline with the retention your organisation requires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The injection question is a filter, not a security boundary.&lt;/li&gt;
&lt;li&gt;Retrieval recall still matters: if the right passage isn't in the top N, the agent abstains.&lt;/li&gt;
&lt;li&gt;Thresholds are starting points: tune them on real user questions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Related:&lt;/strong&gt; &lt;a href="https://dev.to/zakaria_khchiche_490919ed/locating-a-signature-in-a-scanned-form-with-jev-114-typesafe-and-microsoft-agent-framework-5214"&gt;Locating a signature in a scanned form with Jev 1.14 (TypeSafe) and Microsoft Agent Framework&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Zakaria Khchiche is a freelance Data &amp;amp; AI Tech Lead in Paris. He builds and runs AI agents in production for large companies and contributes to open-source agent frameworks. &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://zakariakhchiche.github.io/" rel="noopener noreferrer"&gt;Website&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Want your team to build agents like these? I run a hands-on Copilot Studio training (in French, with Spar-x, Qualiopi-certified, eligible for OPCO funding in France): &lt;a href="https://zakariakhchiche.github.io/formation-copilot-studio/" rel="noopener noreferrer"&gt;training program&lt;/a&gt; and a free AI Act article 4 kit: &lt;a href="https://zakariakhchiche.github.io/kit-ai-act/" rel="noopener noreferrer"&gt;AI Act kit&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>copilotstudio</category>
      <category>mcp</category>
      <category>rag</category>
      <category>azure</category>
    </item>
    <item>
      <title>Three silent bugs in an open-source AI agent framework</title>
      <dc:creator>ZAKARIA KHCHICHE</dc:creator>
      <pubDate>Thu, 01 Oct 2026 10:23:24 +0000</pubDate>
      <link>https://dev.to/zakaria_khchiche_490919ed/three-silent-bugs-in-an-open-source-ai-agent-framework-igd</link>
      <guid>https://dev.to/zakaria_khchiche_490919ed/three-silent-bugs-in-an-open-source-ai-agent-framework-igd</guid>
      <description>&lt;p&gt;A bug that crashes gets noticed right away. A bug that returns a plausible but wrong result can run in production for months without anyone noticing.&lt;/p&gt;

&lt;p&gt;While auditing the code of OGX, an open-source framework for building AI agents, I found three of them. None raises an error. Each one quietly skews a result: search relevance, a document filter, or a token bill.&lt;/p&gt;

&lt;h2&gt;
  
  
  Context: OGX, formerly Llama Stack
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ogx-ai/ogx" rel="noopener noreferrer"&gt;OGX&lt;/a&gt; is the continuation of Meta's Llama Stack project. It provides the standard building blocks of an agentic application: inference, an OpenAI-compatible Responses API, and vector stores for RAG (Elasticsearch, pgvector, Qdrant…).&lt;/p&gt;

&lt;p&gt;Teams that build agents on this kind of foundation trust those blocks without rereading them. That is exactly where an audit pays off: looking for what "almost" works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: one extra letter disables a search setting
&lt;/h2&gt;

&lt;p&gt;Elasticsearch hybrid search merges a keyword search and a vector search (RRF). The &lt;code&gt;rank_window_size&lt;/code&gt; parameter sets how many candidates enter that merge.&lt;/p&gt;

&lt;p&gt;In the code, the list of allowed parameters contained &lt;code&gt;rank_windows_size&lt;/code&gt;, with an extra "s". The correct parameter was treated as unknown and silently dropped.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before
&lt;/span&gt;&lt;span class="n"&gt;allowed_rrf_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank_constant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank_windows_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# after
&lt;/span&gt;&lt;span class="n"&gt;allowed_rrf_params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank_constant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rank_window_size&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filter&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requested&lt;/th&gt;
&lt;th&gt;Actually sent (before)&lt;/th&gt;
&lt;th&gt;After fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;rank_window_size = 50&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;10 (default)&lt;/td&gt;
&lt;td&gt;50&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A one-character fix, submitted in &lt;a href="https://github.com/ogx-ai/ogx/pull/6697" rel="noopener noreferrer"&gt;pull request #6697&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: pgvector filters that never match booleans
&lt;/h2&gt;

&lt;p&gt;With pgvector you can filter documents on their metadata, for example "published = true". For "in list" (IN) and "not in list" (NOT IN) filters, the code converted each value to Python text: &lt;code&gt;True&lt;/code&gt; became &lt;code&gt;"True"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;PostgreSQL extracts the JSON metadata as &lt;code&gt;"true"&lt;/code&gt;. The two never match. Same problem for a number stored as &lt;code&gt;2023.0&lt;/code&gt; filtered on &lt;code&gt;2023&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Tested on a real PostgreSQL 16 database with two documents:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Filter&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;published in [true]&lt;/td&gt;
&lt;td&gt;no result&lt;/td&gt;
&lt;td&gt;document 0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;published not in [true]&lt;/td&gt;
&lt;td&gt;documents 0 and 1&lt;/td&gt;
&lt;td&gt;document 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;year in [2023]&lt;/td&gt;
&lt;td&gt;no result&lt;/td&gt;
&lt;td&gt;document 1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The worst case is the exclusion filter: it lets through documents you meant to exclude. In a RAG pipeline, that means unpublished or outdated sources injected into the model's context. The fix applies to lists the same typed casts already used for single-value comparisons.&lt;/p&gt;

&lt;p&gt;Fix submitted in &lt;a href="https://github.com/ogx-ai/ogx/pull/6698" rel="noopener noreferrer"&gt;pull request #6698&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: a token counter that forgets earlier calls
&lt;/h2&gt;

&lt;p&gt;An agent often makes several model calls for a single response, for example when it uses tools. The Responses API correctly sums input and output tokens across those calls.&lt;/p&gt;

&lt;p&gt;But two detailed counters were overwritten instead of summed: cached tokens and reasoning tokens. Only the last call counted.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Example over 2 calls&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Cached tokens (80 + 120)&lt;/td&gt;
&lt;td&gt;120&lt;/td&gt;
&lt;td&gt;200&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reasoning tokens (10 + 15)&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are exactly the two figures used for cost tracking. Caching lowers the bill, reasoning raises it. A dashboard built on these values under-estimated both.&lt;/p&gt;

&lt;p&gt;Reported in &lt;a href="https://github.com/ogx-ai/ogx/issues/6699" rel="noopener noreferrer"&gt;issue #6699&lt;/a&gt;, with a proposed fix (&lt;a href="https://github.com/Zakariakhchiche/ogx/commit/e8177d053f2a72a81909a5b9d9c21ede5ba895ee" rel="noopener noreferrer"&gt;commit e8177d0&lt;/a&gt;).&lt;/p&gt;

&lt;h2&gt;
  
  
  What this means for teams shipping agents
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Test what is silent.&lt;/strong&gt; An ignored parameter or a filter that doesn't filter triggers no alert. You need a test that checks the value actually sent, not just the absence of errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test with real types.&lt;/strong&gt; Bugs 2 and 3 only show up with booleans, floats or chained calls. "Strings and a single call" test suites miss them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't blindly trust the foundation.&lt;/strong&gt; A popular open-source framework is still code written fast. Reviewing the critical blocks (filtering, costs) before production takes a few hours.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contribute back.&lt;/strong&gt; Each fix here is a few lines. Submitting it upstream helps everyone who builds on the same foundation.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Building agents on OGX or another framework? I'd love to hear which silent bugs you've run into.&lt;/p&gt;

&lt;p&gt;Let's talk, or get your own agents audited: &lt;a href="https://www.linkedin.com/in/zakariakhchiche/" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt; · &lt;a href="https://www.malt.fr/profile/zakariakhchiche" rel="noopener noreferrer"&gt;Malt&lt;/a&gt; · &lt;a href="https://medium.com/@ZKHCHICHE" rel="noopener noreferrer"&gt;Medium&lt;/a&gt;&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>bug</category>
      <category>security</category>
    </item>
  </channel>
</rss>
