naas-abi 1.0.0__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 (62) hide show
  1. naas_abi/__init__.py +35 -0
  2. naas_abi/agents/AbiAgent.py +442 -0
  3. naas_abi/agents/AbiAgent_test.py +157 -0
  4. naas_abi/agents/EntitytoSPARQLAgent.py +952 -0
  5. naas_abi/agents/EntitytoSPARQLAgent_test.py +66 -0
  6. naas_abi/agents/KnowledgeGraphBuilderAgent.py +321 -0
  7. naas_abi/agents/KnowledgeGraphBuilderAgent_test.py +86 -0
  8. naas_abi/agents/OntologyEngineerAgent.py +115 -0
  9. naas_abi/agents/OntologyEngineerAgent_test.py +42 -0
  10. naas_abi/apps/oxigraph_admin/main.py +392 -0
  11. naas_abi/apps/oxigraph_admin/terminal_style.py +151 -0
  12. naas_abi/apps/sparql_terminal/main.py +68 -0
  13. naas_abi/apps/sparql_terminal/terminal_style.py +236 -0
  14. naas_abi/apps/terminal_agent/main.py +553 -0
  15. naas_abi/apps/terminal_agent/terminal_style.py +175 -0
  16. naas_abi/cli.py +714 -0
  17. naas_abi/mappings.py +83 -0
  18. naas_abi/models/airgap_gemma.py +220 -0
  19. naas_abi/models/airgap_qwen.py +24 -0
  20. naas_abi/models/default.py +23 -0
  21. naas_abi/models/gpt_4_1.py +25 -0
  22. naas_abi/pipelines/AIAgentOntologyGenerationPipeline.py +635 -0
  23. naas_abi/pipelines/AIAgentOntologyGenerationPipeline_test.py +133 -0
  24. naas_abi/pipelines/AddIndividualPipeline.py +215 -0
  25. naas_abi/pipelines/AddIndividualPipeline_test.py +66 -0
  26. naas_abi/pipelines/InsertDataSPARQLPipeline.py +197 -0
  27. naas_abi/pipelines/InsertDataSPARQLPipeline_test.py +96 -0
  28. naas_abi/pipelines/MergeIndividualsPipeline.py +245 -0
  29. naas_abi/pipelines/MergeIndividualsPipeline_test.py +98 -0
  30. naas_abi/pipelines/RemoveIndividualPipeline.py +166 -0
  31. naas_abi/pipelines/RemoveIndividualPipeline_test.py +58 -0
  32. naas_abi/pipelines/UpdateCommercialOrganizationPipeline.py +198 -0
  33. naas_abi/pipelines/UpdateDataPropertyPipeline.py +175 -0
  34. naas_abi/pipelines/UpdateLegalNamePipeline.py +107 -0
  35. naas_abi/pipelines/UpdateLinkedInPagePipeline.py +179 -0
  36. naas_abi/pipelines/UpdatePersonPipeline.py +184 -0
  37. naas_abi/pipelines/UpdateSkillPipeline.py +118 -0
  38. naas_abi/pipelines/UpdateTickerPipeline.py +104 -0
  39. naas_abi/pipelines/UpdateWebsitePipeline.py +106 -0
  40. naas_abi/triggers.py +131 -0
  41. naas_abi/workflows/AgentRecommendationWorkflow.py +321 -0
  42. naas_abi/workflows/AgentRecommendationWorkflow_test.py +160 -0
  43. naas_abi/workflows/ArtificialAnalysisWorkflow.py +337 -0
  44. naas_abi/workflows/ArtificialAnalysisWorkflow_test.py +57 -0
  45. naas_abi/workflows/ConvertOntologyGraphToYamlWorkflow.py +210 -0
  46. naas_abi/workflows/ConvertOntologyGraphToYamlWorkflow_test.py +78 -0
  47. naas_abi/workflows/CreateClassOntologyYamlWorkflow.py +208 -0
  48. naas_abi/workflows/CreateClassOntologyYamlWorkflow_test.py +65 -0
  49. naas_abi/workflows/CreateIndividualOntologyYamlWorkflow.py +183 -0
  50. naas_abi/workflows/CreateIndividualOntologyYamlWorkflow_test.py +86 -0
  51. naas_abi/workflows/ExportGraphInstancesToExcelWorkflow.py +450 -0
  52. naas_abi/workflows/ExportGraphInstancesToExcelWorkflow_test.py +33 -0
  53. naas_abi/workflows/GetObjectPropertiesFromClassWorkflow.py +385 -0
  54. naas_abi/workflows/GetObjectPropertiesFromClassWorkflow_test.py +57 -0
  55. naas_abi/workflows/GetSubjectGraphWorkflow.py +84 -0
  56. naas_abi/workflows/GetSubjectGraphWorkflow_test.py +71 -0
  57. naas_abi/workflows/SearchIndividualWorkflow.py +190 -0
  58. naas_abi/workflows/SearchIndividualWorkflow_test.py +98 -0
  59. naas_abi-1.0.0.dist-info/METADATA +9 -0
  60. naas_abi-1.0.0.dist-info/RECORD +62 -0
  61. naas_abi-1.0.0.dist-info/WHEEL +5 -0
  62. naas_abi-1.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,321 @@
1
+ import re
2
+ from dataclasses import dataclass
3
+ from enum import Enum
4
+ from pathlib import Path
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ import requests
8
+ from fastapi import APIRouter
9
+ from langchain_core.tools import BaseTool, StructuredTool
10
+ from naas_abi_core.services.triple_store.TripleStorePorts import ITripleStoreService
11
+ from naas_abi_core.workflow import Workflow, WorkflowConfiguration, WorkflowParameters
12
+ from rdflib import Graph, Namespace
13
+
14
+ ABI = Namespace("http://naas.ai/ontology/abi#")
15
+ INTENT_MAPPING = Namespace("http://ontology.naas.ai/intentMapping/")
16
+
17
+
18
+ @dataclass
19
+ class AgentRecommendationConfiguration(WorkflowConfiguration):
20
+ """Configuration for Agent Recommendation Workflow.
21
+
22
+ Attributes:
23
+ triple_store (ITripleStoreService): The ontology store service to use
24
+ oxigraph_url (str): URL of the Oxigraph SPARQL endpoint
25
+ queries_file_path (str): Path to the SPARQL queries TTL file
26
+ """
27
+
28
+ triple_store: ITripleStoreService
29
+ oxigraph_url: str = "http://localhost:7878"
30
+ queries_file_path: str = "src/core/modules/abi/ontologies/application-level/AgentRecommendationSparqlQueries.ttl"
31
+
32
+
33
+ class AgentRecommendationParameters(WorkflowParameters):
34
+ """Parameters for Agent Recommendation Workflow execution.
35
+
36
+ Attributes:
37
+ intent_description (str): Natural language description of what the user wants
38
+ min_intelligence_score (int): Minimum intelligence index (0-100)
39
+ max_input_cost (float): Maximum input cost per million tokens
40
+ max_results (int): Maximum number of recommendations to return
41
+ provider_preference (str): Optional preferred provider name
42
+ """
43
+
44
+ intent_description: str
45
+ min_intelligence_score: Optional[int] = 10
46
+ max_input_cost: Optional[float] = None
47
+ max_results: Optional[int] = 10
48
+ provider_preference: Optional[str] = None
49
+
50
+
51
+ class AgentRecommendationWorkflow(Workflow):
52
+ __configuration: AgentRecommendationConfiguration
53
+
54
+ def __init__(self, configuration: AgentRecommendationConfiguration):
55
+ self.__configuration = configuration
56
+ self._load_queries()
57
+
58
+ def _load_queries(self) -> None:
59
+ """Load SPARQL queries from the TTL file."""
60
+ self._queries = {}
61
+
62
+ # Load the TTL file and parse query templates
63
+ queries_file = Path(self.__configuration.queries_file_path)
64
+ if not queries_file.exists():
65
+ raise FileNotFoundError(f"Queries file not found: {queries_file}")
66
+
67
+ # Parse the RDF graph to extract query templates
68
+ graph = Graph()
69
+ graph.parse(queries_file, format="turtle")
70
+
71
+ # Extract templatable SPARQL queries
72
+ for subject in graph.subjects(predicate=INTENT_MAPPING.intentDescription):
73
+ query_id = str(subject).split("/")[-1].replace("Query", "")
74
+ intent_desc = str(graph.value(subject, INTENT_MAPPING.intentDescription))
75
+ sparql_template = str(graph.value(subject, INTENT_MAPPING.sparqlTemplate))
76
+
77
+ self._queries[query_id] = {
78
+ "intent_description": intent_desc,
79
+ "sparql_template": sparql_template,
80
+ }
81
+
82
+ def run_workflow(self, parameters: WorkflowParameters) -> Dict[str, Any]:
83
+ if not isinstance(parameters, AgentRecommendationParameters):
84
+ raise ValueError("Parameters must be of type AgentRecommendationParameters")
85
+
86
+ print(
87
+ f"🔍 [AgentRecommendation] Processing intent: '{parameters.intent_description}'"
88
+ )
89
+
90
+ # Step 1: Match intent to appropriate query
91
+ selected_query = self._match_intent_to_query(parameters.intent_description)
92
+ print(
93
+ f"📋 [AgentRecommendation] Matched query: {selected_query['intent_description']}"
94
+ )
95
+
96
+ # Step 2: Template the SPARQL query with parameters
97
+ templated_query = self._template_query(selected_query, parameters)
98
+ print("🔧 [AgentRecommendation] Templated SPARQL query:")
99
+ print(templated_query)
100
+
101
+ # Step 3: Execute the query against Oxigraph
102
+ print("⚡ [AgentRecommendation] Executing SPARQL query against Oxigraph...")
103
+ results = self._execute_sparql_query(templated_query)
104
+ print(f"📊 [AgentRecommendation] Query returned {len(results)} raw results")
105
+
106
+ # Step 4: Format recommendations for user
107
+ recommendations = self._format_recommendations(results, parameters)
108
+ print(
109
+ f"✅ [AgentRecommendation] Formatted {len(recommendations)} recommendations"
110
+ )
111
+
112
+ return {
113
+ "intent": parameters.intent_description,
114
+ "query_used": selected_query["intent_description"],
115
+ "recommendations": recommendations,
116
+ "total_found": len(recommendations),
117
+ }
118
+
119
+ def _match_intent_to_query(self, intent_description: str) -> Dict[str, str]:
120
+ """Match user intent to the most appropriate SPARQL query."""
121
+ intent_lower = intent_description.lower()
122
+
123
+ # Intent matching logic
124
+ if any(
125
+ word in intent_lower
126
+ for word in ["business", "proposal", "professional", "document"]
127
+ ):
128
+ return self._queries["abi#findBusinessProposalAgents"]
129
+ elif any(
130
+ word in intent_lower
131
+ for word in ["code", "programming", "development", "script"]
132
+ ):
133
+ return self._queries["abi#findCodingAgents"]
134
+ elif any(
135
+ word in intent_lower
136
+ for word in ["math", "calculation", "equation", "statistics"]
137
+ ):
138
+ return self._queries["abi#findMathAgents"]
139
+ elif any(word in intent_lower for word in ["fast", "quick", "speed", "rapid"]):
140
+ return self._queries["abi#findFastestAgents"]
141
+ elif any(word in intent_lower for word in ["cheap", "cost", "value", "budget"]):
142
+ return self._queries["abi#findBestValueAgents"]
143
+ else:
144
+ # Default to business proposal for general requests
145
+ return self._queries["abi#findBusinessProposalAgents"]
146
+
147
+ def _template_query(
148
+ self, query_info: Dict[str, str], parameters: AgentRecommendationParameters
149
+ ) -> str:
150
+ """Template the SPARQL query with user parameters."""
151
+ template = query_info["sparql_template"]
152
+
153
+ # Replace template variables
154
+ replacements: Dict[str, Any] = {
155
+ "min_intelligence_score": parameters.min_intelligence_score or 10,
156
+ "max_results": parameters.max_results or 10,
157
+ "min_coding_score": parameters.min_intelligence_score
158
+ or 10, # Use intelligence as fallback
159
+ "min_math_score": parameters.min_intelligence_score
160
+ or 10, # Use intelligence as fallback
161
+ }
162
+
163
+ # Add optional parameters
164
+ if parameters.max_input_cost:
165
+ replacements["max_input_cost"] = parameters.max_input_cost
166
+
167
+ if parameters.provider_preference:
168
+ replacements["provider_name"] = parameters.provider_preference
169
+
170
+ # Simple template replacement (for now - could use Jinja2 for more complex templating)
171
+ for key, value in replacements.items():
172
+ template = template.replace("{{ " + key + " }}", str(value))
173
+
174
+ # Handle conditional blocks (basic implementation)
175
+ template = self._handle_conditional_blocks(template, replacements)
176
+
177
+ return template
178
+
179
+ def _handle_conditional_blocks(
180
+ self, template: str, replacements: Dict[str, Any]
181
+ ) -> str:
182
+ """Handle {% if %} conditional blocks in templates."""
183
+ # Simple regex to find and process {% if variable %} blocks
184
+
185
+ # Pattern to match {% if variable %} ... {% endif %}
186
+ pattern = r"\{\%\s*if\s+(\w+)\s*\%\}(.*?)\{\%\s*endif\s*\%\}"
187
+
188
+ def replace_conditional(match):
189
+ variable = match.group(1)
190
+ content = match.group(2)
191
+
192
+ # Check if variable exists and has a truthy value in replacements
193
+ if variable in replacements and replacements[variable]:
194
+ return content
195
+ else:
196
+ return ""
197
+
198
+ return re.sub(pattern, replace_conditional, template, flags=re.DOTALL)
199
+
200
+ def _execute_sparql_query(self, sparql_query: str) -> List[Dict[str, Any]]:
201
+ """Execute SPARQL query against Oxigraph."""
202
+ try:
203
+ response = requests.post(
204
+ f"{self.__configuration.oxigraph_url}/query",
205
+ headers={"Content-Type": "application/sparql-query"},
206
+ data=sparql_query,
207
+ )
208
+ response.raise_for_status()
209
+
210
+ data = response.json()
211
+ return data.get("results", {}).get("bindings", [])
212
+
213
+ except requests.RequestException as e:
214
+ raise RuntimeError(f"Failed to execute SPARQL query: {e}")
215
+
216
+ def _format_recommendations(
217
+ self, results: List[Dict[str, Any]], parameters: AgentRecommendationParameters
218
+ ) -> List[Dict[str, Any]]:
219
+ """Format query results into user-friendly recommendations."""
220
+ recommendations = []
221
+
222
+ for result in results:
223
+ recommendation = {
224
+ "agent": self._extract_value(result, "agentLabel")
225
+ or self._extract_value(result, "agent"),
226
+ "model": self._extract_value(result, "model"),
227
+ "provider": self._extract_value(result, "provider"),
228
+ "costs": {
229
+ "input_per_million_tokens": float(
230
+ self._extract_value(result, "inputCost") or 0
231
+ ),
232
+ "output_per_million_tokens": float(
233
+ self._extract_value(result, "outputCost") or 0
234
+ ),
235
+ "currency": "USD",
236
+ },
237
+ "performance": {
238
+ "intelligence_index": float(
239
+ self._extract_value(result, "intelligenceIndex") or 0
240
+ ),
241
+ "coding_index": float(
242
+ self._extract_value(result, "codingIndex") or 0
243
+ ),
244
+ "math_index": float(self._extract_value(result, "mathIndex") or 0),
245
+ "output_speed": float(
246
+ self._extract_value(result, "outputSpeed") or 0
247
+ ),
248
+ },
249
+ "recommendation_reason": self._generate_recommendation_reason(
250
+ result, parameters.intent_description
251
+ ),
252
+ }
253
+
254
+ recommendations.append(recommendation)
255
+
256
+ return recommendations
257
+
258
+ def _extract_value(self, result: Dict[str, Any], key: str) -> Optional[str]:
259
+ """Extract value from SPARQL result binding."""
260
+ if key in result and "value" in result[key]:
261
+ return result[key]["value"]
262
+ return None
263
+
264
+ def _generate_recommendation_reason(
265
+ self, result: Dict[str, Any], intent: str
266
+ ) -> str:
267
+ """Generate a human-readable reason for the recommendation."""
268
+ intelligence = float(self._extract_value(result, "intelligenceIndex") or 0)
269
+ coding = float(self._extract_value(result, "codingIndex") or 0)
270
+ math = float(self._extract_value(result, "mathIndex") or 0)
271
+ input_cost = float(self._extract_value(result, "inputCost") or 0)
272
+
273
+ reasons = []
274
+
275
+ if intelligence >= 80:
276
+ reasons.append("high intelligence score")
277
+ if coding >= 80:
278
+ reasons.append("excellent coding capabilities")
279
+ if math >= 80:
280
+ reasons.append("strong mathematical reasoning")
281
+ if input_cost <= 1.0:
282
+ reasons.append("cost-effective pricing")
283
+
284
+ if not reasons:
285
+ reasons.append("good overall performance")
286
+
287
+ return f"Recommended for {intent.lower()} due to {', '.join(reasons)}"
288
+
289
+ def as_tools(self) -> list[BaseTool]:
290
+ """Returns a list of LangChain tools for this workflow.
291
+
292
+ Returns:
293
+ list[BaseTool]: List containing the workflow tool
294
+ """
295
+ return [
296
+ StructuredTool(
297
+ name="recommend_ai_agents",
298
+ description="Recommend AI agents based on user intent and requirements. Analyzes capabilities, performance, and costs to suggest the best agents for specific tasks like business proposals, coding, math, etc.",
299
+ func=lambda **kwargs: self.run_workflow(
300
+ AgentRecommendationParameters(**kwargs)
301
+ ),
302
+ args_schema=AgentRecommendationParameters,
303
+ )
304
+ ]
305
+
306
+ def as_api(
307
+ self,
308
+ router: APIRouter,
309
+ route_name: str = "",
310
+ name: str = "",
311
+ description: str = "",
312
+ description_stream: str = "",
313
+ tags: list[str | Enum] | None = None,
314
+ ) -> None:
315
+ if tags is None:
316
+ tags = []
317
+ return None
318
+
319
+ def get_configuration(self) -> AgentRecommendationConfiguration:
320
+ """Get the workflow configuration."""
321
+ return self.__configuration
@@ -0,0 +1,160 @@
1
+ from unittest.mock import Mock, patch
2
+
3
+ import pytest
4
+ from naas_abi.workflows.AgentRecommendationWorkflow import (
5
+ AgentRecommendationConfiguration,
6
+ AgentRecommendationParameters,
7
+ AgentRecommendationWorkflow,
8
+ )
9
+
10
+
11
+ @pytest.fixture
12
+ def workflow() -> AgentRecommendationWorkflow:
13
+ """Create a test workflow instance."""
14
+ mock_triple_store = Mock()
15
+
16
+ workflow_configuration = AgentRecommendationConfiguration(
17
+ triple_store=mock_triple_store,
18
+ oxigraph_url="http://localhost:7878",
19
+ queries_file_path="src/core/modules/abi/ontologies/application-level/AgentRecommendationSparqlQueries.ttl",
20
+ )
21
+
22
+ return AgentRecommendationWorkflow(workflow_configuration)
23
+
24
+
25
+ @pytest.fixture
26
+ def sample_parameters() -> AgentRecommendationParameters:
27
+ """Create sample workflow parameters."""
28
+ return AgentRecommendationParameters(
29
+ intent_description="I want to create a business proposal",
30
+ min_intelligence_score=75,
31
+ max_input_cost=5.0,
32
+ max_results=5,
33
+ )
34
+
35
+
36
+ def test_workflow_initialization(workflow):
37
+ """Test that workflow initializes correctly and loads queries."""
38
+ assert workflow is not None
39
+ assert hasattr(workflow, "_queries")
40
+ # Note: This will fail if the TTL file doesn't exist, which is expected
41
+
42
+
43
+ def test_intent_matching(workflow):
44
+ """Test intent matching to appropriate queries."""
45
+ # Test business proposal matching
46
+ business_query = workflow._match_intent_to_query(
47
+ "I need to create a business proposal"
48
+ )
49
+ assert "business" in business_query["intent_description"].lower()
50
+
51
+ # Test coding matching
52
+ coding_query = workflow._match_intent_to_query("I want to write some Python code")
53
+ assert "coding" in coding_query["intent_description"].lower()
54
+
55
+
56
+ def test_template_replacement(workflow):
57
+ """Test SPARQL template variable replacement."""
58
+ template = "SELECT * WHERE { ?x :score ?score . FILTER(?score >= {{ min_intelligence_score }}) } LIMIT {{ max_results }}"
59
+ parameters = AgentRecommendationParameters(
60
+ intent_description="test", min_intelligence_score=80, max_results=10
61
+ )
62
+
63
+ templated = workflow._template_query({"sparql_template": template}, parameters)
64
+ assert "80" in templated
65
+ assert "10" in templated
66
+ assert "{{" not in templated
67
+
68
+
69
+ def test_conditional_blocks(workflow):
70
+ """Test handling of conditional template blocks."""
71
+ template = """
72
+ SELECT * WHERE {
73
+ ?x :hasProperty ?y .
74
+ {% if max_input_cost %}
75
+ FILTER(?cost <= {{ max_input_cost }})
76
+ {% endif %}
77
+ }
78
+ """
79
+
80
+ # Test with conditional parameter
81
+ replacements = {"max_input_cost": 5.0}
82
+ result = workflow._handle_conditional_blocks(template, replacements)
83
+ assert "FILTER(?cost" in result
84
+
85
+ # Test without conditional parameter
86
+ replacements = {}
87
+ result = workflow._handle_conditional_blocks(template, replacements)
88
+ assert "FILTER(?cost" not in result
89
+
90
+
91
+ @patch("requests.post")
92
+ def test_sparql_execution(mock_post, workflow):
93
+ """Test SPARQL query execution."""
94
+ # Mock successful response
95
+ mock_response = Mock()
96
+ mock_response.raise_for_status.return_value = None
97
+ mock_response.json.return_value = {
98
+ "results": {
99
+ "bindings": [
100
+ {
101
+ "agent": {"value": "Claude AI Agent"},
102
+ "provider": {"value": "Anthropic"},
103
+ "inputCost": {"value": "8.0"},
104
+ "intelligenceIndex": {"value": "85"},
105
+ }
106
+ ]
107
+ }
108
+ }
109
+ mock_post.return_value = mock_response
110
+
111
+ results = workflow._execute_sparql_query("SELECT * WHERE { ?s ?p ?o }")
112
+
113
+ assert len(results) == 1
114
+ assert results[0]["agent"]["value"] == "Claude AI Agent"
115
+ mock_post.assert_called_once()
116
+
117
+
118
+ def test_recommendation_formatting(workflow):
119
+ """Test formatting of SPARQL results into recommendations."""
120
+ raw_results = [
121
+ {
122
+ "agentLabel": {"value": "Claude AI Agent"},
123
+ "provider": {"value": "Anthropic"},
124
+ "inputCost": {"value": "8.0"},
125
+ "outputCost": {"value": "24.0"},
126
+ "intelligenceIndex": {"value": "85"},
127
+ }
128
+ ]
129
+
130
+ parameters = AgentRecommendationParameters(
131
+ intent_description="create a business proposal"
132
+ )
133
+
134
+ recommendations = workflow._format_recommendations(raw_results, parameters)
135
+
136
+ assert len(recommendations) == 1
137
+ recommendation = recommendations[0]
138
+
139
+ assert recommendation["agent"] == "Claude AI Agent"
140
+ assert recommendation["provider"] == "Anthropic"
141
+ assert recommendation["costs"]["input_per_million_tokens"] == 8.0
142
+ assert recommendation["performance"]["intelligence_index"] == 85.0
143
+ assert "business proposal" in recommendation["recommendation_reason"]
144
+
145
+
146
+ def test_as_tools_returns_valid_tools(workflow):
147
+ """Test that workflow returns valid LangChain tools."""
148
+ tools = workflow.as_tools()
149
+
150
+ assert len(tools) == 1
151
+ tool = tools[0]
152
+
153
+ assert tool.name == "recommend_ai_agents"
154
+ assert "AI agents" in tool.description
155
+ assert hasattr(tool, "func")
156
+ assert hasattr(tool, "args_schema")
157
+
158
+
159
+ if __name__ == "__main__":
160
+ pytest.main([__file__])