unique_deep_research 3.0.28__tar.gz → 3.1.0__tar.gz

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 (23) hide show
  1. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/CHANGELOG.md +3 -0
  2. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/PKG-INFO +5 -2
  3. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/pyproject.toml +5 -5
  4. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/config.py +55 -10
  5. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/service.py +6 -5
  6. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/unique_custom/agents.py +4 -2
  7. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/unique_custom/tools.py +9 -10
  8. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/unique_custom/utils.py +2 -2
  9. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/LICENSE +0 -0
  10. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/README.md +0 -0
  11. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/__init__.py +0 -0
  12. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/markdown_utils.py +0 -0
  13. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/clarifying_agent.j2 +0 -0
  14. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/openai/oai_research_system_message.j2 +0 -0
  15. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/report_cleanup_prompt.j2 +0 -0
  16. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/research_instructions_agent.j2 +0 -0
  17. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/unique/compress_research_system.j2 +0 -0
  18. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/unique/lead_agent_system.j2 +0 -0
  19. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/unique/report_writer_system_open_deep_research.j2 +0 -0
  20. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/templates/unique/research_agent_system.j2 +0 -0
  21. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/unique_custom/__init__.py +0 -0
  22. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/unique_custom/citation.py +0 -0
  23. {unique_deep_research-3.0.28 → unique_deep_research-3.1.0}/unique_deep_research/unique_custom/state.py +0 -0
@@ -5,6 +5,9 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [3.1.00] - 2026-01-30
9
+ - Support other search engines than Google
10
+
8
11
  ## [3.0.28] - 2026-01-26
9
12
  - Specify `langchain-openai` version
10
13
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: unique_deep_research
3
- Version: 3.0.28
3
+ Version: 3.1.0
4
4
  Summary: Deep Research Tool for complex research tasks
5
5
  License: Proprietary
6
6
  Author: Martin Fadler
@@ -22,8 +22,8 @@ Requires-Dist: pydantic (>=2.8.2,<3.0.0)
22
22
  Requires-Dist: tiktoken (>=0.12.0,<0.13.0)
23
23
  Requires-Dist: timeout-decorator (>=0.5.0,<0.6.0)
24
24
  Requires-Dist: typing-extensions (>=4.9.0,<5.0.0)
25
- Requires-Dist: unique-toolkit (>=1.38.3,<2.0.0)
26
25
  Requires-Dist: unique-web-search (>=1.7.0,<2.0.0)
26
+ Requires-Dist: unique_toolkit (>=1.38.3,<2.0.0)
27
27
  Description-Content-Type: text/markdown
28
28
 
29
29
  # Deep Research Tool
@@ -36,6 +36,9 @@ All notable changes to this project will be documented in this file.
36
36
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
37
37
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
38
38
 
39
+ ## [3.1.00] - 2026-01-30
40
+ - Support other search engines than Google
41
+
39
42
  ## [3.0.28] - 2026-01-26
40
43
  - Specify `langchain-openai` version
41
44
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "unique_deep_research"
3
- version = "3.0.28"
3
+ version = "3.1.00"
4
4
  description = "Deep Research Tool for complex research tasks"
5
5
  authors = [
6
6
  "Martin Fadler <martin.fadler@unique.ch>",
@@ -15,7 +15,7 @@ license = "Proprietary"
15
15
  python = "^3.12"
16
16
  pydantic = "^2.8.2"
17
17
  typing-extensions = "^4.9.0"
18
- unique-toolkit = "^1.38.3"
18
+ unique_toolkit = "^1.38.3"
19
19
  jinja2 = "^3.1.2"
20
20
  openai = "^1.99.0"
21
21
  langgraph = "^1.0.0"
@@ -31,9 +31,9 @@ langchain = {version = "^1.1.0", extras = ["openai"]}
31
31
 
32
32
 
33
33
  [tool.poetry.group.dev.dependencies]
34
- unique-sdk = { path = "../../unique_sdk" }
35
- unique-toolkit = { path = "../../unique_toolkit" }
36
- unique-web-search = { path = "../unique_web_search" }
34
+ unique-sdk = { path = "../../unique_sdk" , develop = true}
35
+ unique-toolkit = { path = "../../unique_toolkit" , develop = true}
36
+ unique-web-search = { path = "../unique_web_search" , develop = true}
37
37
  pytest = "^8.4.1"
38
38
  pytest-asyncio = "^0.25.0"
39
39
  pytest-cov = "^7.0.0"
@@ -1,13 +1,19 @@
1
1
  from dataclasses import dataclass
2
2
  from enum import StrEnum
3
3
  from pathlib import Path
4
- from typing import Literal
4
+ from typing import Any, Generic, Literal, TypeVar
5
5
 
6
6
  from jinja2 import Environment, FileSystemLoader
7
- from pydantic import BaseModel, Field
7
+ from pydantic import BaseModel, Field, field_validator
8
8
  from unique_toolkit._common.validators import LMI, get_LMI_default_field
9
+ from unique_toolkit.agentic.tools.config import get_configuration_dict
9
10
  from unique_toolkit.agentic.tools.schemas import BaseToolConfig
10
11
  from unique_toolkit.language_model.infos import LanguageModelName
12
+ from unique_web_search.config import (
13
+ ActivatedSearchEngine,
14
+ DefaultSearchEngine,
15
+ )
16
+ from unique_web_search.services.search_engine import GoogleConfig
11
17
 
12
18
  # Global template environment for the deep research tool
13
19
  TEMPLATE_DIR = Path(__file__).parent / "templates"
@@ -32,10 +38,14 @@ class UniqueCustomEngineConfig:
32
38
  RESPONSES_API_TIMEOUT_SECONDS = 3600
33
39
 
34
40
 
35
- class BaseEngine(BaseModel):
36
- engine_type: Literal[DeepResearchEngine.UNIQUE, DeepResearchEngine.OPENAI] = Field(
37
- description="The type of engine to use for deep research"
38
- )
41
+ T = TypeVar("T", bound=DeepResearchEngine)
42
+
43
+
44
+ class BaseEngine(BaseModel, Generic[T]):
45
+ model_config = get_configuration_dict()
46
+
47
+ engine_type: T = Field(description="The type of engine to use for deep research")
48
+
39
49
  small_model: LMI = get_LMI_default_field(
40
50
  LanguageModelName.AZURE_GPT_4o_2024_1120,
41
51
  description="A smaller fast model for less demanding tasks",
@@ -55,7 +65,9 @@ class BaseEngine(BaseModel):
55
65
  return DeepResearchEngine(self.engine_type)
56
66
 
57
67
 
58
- class OpenAIEngine(BaseEngine):
68
+ class OpenAIEngine(BaseEngine[Literal[DeepResearchEngine.OPENAI]]):
69
+ model_config = get_configuration_dict()
70
+
59
71
  engine_type: Literal[DeepResearchEngine.OPENAI] = Field(
60
72
  default=DeepResearchEngine.OPENAI
61
73
  )
@@ -65,18 +77,51 @@ class OpenAIEngine(BaseEngine):
65
77
  )
66
78
 
67
79
 
68
- class Tools(BaseModel):
69
- web_tools: bool = Field(
80
+ class WebToolsConfig(BaseModel):
81
+ model_config = get_configuration_dict()
82
+
83
+ enable: bool = Field(
70
84
  default=True,
71
85
  description="Allow agent to use web search tools to access the web",
72
86
  )
87
+
88
+ search_engine: ActivatedSearchEngine = Field( # pyright: ignore[reportInvalidTypeForm]
89
+ default_factory=DefaultSearchEngine, # pyright: ignore[reportArgumentType]
90
+ description="Search Engine Configuration",
91
+ discriminator="search_engine_name",
92
+ title="Search Engine Configuration",
93
+ )
94
+
95
+
96
+ class Tools(BaseModel):
97
+ model_config = get_configuration_dict()
98
+
99
+ web_tools: WebToolsConfig = Field(
100
+ default=WebToolsConfig(),
101
+ description="Configuration for web search tools",
102
+ )
73
103
  internal_tools: bool = Field(
74
104
  default=True,
75
105
  description="Allow agent to use internal search tools access information from the knowledge base and uploaded documents",
76
106
  )
77
107
 
108
+ @field_validator("web_tools", mode="before")
109
+ @classmethod
110
+ def handle_bool_case(cls, v: Any) -> Any:
111
+ if isinstance(v, bool):
112
+ if v:
113
+ # Backward compatibility with old config behaviour
114
+ return WebToolsConfig(
115
+ enable=True, search_engine=GoogleConfig(fetch_size=10)
116
+ )
117
+ else:
118
+ return WebToolsConfig(enable=False)
119
+ return v
120
+
121
+
122
+ class UniqueEngine(BaseEngine[Literal[DeepResearchEngine.UNIQUE]]):
123
+ model_config = get_configuration_dict()
78
124
 
79
- class UniqueEngine(BaseEngine):
80
125
  engine_type: Literal[DeepResearchEngine.UNIQUE] = Field(
81
126
  default=DeepResearchEngine.UNIQUE
82
127
  )
@@ -207,7 +207,7 @@ class DeepResearchTool(Tool[DeepResearchToolConfig]):
207
207
  except Exception as e:
208
208
  if self.is_message_execution():
209
209
  await self._update_execution_status(MessageExecutionUpdateStatus.FAILED)
210
- self.logger.error(f"Deep Research tool run failed: {e}")
210
+ self.logger.exception(f"Deep Research tool run failed: {e}")
211
211
  await self.chat_service.modify_assistant_message_async(
212
212
  content="Deep Research failed to complete for an unknown reason",
213
213
  set_completed_at=True,
@@ -394,7 +394,7 @@ class DeepResearchTool(Tool[DeepResearchToolConfig]):
394
394
  self.write_message_log_text_message("**Research done**")
395
395
  return result
396
396
  except Exception as e:
397
- self.logger.error(f"Research failed: {e}")
397
+ self.logger.exception(f"Research failed: {e}")
398
398
  return "", []
399
399
 
400
400
  async def custom_research(self, research_brief: str) -> tuple[str, list[Any]]:
@@ -427,12 +427,13 @@ class DeepResearchTool(Tool[DeepResearchToolConfig]):
427
427
  enable_web_tools = True
428
428
  enable_internal_tools = True
429
429
  if isinstance(self.config.engine, UniqueEngine):
430
- enable_web_tools = self.config.engine.tools.web_tools
430
+ enable_web_tools = self.config.engine.tools.web_tools.enable
431
431
  enable_internal_tools = self.config.engine.tools.internal_tools
432
432
 
433
433
  config = {
434
434
  "configurable": {
435
435
  "engine_config": self.config.engine,
436
+ "language_model_service": self.language_model_service,
436
437
  "openai_client": self.client,
437
438
  "chat_service": self.chat_service,
438
439
  "content_service": self.content_service,
@@ -470,7 +471,7 @@ class DeepResearchTool(Tool[DeepResearchToolConfig]):
470
471
 
471
472
  except Exception as e:
472
473
  error_msg = f"Custom research failed: {str(e)}"
473
- self.logger.error(error_msg, exc_info=True)
474
+ self.logger.exception(error_msg)
474
475
  return error_msg, []
475
476
 
476
477
  async def openai_research(self, research_brief: str) -> tuple[str, list[Any]]:
@@ -698,7 +699,7 @@ class DeepResearchTool(Tool[DeepResearchToolConfig]):
698
699
  if event.response.error:
699
700
  return event.response.error.message, []
700
701
  except Exception as e:
701
- self.logger.error(f"Error processing research stream event: {e}")
702
+ self.logger.exception(f"Error processing research stream event: {e}")
702
703
 
703
704
  self.logger.error("Stream ended without completion")
704
705
  return "", []
@@ -253,7 +253,7 @@ async def supervisor_tools(
253
253
  all_tool_messages.extend(research_tool_messages)
254
254
 
255
255
  except Exception as e:
256
- _LOGGER.error(f"Research execution failed: {e}")
256
+ _LOGGER.exception(f"Research execution failed: {e}")
257
257
  return Command(
258
258
  goto="__end__",
259
259
  update={
@@ -596,7 +596,9 @@ async def _handle_conduct_research_batch(
596
596
  tool_messages = []
597
597
  for observation, tool_call in zip(tool_results, allowed_calls):
598
598
  if isinstance(observation, Exception):
599
- _LOGGER.error(f"Research task failed: {str(observation)}")
599
+ _LOGGER.exception(
600
+ f"Research task failed: {str(observation)}", exc_info=observation
601
+ )
600
602
  error_content = f"Research task failed: {str(observation)}"
601
603
  tool_messages.append(
602
604
  ToolMessage(
@@ -22,8 +22,7 @@ from unique_toolkit.chat.schemas import (
22
22
  )
23
23
  from unique_toolkit.content import ContentReference
24
24
  from unique_toolkit.content.schemas import ContentChunk, ContentSearchType
25
- from unique_web_search.client_settings import get_google_search_settings
26
- from unique_web_search.services.search_engine.google import GoogleConfig, GoogleSearch
25
+ from unique_web_search.services.search_engine import get_search_engine_service
27
26
 
28
27
  from .utils import (
29
28
  get_citation_manager,
@@ -153,18 +152,18 @@ async def web_search(query: str, config: RunnableConfig) -> str:
153
152
  You can search again with a new query if you need more results.
154
153
  Should be followed up by web_fetch to get the complete content of the results.
155
154
  """
156
- google_settings = get_google_search_settings()
155
+ if "configurable" not in config:
156
+ raise ValueError("RunnableConfig missing 'configurable' section")
157
157
 
158
- if not google_settings.is_configured:
159
- _LOGGER.error("Google Search not configured")
160
- raise ValueError("Google Search not configured")
158
+ configurable = config["configurable"]
159
+ engine_config = configurable["engine_config"].tools.web_tools.search_engine
161
160
 
162
- # Create Google search configuration and service
163
- google_config = GoogleConfig(fetch_size=10)
164
- google_search = GoogleSearch(google_config)
161
+ search_engine_service = get_search_engine_service(
162
+ engine_config, configurable["language_model_service"]
163
+ )
165
164
 
166
165
  # Perform the search
167
- search_results = await google_search.search(query)
166
+ search_results = await search_engine_service.search(query)
168
167
  write_tool_message_log(
169
168
  config,
170
169
  "**Searching the web**",
@@ -355,7 +355,7 @@ async def execute_tool_safely(
355
355
  _LOGGER.warning(f"Token limit exceeded in tool {tool_name}: {str(e)}")
356
356
  return f"Tool {tool_name} failed due to token limit: {str(e)}"
357
357
  else:
358
- _LOGGER.error(f"Error executing tool {tool_name}: {str(e)}")
358
+ _LOGGER.exception(f"Error executing tool {tool_name}: {str(e)}")
359
359
  return f"Error executing tool {tool_name}: {str(e)}"
360
360
 
361
361
 
@@ -588,7 +588,7 @@ async def ainvoke_with_token_handling(
588
588
  )
589
589
  await asyncio.sleep(delay)
590
590
  else:
591
- _LOGGER.error(
591
+ _LOGGER.exception(
592
592
  f"Model invocation failed after {max_retries + 1} attempts: {str(e)}"
593
593
  )
594
594
  raise e