ws-bom-robot-app 0.0.37__py3-none-any.whl → 0.0.103__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. ws_bom_robot_app/config.py +35 -7
  2. ws_bom_robot_app/cron_manager.py +15 -14
  3. ws_bom_robot_app/llm/agent_context.py +26 -0
  4. ws_bom_robot_app/llm/agent_description.py +123 -123
  5. ws_bom_robot_app/llm/agent_handler.py +176 -180
  6. ws_bom_robot_app/llm/agent_lcel.py +107 -54
  7. ws_bom_robot_app/llm/api.py +100 -7
  8. ws_bom_robot_app/llm/defaut_prompt.py +15 -15
  9. ws_bom_robot_app/llm/evaluator.py +319 -0
  10. ws_bom_robot_app/llm/feedbacks/__init__.py +0 -0
  11. ws_bom_robot_app/llm/feedbacks/feedback_manager.py +66 -0
  12. ws_bom_robot_app/llm/main.py +159 -110
  13. ws_bom_robot_app/llm/models/api.py +70 -5
  14. ws_bom_robot_app/llm/models/feedback.py +30 -0
  15. ws_bom_robot_app/llm/nebuly_handler.py +185 -0
  16. ws_bom_robot_app/llm/providers/llm_manager.py +244 -80
  17. ws_bom_robot_app/llm/tools/models/main.py +8 -0
  18. ws_bom_robot_app/llm/tools/tool_builder.py +68 -23
  19. ws_bom_robot_app/llm/tools/tool_manager.py +343 -133
  20. ws_bom_robot_app/llm/tools/utils.py +41 -25
  21. ws_bom_robot_app/llm/utils/agent.py +34 -0
  22. ws_bom_robot_app/llm/utils/chunker.py +6 -1
  23. ws_bom_robot_app/llm/utils/cleanup.py +81 -0
  24. ws_bom_robot_app/llm/utils/cms.py +123 -0
  25. ws_bom_robot_app/llm/utils/download.py +183 -79
  26. ws_bom_robot_app/llm/utils/print.py +29 -29
  27. ws_bom_robot_app/llm/vector_store/db/__init__.py +0 -0
  28. ws_bom_robot_app/llm/vector_store/db/base.py +193 -0
  29. ws_bom_robot_app/llm/vector_store/db/chroma.py +97 -0
  30. ws_bom_robot_app/llm/vector_store/db/faiss.py +91 -0
  31. ws_bom_robot_app/llm/vector_store/db/manager.py +15 -0
  32. ws_bom_robot_app/llm/vector_store/db/qdrant.py +73 -0
  33. ws_bom_robot_app/llm/vector_store/generator.py +137 -137
  34. ws_bom_robot_app/llm/vector_store/integration/api.py +216 -0
  35. ws_bom_robot_app/llm/vector_store/integration/azure.py +1 -1
  36. ws_bom_robot_app/llm/vector_store/integration/base.py +58 -15
  37. ws_bom_robot_app/llm/vector_store/integration/confluence.py +41 -11
  38. ws_bom_robot_app/llm/vector_store/integration/dropbox.py +1 -1
  39. ws_bom_robot_app/llm/vector_store/integration/gcs.py +1 -1
  40. ws_bom_robot_app/llm/vector_store/integration/github.py +22 -22
  41. ws_bom_robot_app/llm/vector_store/integration/googledrive.py +46 -17
  42. ws_bom_robot_app/llm/vector_store/integration/jira.py +112 -75
  43. ws_bom_robot_app/llm/vector_store/integration/manager.py +6 -2
  44. ws_bom_robot_app/llm/vector_store/integration/s3.py +1 -1
  45. ws_bom_robot_app/llm/vector_store/integration/sftp.py +1 -1
  46. ws_bom_robot_app/llm/vector_store/integration/sharepoint.py +7 -14
  47. ws_bom_robot_app/llm/vector_store/integration/shopify.py +143 -0
  48. ws_bom_robot_app/llm/vector_store/integration/sitemap.py +9 -1
  49. ws_bom_robot_app/llm/vector_store/integration/slack.py +3 -2
  50. ws_bom_robot_app/llm/vector_store/integration/thron.py +236 -0
  51. ws_bom_robot_app/llm/vector_store/loader/base.py +52 -8
  52. ws_bom_robot_app/llm/vector_store/loader/docling.py +71 -33
  53. ws_bom_robot_app/llm/vector_store/loader/json_loader.py +25 -25
  54. ws_bom_robot_app/main.py +148 -146
  55. ws_bom_robot_app/subprocess_runner.py +106 -0
  56. ws_bom_robot_app/task_manager.py +207 -54
  57. ws_bom_robot_app/util.py +65 -20
  58. ws_bom_robot_app-0.0.103.dist-info/METADATA +364 -0
  59. ws_bom_robot_app-0.0.103.dist-info/RECORD +76 -0
  60. {ws_bom_robot_app-0.0.37.dist-info → ws_bom_robot_app-0.0.103.dist-info}/WHEEL +1 -1
  61. ws_bom_robot_app/llm/settings.py +0 -4
  62. ws_bom_robot_app/llm/utils/agent_utils.py +0 -17
  63. ws_bom_robot_app/llm/utils/kb.py +0 -34
  64. ws_bom_robot_app-0.0.37.dist-info/METADATA +0 -277
  65. ws_bom_robot_app-0.0.37.dist-info/RECORD +0 -60
  66. {ws_bom_robot_app-0.0.37.dist-info → ws_bom_robot_app-0.0.103.dist-info}/top_level.txt +0 -0
@@ -1,180 +1,176 @@
1
- from asyncio import Queue
2
- from langchain_core.agents import AgentFinish
3
- from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
4
- from langchain.callbacks.base import AsyncCallbackHandler
5
- from ws_bom_robot_app.llm.utils.print import printJson, printString
6
- from typing import Any, Dict, List, Optional, Union
7
- from uuid import UUID
8
- import ws_bom_robot_app.llm.settings as settings
9
- from langchain_core.callbacks.base import AsyncCallbackHandler
10
- from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
11
- from langchain_core.messages import BaseMessage, HumanMessage, AIMessage
12
- import json
13
-
14
- # Here is a custom handler that will print the tokens to stdout.
15
- # Instead of printing to stdout you can send the data elsewhere; e.g., to a streaming API response
16
-
17
-
18
- def _parse_token(llm:str,token: str) -> str:
19
- """Parses the token based on the LLM provider."""
20
- if llm == "anthropic" and isinstance(token, list):
21
- first = token[0]
22
- if 'text' in first:
23
- token = first['text']
24
- else:
25
- #[{'id': 'toolu_01GGLwJcrQ8PvFMUkQPGu8n7', 'input': {}, 'name': 'document_retriever_xxx', 'type': 'tool_use', 'index': 1}]
26
- token = ""
27
- return token
28
-
29
- class AgentHandler(AsyncCallbackHandler):
30
-
31
- def __init__(self, queue: Queue, llm:str, threadId: str = None) -> None:
32
- super().__init__()
33
- self._threadId = threadId
34
- self.json_block = ""
35
- self.is_json_block = False
36
- self.backtick_count = 0 # Conteggio dei backticks per il controllo accurato
37
- self.queue = queue
38
- self.llm = llm
39
-
40
- async def on_llm_start(
41
- self,
42
- serialized: Dict[str, Any],
43
- prompts: List[str],
44
- *,
45
- run_id: UUID,
46
- parent_run_id: UUID = None,
47
- tags: List[str] = None,
48
- metadata: Dict[str, Any] = None,
49
- **kwargs: Any,
50
- ) -> None:
51
- firstChunk = {
52
- "type": "info",
53
- "threadId": self._threadId,
54
- }
55
- await self.queue.put(printString(firstChunk))
56
-
57
- """async def on_chat_model_start(self, serialized: Dict[str, Any], messages: List[List[BaseMessage]], *, run_id: UUID = None, parent_run_id = None, tags = None, metadata = None, **kwargs: Any) -> Any:
58
- pass"""
59
-
60
- async def on_tool_end(self, output: Any, *, run_id: UUID, parent_run_id: UUID = None, tags: List[str] = None, **kwargs: Any) -> None:
61
- pass
62
-
63
- async def on_llm_new_token(
64
- self,
65
- token: str,
66
- *,
67
- chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
68
- run_id: UUID,
69
- parent_run_id: Optional[UUID] = None,
70
- tags: Optional[List[str]] = None,
71
- **kwargs: Any,
72
- ) -> None:
73
- """Gestisce i nuovi token durante lo streaming."""
74
-
75
- if token:
76
- token = _parse_token(self.llm,token)
77
- if token:
78
- self.backtick_count += token.count("`")
79
-
80
- if self.backtick_count >= 3:
81
- if not self.is_json_block:
82
- self.is_json_block = True
83
- self.json_block = ""
84
- else:
85
- self.is_json_block = False
86
- self.json_block += token.replace("```json", '')
87
- await self.process_json_block(self.json_block)
88
- self.json_block = ""
89
- self.backtick_count = 0
90
- elif self.is_json_block:
91
- self.json_block += token
92
- else:
93
- await self.queue.put(printString(token))
94
-
95
- async def on_agent_finish(
96
- self,
97
- finish: AgentFinish,
98
- *,
99
- run_id: UUID,
100
- parent_run_id: UUID = None,
101
- tags: List[str] = None,
102
- **kwargs: Any,
103
- ) -> None:
104
- settings.chat_history.extend(
105
- [
106
- AIMessage(content=_parse_token(self.llm,finish.return_values["output"])),
107
- ]
108
- )
109
- finalChunk = {"type": "end"}
110
- await self.queue.put(printJson(finalChunk))
111
- await self.queue.put(None)
112
-
113
- async def process_json_block(self, json_block: str):
114
- """Processa il blocco JSON completo."""
115
- # Rimuove il delimitatore iniziale '```json' se presente, e spazi vuoti
116
- json_block_clean = json_block.replace('```', '').replace('json', '').strip()
117
- # Verifica che il blocco non sia vuoto prima di tentare il parsing
118
- if json_block_clean:
119
- try:
120
- # Prova a fare il parsing del JSON
121
- parsed_json = json.loads(json_block_clean)
122
- await self.queue.put(printJson(parsed_json))
123
- except json.JSONDecodeError as e:
124
- # Se il JSON è malformato, logga l'errore
125
- raise e
126
-
127
- class RawAgentHandler(AsyncCallbackHandler):
128
-
129
- def __init__(self,queue: Queue, llm: str) -> None:
130
- super().__init__()
131
- self.queue = queue
132
- self.llm = llm
133
- async def on_llm_start(
134
- self,
135
- serialized: Dict[str, Any],
136
- prompts: List[str],
137
- *,
138
- run_id: UUID,
139
- parent_run_id: UUID = None,
140
- tags: List[str] = None,
141
- metadata: Dict[str, Any] = None,
142
- **kwargs: Any,
143
- ) -> None:
144
- pass
145
-
146
- """async def on_chat_model_start(self, serialized: Dict[str, Any], messages: List[List[BaseMessage]], *, run_id: UUID = None, parent_run_id = None, tags = None, metadata = None, **kwargs: Any) -> Any:
147
- pass"""
148
-
149
- async def on_tool_end(self, output: Any, *, run_id: UUID, parent_run_id: UUID = None, tags: List[str] = None, **kwargs: Any) -> None:
150
- pass
151
-
152
- async def on_llm_new_token(
153
- self,
154
- token: str,
155
- *,
156
- chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
157
- run_id: UUID,
158
- parent_run_id: Optional[UUID] = None,
159
- tags: Optional[List[str]] = None,
160
- **kwargs: Any,
161
- ) -> None:
162
- """Handles new tokens during streaming."""
163
- if token: # Only process non-empty tokens
164
- await self.queue.put(_parse_token(self.llm,token))
165
-
166
- async def on_agent_finish(
167
- self,
168
- finish: AgentFinish,
169
- *,
170
- run_id: UUID,
171
- parent_run_id: UUID = None,
172
- tags: List[str] = None,
173
- **kwargs: Any,
174
- ) -> None:
175
- settings.chat_history.extend(
176
- [
177
- AIMessage(content=_parse_token(self.llm,finish.return_values["output"]))
178
- ]
179
- )
180
- await self.queue.put(None)
1
+ from asyncio import Queue
2
+ from langchain_core.agents import AgentFinish
3
+ from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
4
+ from langchain.callbacks.base import AsyncCallbackHandler
5
+ from ws_bom_robot_app.llm.utils.print import print_json, print_string
6
+ from typing import Any, Dict, List, Optional, Union
7
+ from uuid import UUID
8
+ from langchain_core.callbacks.base import AsyncCallbackHandler
9
+ from langchain_core.outputs import ChatGenerationChunk, GenerationChunk
10
+ from langchain_core.messages import BaseMessage, AIMessage
11
+ import json, logging, re
12
+
13
+ # Here is a custom handler that will print the tokens to stdout.
14
+ # Instead of printing to stdout you can send the data elsewhere; e.g., to a streaming API response
15
+
16
+
17
+ def _parse_token(llm:str,token: str) -> str:
18
+ """Parses the token based on the LLM provider."""
19
+ if llm == "anthropic" and isinstance(token, list):
20
+ first = token[0]
21
+ if 'text' in first:
22
+ token = first['text']
23
+ else:
24
+ #[{'id': 'toolu_01GGLwJcrQ8PvFMUkQPGu8n7', 'input': {}, 'name': 'document_retriever_xxx', 'type': 'tool_use', 'index': 1}]
25
+ token = ""
26
+ return token
27
+
28
+ class AgentHandler(AsyncCallbackHandler):
29
+
30
+ def __init__(self, queue: Queue, llm:str, threadId: str = None) -> None:
31
+ super().__init__()
32
+ self._threadId = threadId
33
+ self.queue = queue
34
+ self.llm = llm
35
+ self.__started: bool = False
36
+ # on new token event
37
+ self.stream_buffer = "" # accumulates text that hasn't been processed yet
38
+ self.in_json_block = False
39
+ self.json_buffer = ""
40
+ self.json_start_regex = re.compile(r'(`{1,3}\s*json\b)') # detect a potential json start fence.
41
+ self.json_end_regex = re.compile(r'(`{1,3})') # an end fence (one to three backticks).
42
+ self.stream_cut_last_output_chunk_size = 16 # safe cut last chunk size to output if no markers are found
43
+ async def on_chat_model_start(self, serialized, messages, *, run_id, parent_run_id = None, tags = None, metadata = None, **kwargs):
44
+ if not self.__started:
45
+ self.__started = True
46
+ firstChunk = {
47
+ "type": "info",
48
+ "threadId": self._threadId,
49
+ }
50
+ await self.queue.put(print_json(firstChunk))
51
+
52
+ async def on_llm_new_token(
53
+ self,
54
+ token: str,
55
+ *,
56
+ chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
57
+ run_id: UUID,
58
+ parent_run_id: Optional[UUID] = None,
59
+ tags: Optional[List[str]] = None,
60
+ **kwargs: Any,
61
+ ) -> None:
62
+ if token and "llm_chain" not in (tags or []):
63
+ token = _parse_token(self.llm,token)
64
+ if token:
65
+ self.stream_buffer += token # append new data to pending buffer
66
+ if not self.in_json_block:
67
+ # search for the start of a json block.
68
+ start_match = self.json_start_regex.search(self.stream_buffer)
69
+ if start_match:
70
+ start_index = start_match.start()
71
+ # everything before the start marker is normal content.
72
+ if start_index > 0:
73
+ _before = self.stream_buffer[:start_index].replace('`','').strip() # remove eventual preceding backticks.
74
+ if _before:
75
+ await self.queue.put(print_string(_before))
76
+ # remove the start marker from pending.
77
+ self.stream_buffer = self.stream_buffer[start_match.end():]
78
+ # switch into json mode.
79
+ self.in_json_block = True
80
+ self.json_buffer = ""
81
+ else:
82
+ # no json start marker found. It might be because the marker is split between chunks.
83
+ # to avoid losing potential marker fragments, output what we can safely process:
84
+ # if the pending text is long, we output most of it except the last few characters.
85
+ if len(self.stream_buffer) > self.stream_cut_last_output_chunk_size:
86
+ safe_cut = self.stream_buffer[:-3]
87
+ if safe_cut.startswith('`'):
88
+ safe_cut = safe_cut[1:]
89
+ await self.queue.put(print_string(safe_cut))
90
+ self.stream_buffer = self.stream_buffer[-3:]
91
+ else:
92
+ # in json block: look for an end fence.
93
+ end_match = self.json_end_regex.search(self.stream_buffer)
94
+ if end_match:
95
+ end_index = end_match.start()
96
+ self.json_buffer += self.stream_buffer[:end_index]
97
+ try:
98
+ data = json.loads(self.json_buffer.replace('`',''))
99
+ await self.queue.put(print_json(data))
100
+ except json.JSONDecodeError as e:
101
+ logging.error(f"on_token: invalid json: {e} | {self.json_buffer}")
102
+ finally:
103
+ self.json_buffer = ""
104
+ # remove the end fence from pending.
105
+ self.stream_buffer = self.stream_buffer[end_match.end():].strip()
106
+ self.in_json_block = False
107
+ else:
108
+ # no end marker found
109
+ # accumulate everything and break to wait for more data.
110
+ self.json_buffer += self.stream_buffer
111
+ self.stream_buffer = ""
112
+
113
+ async def on_tool_end(self, output, *, run_id, parent_run_id = None, tags = None, **kwargs):
114
+ if "stream" in (tags or []):
115
+ await self.queue.put(print_json(output))
116
+
117
+ async def on_agent_finish(
118
+ self,
119
+ finish: AgentFinish,
120
+ *,
121
+ run_id: UUID,
122
+ parent_run_id: UUID = None,
123
+ tags: List[str] = None,
124
+ **kwargs: Any,
125
+ ) -> None:
126
+ # end-of-stream: flush any remaining text
127
+ if self.in_json_block:
128
+ try:
129
+ data = json.loads(self.json_buffer)
130
+ await self.queue.put(print_json(data))
131
+ except json.JSONDecodeError as e :
132
+ logging.error(f"on_agent_finish: invalid json: {e} | {self.json_buffer}")
133
+ #await self.queue.put(print_string(self.json_buffer))
134
+ elif self.stream_buffer:
135
+ await self.queue.put(print_string(self.stream_buffer))
136
+
137
+ finalChunk = {"type": "end"}
138
+ await self.queue.put(print_json(finalChunk))
139
+ await self.queue.put(None)
140
+
141
+
142
+ class RawAgentHandler(AsyncCallbackHandler):
143
+
144
+ def __init__(self,queue: Queue, llm: str) -> None:
145
+ super().__init__()
146
+ self.queue = queue
147
+ self.llm = llm
148
+
149
+ async def on_llm_new_token(
150
+ self,
151
+ token: str,
152
+ *,
153
+ chunk: Optional[Union[GenerationChunk, ChatGenerationChunk]] = None,
154
+ run_id: UUID,
155
+ parent_run_id: Optional[UUID] = None,
156
+ tags: Optional[List[str]] = None,
157
+ **kwargs: Any,
158
+ ) -> None:
159
+ """Handles new tokens during streaming."""
160
+ if token: # only process non-empty tokens
161
+ await self.queue.put(_parse_token(self.llm,token))
162
+
163
+ async def on_tool_end(self, output, *, run_id, parent_run_id = None, tags = None, **kwargs):
164
+ if "stream" in (tags or []):
165
+ await self.queue.put(print_json(output))
166
+
167
+ async def on_agent_finish(
168
+ self,
169
+ finish: AgentFinish,
170
+ *,
171
+ run_id: UUID,
172
+ parent_run_id: UUID = None,
173
+ tags: List[str] = None,
174
+ **kwargs: Any,
175
+ ) -> None:
176
+ await self.queue.put(None)
@@ -1,54 +1,107 @@
1
- from typing import Any
2
- from langchain.agents import AgentExecutor, create_tool_calling_agent
3
- from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
4
- from langchain_core.runnables import RunnableLambda
5
- from langchain_core.tools import render_text_description
6
- from datetime import datetime
7
- from ws_bom_robot_app.llm.providers.llm_manager import LlmInterface
8
- from ws_bom_robot_app.llm.models.api import LlmMessage, LlmRules
9
- from ws_bom_robot_app.llm.utils.agent_utils import get_rules
10
- from ws_bom_robot_app.llm.defaut_prompt import default_prompt, tool_prompt
11
-
12
- class AgentLcel:
13
-
14
- def __init__(self, llm: LlmInterface, sys_message: str, tools: list, rules: LlmRules = None):
15
- self.sys_message = sys_message.format(
16
- date_stamp=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
17
- lang="it",
18
- )
19
- self.__llm = llm
20
- self.__tools = tools
21
- self.rules = rules
22
- self.embeddings = llm.get_embeddings()
23
- self.memory_key = "chat_history"
24
- self.__llm_with_tools = llm.get_llm().bind_tools(self.__tools) if len(self.__tools) > 0 else llm.get_llm()
25
- self.executor = self.__create_agent()
26
-
27
- async def __create_prompt(self, input: dict) -> ChatPromptTemplate:
28
- message : LlmMessage = input["input"]
29
- input = message.content
30
- rules_prompt = await get_rules(self.embeddings, self.rules, input) if self.rules else ""
31
- system = default_prompt + (tool_prompt(render_text_description(self.__tools)) if len(self.__tools)>0 else "") + self.sys_message + rules_prompt
32
- return ChatPromptTemplate.from_messages(
33
- [
34
- (
35
- "system", system
36
- ),
37
- MessagesPlaceholder(variable_name=self.memory_key),
38
- ("user", "{input}"),
39
- MessagesPlaceholder(variable_name="agent_scratchpad"),
40
- ]
41
- )
42
-
43
- def __create_agent(self) -> AgentExecutor:
44
- agent: Any = (
45
- {
46
- "input": lambda x: x["input"],
47
- "agent_scratchpad": lambda x: self.__llm.get_formatter(x["intermediate_steps"]),
48
- "chat_history": lambda x: x["chat_history"],
49
- }
50
- | RunnableLambda(self.__create_prompt)
51
- | self.__llm_with_tools
52
- | self.__llm.get_parser()
53
- )
54
- return AgentExecutor(agent=agent,tools=self.__tools,verbose=False)
1
+ from typing import Any, Optional
2
+ from langchain.agents import AgentExecutor, create_tool_calling_agent
3
+ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
4
+ from langchain_core.runnables import RunnableLambda
5
+ from langchain_core.tools import render_text_description
6
+ from pydantic import create_model, BaseModel
7
+ import chevron
8
+ from ws_bom_robot_app.llm.agent_context import AgentContext
9
+ from ws_bom_robot_app.llm.providers.llm_manager import LlmInterface
10
+ from ws_bom_robot_app.llm.models.api import LlmMessage, LlmRules
11
+ from ws_bom_robot_app.llm.utils.agent import get_rules
12
+ from ws_bom_robot_app.llm.defaut_prompt import default_prompt, tool_prompt
13
+
14
+ class AgentLcel:
15
+
16
+ def __init__(self, llm: LlmInterface, sys_message: str, sys_context: AgentContext, tools: list, rules: LlmRules = None, json_schema: Optional[dict] = None):
17
+ self.sys_message = chevron.render(template=sys_message,data=sys_context)
18
+ self.__llm = llm
19
+ self.__tools = tools
20
+ self.rules = rules
21
+ self.json_schema = json_schema
22
+ self.embeddings = llm.get_embeddings()
23
+ self.memory_key: str = "chat_history"
24
+ self.__llm_with_tools = llm.get_llm().bind_tools(self.__tools) if len(self.__tools) > 0 else llm.get_llm()
25
+ if self.json_schema:
26
+ self.__pydantic_schema = self.__create_pydantic_schema()
27
+ else:
28
+ self.__pydantic_schema = None
29
+
30
+ self.executor = self.__create_agent()
31
+
32
+ def __create_pydantic_schema(self) -> type[BaseModel]:
33
+ """Crea un Pydantic model dinamico dallo schema JSON."""
34
+ if not self.json_schema:
35
+ return None
36
+
37
+ type_map = {
38
+ "string": str,
39
+ "text": str,
40
+ "number": float,
41
+ "float": float,
42
+ "int": int,
43
+ "integer": int,
44
+ "bool": bool,
45
+ "boolean": bool,
46
+ "list": list,
47
+ "array": list,
48
+ "dict": dict,
49
+ "object": dict,
50
+ }
51
+
52
+ fields: dict[str, tuple[Any, Any]] = {}
53
+ for k, v in self.json_schema.items():
54
+ if isinstance(v, str):
55
+ py_type = type_map.get(v.lower(), str)
56
+ fields[k] = (py_type, ...)
57
+ elif isinstance(v, dict):
58
+ fields[k] = (dict, ...)
59
+ elif isinstance(v, list):
60
+ fields[k] = (list, ...)
61
+ else:
62
+ fields[k] = (type(v), ...)
63
+
64
+ return create_model('JsonSchema', **fields)
65
+
66
+ def __get_output_parser(self):
67
+ return self.__llm.get_parser()
68
+
69
+ async def __create_prompt(self, input: dict) -> ChatPromptTemplate:
70
+ from langchain_core.messages import SystemMessage
71
+ message : LlmMessage = input[self.memory_key][-1]
72
+ rules_prompt = await get_rules(self.embeddings, self.rules, message.content) if self.rules else ""
73
+ system = default_prompt + (tool_prompt(render_text_description(self.__tools)) if len(self.__tools)>0 else "") + self.sys_message + rules_prompt
74
+
75
+ # Aggiungi istruzioni per output JSON strutturato se necessario
76
+ if self.json_schema and self.__pydantic_schema:
77
+ json_instructions = f"\n\nIMPORTANT: You must format your final response as a JSON object with the following structure:\n"
78
+ for field_name, field_info in self.__pydantic_schema.model_fields.items():
79
+ field_type = field_info.annotation.__name__ if hasattr(field_info.annotation, '__name__') else str(field_info.annotation)
80
+ json_instructions += f"- {field_name}: {field_type}\n"
81
+ json_instructions += "\nProvide ONLY the JSON object in your response, no additional text."
82
+ system += json_instructions
83
+
84
+ messages = [
85
+ SystemMessage(content=system),
86
+ MessagesPlaceholder(variable_name=self.memory_key),
87
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
88
+ ]
89
+
90
+ prompt = ChatPromptTemplate.from_messages(
91
+ messages=messages,
92
+ template_format=None,
93
+ )
94
+ return prompt
95
+
96
+ def __create_agent(self):
97
+ # Un solo AgentExecutor per entrambe le modalità
98
+ agent = (
99
+ {
100
+ "agent_scratchpad": lambda x: self.__llm.get_formatter(x["intermediate_steps"]),
101
+ self.memory_key: lambda x: x[self.memory_key],
102
+ }
103
+ | RunnableLambda(self.__create_prompt)
104
+ | self.__llm_with_tools
105
+ | self.__get_output_parser()
106
+ )
107
+ return AgentExecutor(agent=agent, tools=self.__tools, verbose=False)