[ADD] ai_connection#77
Conversation
angelmoya
left a comment
There was a problem hiding this comment.
I've been independently developing an ai_connection module with session management, call history, skill integration, and agent support. I discovered this PR while working on it and I'm now adapting my modules to extend from this base rather than duplicating effort.
As I started building ai_connection_session (persistent call records with token tracking) and ai_connection_agent (agent loop on top of connections) on top of this module, I identified several points where the current interface makes it difficult for downstream modules to hook into the execution flow without overriding the entire method chain.
The review above focuses on making the core extensible — returning structured data from _run_ai() so callers can persist it, enriching the client.py contract with standard fields like usage and model, and a few guardrails (max_iterations, error handling) that become critical once you add persistence.
None of these changes break backward compatibility — they extend the existing contracts so that the growing ecosystem of AI modules (session, agent, skill, debug) can compose cleanly on top of this foundation.
c5ca8ad to
b9f1d01
Compare
|
Hi @etobella , thanks for the force-push cleanup — the _process_tool_call / _process_tool_call_result extraction and the max_iterations guard are great additions. About the tool error handling: I understand you have doubts, let me explain the rationale so we can reach the right approach together. The problem in the current code:When tool._execute_tool() raises (e.g. a UserError, a ValueError, a missing record), the exception propagates up through _run_ai() → _run(). The entire conversation state — messages accumulated across iterations — is lost. From the LLM's perspective, it called a tool and got no response at all, which is not a recoverable protocol state. Why catching and returning the error matters:LLMs are designed to handle tool results. If we return {"role": "tool", "content": "Error: record not found"} instead of crashing:
Where I think it fits:_process_tool_call is the natural place — wrapping the tool._execute_tool() call in a try/except, and if an exception occurs, returning the error message as a tool result message. This keeps _run_ai clean and provider-agnostic: What specific part are you unsure about? Is it the broad Exception scope, the format of the error message, or a concern about hiding real bugs? Happy to refine the approach. |
|
Another thing I noticed is that there's no streaming support. The current The OpenAI/Ollama APIs already support this (SSE with Not a blocker for merge — the current interface is fine for the MVP and |
|
I've thought about this before your comments and there I took a decission about it. The core issue is that Odoo's Another thing, I saw that you proposed a lot of migrations, but I was refactoring most of the modules with the Plan, I will check if they fit what I have in mind, otherwise, I might superseed your changes 😉 You were too fast 😆 |
|
Hi! Perfect, if you already have plans and refactoring in place for those modules, go ahead and push them. The main reason I opened these was precisely because I realized we were working on the exact same things in parallel, and I wanted to sync up. Regarding the work I’ve been doing on top of this: besides the skills system we discussed, I am currently focusing on making the messages multimodal, for multimodal support (images, audio, documents), we'll need a provider-agnostic way to pass files through the connection. Something like: def _run(self, prompt, tools=None, record=None, system_prompt=None,
messages=None, max_iterations=None, files=None):
...
if prompt:
msg = {"role": "user", "content": prompt}
if files:
msg["files"] = files
messages.append(msg)Where each file is a generic dict: {
"type": "image"|"audio"|"document"|"text",
"mimetype": "image/jpeg",
"data": "<base64>",
"filename": "photo.jpg"
}Each provider's What do you think? Would you handle it differently for passing files generically to the LLM? I tested this, and works fine. |
|
@angelmoya type has no sense, as this depends on each AI system, the transformation should be done by mimetype on each client. |
|
Hi @etobella , Exactly, that's my point. When another module calls ai_connection, it doesn't know (and shouldn't care) which specific provider or submodule is going to handle the request underneath. That is why we need to define a standardized payload structure at the ai_connection level. This ensures any client module can pass messages and files in a unified way. Then, each provider-specific submodule will be responsible for extracting those files, reading the mimetype, and transforming them into whatever native format that specific AI client requires. However, regarding the "type" field, while it's true that we can use the "mimetype" to identify the file extension, having a high-level "type" (like "image", "audio", "document") standardizes the abstraction layer. It avoids forcing every single provider submodule to parse the mimetype string just to understand the general category of the file, which most LLMs require at their root level. For instance: Our own tools/modules flow: If we develop a tool that allows the LLM to load files from a document's attachments, the tool will read the attachment, call ai_connection passing this standard file structure, and then ai_connection will route it to the active submodule (e.g., OpenAI, Ollama) so it can perform the correct native transformation. OpenAI / Anthropic (Claude): Both use a block structure where you must explicitly declare the high-level type at the root: Gemini: Separates the structural part from the data block: Ollama: Expects a clean, separated array of images at the message level: Keeping a simple, high-level "type" abstraction alongside the "mimetype" makes it incredibly easy for any future submodule to quickly route the file to the correct internal handler, avoiding redundant boilerplate code to parse strings like image/png, image/webp, audio/mp3, etc., in every single driver. Let me know if you see the value in keeping it as a high-level helper for the submodules! I hope I’m explaining myself well, but if you have any questions or want to clear things up, I’m totally available to hop on a quick call and sync up whenever you want. |
|
according to your information, it is easily handled. I would not add the code here. |
|
Hi @etobella My bad! I hadn't realized that they were already being passed as attachments in the code, so you're totally right, the high-level type is not a blocker at all. Let's leave it as it is. However, unless I've misunderstood something, I think the question from my previous message about what happens when a tool call fails remains unanswered. If we can clear that up, it's the very last thing left! Sorry for being a bit insistent, and thanks a lot for your patience and effort on this. We are almost there! 🚀 |
Just sharing some through about streaming, because even the odoo server do not stream the result to the end user there are path where streaming the IA result is interesting such as
I haven't check yet the implementation if possible or not just reacting to the conversation |
|
Hi @petrus-v Exactly! Those are very solid points. Avoiding timeouts on long reasoning tasks and reducing memory footprint are crucial architectural reasons to consider streaming, even if Odoo's standard layout isn't fully built for it. To give some context on what I'm doing: I am currently testing a basic implementation of stream=True. The immediate goal is just to validate the module's core flow this way. It works fine for straightforward questions that don't involve complex agent loops, tool calls, or heavy reasoning. However, as Enric pointed out, Odoo's standard bus.bus notifications won't work for real-time text chunking because they are bound to the database transaction (cr.commit()). If we want to support full real-time streaming to the UI in the future, the right approach would be:
This way, the browser can feed the UI in real time without forcing premature database commits. For now, keeping it simple as an MVP is perfectly fine, but it’s great to keep this architecture in mind for a follow-up! |
|
Hi @etobella , thanks for the great foundation you laid out here! I have created an alternative PR #86 based on your work. After giving While the new PR doesn't implement a strict "real-time" streaming transport out of the This approach allows us to naturally isolate and commit each AI iteration (or tool call) I would love for you to take a look at #86 and hear your thoughts. Let |
|
@angelmoya _on_stream_batch will not work if you don't use commit and that should be avoided... |
|
Hi @etobella and @ValentinVinagre The PR I previously uploaded didn't have the final code; I pushed the wrong version by mistake. The main idea is that each iteration inside the while loop now runs in a separate process using _trigger, which calls the cron. Once a step finishes, it triggers the cron again to execute the next iteration of the loop. So, instead of a while loop, we now have a recursive or recurrent call. Since it executes via trigger, Odoo automatically commits after each iteration before continuing. It is not exactly streaming, but it will help us get feedback for every single thinking iteration. |
|
it will take at least 3 minutes if if has a message, a tool calling and a final mesaage. that doesn't look like a good idea. crons are not executed automatically. also, it could be longer if there is other cron activities... doean't seem like the best plan |
|
@etobella I understand your concern, but that would only be true if we were waiting for the standard cron schedule. Using _trigger() changes the behavior completely. Calling _trigger() acts exactly like a native, lightweight queue system (very similar to how queue_job works, but built into the core). It completely bypasses the cron's scheduled interval. When we call it, Odoo immediately inserts a record into ir.cron.trigger and instantly wakes up an available background worker to execute the job right away. The delay between iterations is not minutes; it is just milliseconds (the time it takes for the worker to pick up the trigger). |
|
The PR #87 is now fixed and updated with the correct code. Here is the method that does the "magic". This method is executed by a cron, and it essentially re-triggers itself recurrently as long as the execution state is def _cron_process_single_step(self):
"""
Executed by the cron. Finds one running execution and processes its next step.
If we passed an ai_execution_id in context, we prioritize it.
"""
domain = [("state", "=", "running")]
ai_execution_id = self.env.context.get("ai_execution_id")
if ai_execution_id:
domain.append(("id", "=", ai_execution_id))
execution = self.search(domain, limit=1)
if not execution:
return
execution._execute_step()
if execution.state == "running":
cron = self.env.ref(
"ai_connection.cron_ai_step_processor", raise_if_not_found=False
)
if cron:
cron.with_context(ai_execution_id=execution.id)._trigger()You can see the exact implementation here: https://github.com/angelmoya/ai/blob/ef2c82a19bd87d465bde33b1055d56c88e10271c/ai_connection/models/ai_connection_execution.py#L263 |
|
@angelmoya your proposal has a major problem. IF there is 2 AI processes launched together, they will be executed one after the other, not in parallel. No benefits of workers. Scales poorly.... For me, it's a no. I understand your needs and we can keep hooks if you need it, however, but you are making complex guesses just for your specific need. I understand and accept what you want to achieve but I don't think that this is needed in the base module. Also, I would suggest to use queue_job and queue_job to solve the problem explained and queue_job will not be a dependency for this module. As you can see in other modules, we are removing this dependency if possible and leave it in a secondary related module (like edi-framework) Also, why did you directly superseded my PR when I was answering your questions and comments? It is not the usual OCA way, at least a comment before the supersed would have been nice... The right way would have been doing a commit a PR on my branch directly. |
|
Hi @etobella, First of all, thank you for your patience and for thoroughly reviewing almost every point in this PR. I really appreciate the detailed feedback. However, I noticed that the point regarding the I would like to share my perspective on these design choices, as I believe they are crucial for the robustness of the core
Finally, I wanted to clarify why I opened this PR instead of pushing directly to your branch. This is a significant structural design change, and I wanted to open the floor so more community members could review it and share their perspectives. In any case, even though I already had my own module fully developed for this, I chose to adapt my logic on top of your work specifically to preserve your commits and respect your authorship. Thanks again for the constructive debate! |
|
In any case, please note that my PR is fully compatible with accepting your original proposal first. We can merge your work to unblock it, and then continue debating and fine-tuning these additions (the try/except block, UX feedback, and cron adjustments) right after. |
|
did you read my comments or did you let claude answer following your ideas without rereading it? Just to be clear:
|
|
Hi Enric, I do read your comments, but I admit that sometimes I find it difficult to understand exactly what you mean, and I get the impression that the way Odoo's standard cron In your comment, you mentioned: "IF there is 2 AI processes launched together, they will be executed one after the other, not in parallel. No benefits of workers. Scales poorly...". This concern would make perfect sense if we were talking about a standard cron running conventionally, but that is not the case when using Perhaps we are facing a language barrier, or I might be mistaken, or you might simply not be taking into account how it works, but since Odoo 14, this standard method does not function like a traditional synchronous cron. It modifies If I am misunderstanding your point, or if we are looking at this standard cron mechanism differently, I would love to clarify it from this technical perspective. I am completely open to being proven wrong if that is the case; I just want to make sure we are aligning on the actual technical behavior of the core. Finally, regarding your comment about the "rudeness of the supersede", here I truly get the impression that you are the one not reading my comments. I believe I have already clearly explained my point of view: I opened this PR because I believe a structural change like this needs to be openly reviewed and debated by more community members. In any case, I took the time to adapt my logic on top of yours precisely to keep all your work and preserve your commits, fully respecting your authorship. There is no rudeness in debating open-source architecture this way. |
|
Hi @etobella, @ValentinVinagre and @angelmoya, We all agree that ai_connection is the cornerstone of the roadmap (#73), and our common goal is quite clear. So, what do we need to do to minimize effort, align with the OCA methodology, and avoid getting blocked by procedural friction or misunderstandings? On a technical level, your contributions are highly complementary. I propose combining both approaches:
Would you be open to a quick call (20 mins) this week? The goal is strictly pragmatic: to agree on how to integrate these improvements via commits to the original branch to respect the OCA workflow and get this PR ready to merge. Regards. |
|
One last technical question to wrap this up, @etobella In the official SII module ( As we know, creating a record in Could you please confirm if your argument that "IF there is 2 xxx processes launched together, they will be executed one after the other, not in parallel. No benefits of workers. Scales poorly..."" applies to the SII design as well? Because if this architecture is valid for that workflow, I see no reason why it shouldn't be the right approach to handle AI workflows here. |
|
the trigger is launched and processes one ai job after the other. if five users launch at the same time a process, they will be launched one after the other. Odoo only lets the same job to be launched once and cannot be launched again until it is over. For SII there is not a problem to wait a few minutes. Here it is a problem |
|
You are right about the cron behavior. I admit I had misunderstood how That being said, I think this debate is highly positive for building something better together. The same misunderstanding happened to me on other points where I didn't fully grasp your answer at first, such as the file type issue (where I simply missed the change already applied in the code). My main goal is for the AI vertical to have a solid framework that goes beyond just throwing simple queries, and I would love to build that as a community effort. Because of this, your comment implying that I do not read your messages and that I am just letting Claude reply for me was completely uncalled for. Regarding the supersede, I did it precisely because you previously implied that you would implement those changes within my pending PRs. When you mentioned it back then, I didn't see it as an offense, so I didn't expect it to be perceived as one now. My only intention was to propose improvements (perhaps I should have framed it as a depends_on rather than a supersede). At the end of the day, these are just architectural proposals meant to be openly discussed within the community. |
|
@angelmoya It is true that my comment was completely out place. I was angry and I made assertions where I shouldn't. For this reason, I apologize. Unfortunately, I had followed the same path of thought that you did a few weeks ago and the only possible option for streaming was going for queue_job or an external tool. For this reason, that will not be on the base module. As I said, we can keep hooks if necessary, but I don't want unnecessary dependencies on a base module. On the other hand, I would like to give you some extra details to give you a full perspective of my reaction:
|
|
Thank you for your message @etobella . Communication over text can be difficult sometimes. I will make sure to keep my comments shorter in the future; the long ones were precisely me trying to explain points where I lacked clarity:
I have closed the other PR. I will look into the hooks approach and give it a thought. Let's keep building! |
| lambda t, tool_call=tool_call: t.name == tool_call["name"] | ||
| ) | ||
| if tool: | ||
| messages.append(self._process_tool_call(tool, tool_call, record)) |
There was a problem hiding this comment.
This is the exact point where I believe we should catch any potential error so the LLM doesn't freeze if there is an issue with the tool execution.
There was a problem hiding this comment.
the problem with try catch is that there could be extra problems related to that...
Imagine we create an invoice and the system makes it unbalanced, leaving a try catch can give problems
probably better to have a savepoint. I will try to see if it is possible, otherwise it is better to leave it like this.
There was a problem hiding this comment.
Exactly! The savepoint would be perfect for this.
Instead of breaking the execution loop, we can catch the error, roll back the DB state for that tool, and pass the error message back to the LLM.
This way, the agent understands what went wrong with those parameters and can attempt to fix it or try a different approach.
e59bc79 to
f033db1
Compare
No description provided.