covalve 0.1.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 (38) hide show
  1. covalve/__init__.py +35 -0
  2. covalve/infrastructure/base/cache.py +12 -0
  3. covalve/infrastructure/base/guardrails.py +6 -0
  4. covalve/infrastructure/base/llm.py +11 -0
  5. covalve/infrastructure/base/log.py +7 -0
  6. covalve/infrastructure/base/memory.py +10 -0
  7. covalve/infrastructure/base/tools.py +7 -0
  8. covalve/infrastructure/contract.py +20 -0
  9. covalve/runtime/engine.py +98 -0
  10. covalve/runtime/executor/analyze_query.py +43 -0
  11. covalve/runtime/executor/error.py +14 -0
  12. covalve/runtime/executor/error_counter.py +18 -0
  13. covalve/runtime/executor/fallback.py +20 -0
  14. covalve/runtime/executor/guardrail.py +13 -0
  15. covalve/runtime/executor/main_llm.py +72 -0
  16. covalve/runtime/executor/retrieve_conv.py +15 -0
  17. covalve/runtime/executor/save_data.py +28 -0
  18. covalve/runtime/executor/tools_executor.py +65 -0
  19. covalve/runtime/executor/tools_mapper.py +18 -0
  20. covalve/runtime/hook/__init__.py +4 -0
  21. covalve/runtime/hook/context.py +11 -0
  22. covalve/runtime/hook/executor.py +32 -0
  23. covalve/runtime/hook/registry.py +43 -0
  24. covalve/runtime/init.py +76 -0
  25. covalve/runtime/models/context.py +60 -0
  26. covalve/runtime/models/infra.py +25 -0
  27. covalve/runtime/models/io.py +36 -0
  28. covalve/runtime/models/logs.py +14 -0
  29. covalve/runtime/models/metadata.py +33 -0
  30. covalve/runtime/pipeline.py +31 -0
  31. covalve/runtime/registry.py +23 -0
  32. covalve/runtime/validator/graph_traversal.py +44 -0
  33. covalve/schemas/__init__.py +0 -0
  34. covalve/schemas/schema.json +56 -0
  35. covalve-0.1.0.dist-info/METADATA +298 -0
  36. covalve-0.1.0.dist-info/RECORD +38 -0
  37. covalve-0.1.0.dist-info/WHEEL +4 -0
  38. covalve-0.1.0.dist-info/licenses/LICENSE +21 -0
covalve/__init__.py ADDED
@@ -0,0 +1,35 @@
1
+ # entry point
2
+ from covalve.runtime.pipeline import pipeline, base_schema
3
+
4
+ # config & context
5
+ from covalve.runtime.models.context import (
6
+ PipelineConfig,
7
+ PipelineContext,
8
+ ArgsCtx,
9
+ ReturnSchema,
10
+ SchemaCollections,
11
+ )
12
+
13
+ # infrastructure contracts
14
+ from covalve.infrastructure.contract import InfrastructureRegistry
15
+ from covalve.infrastructure.base.llm import LLMBase
16
+ from covalve.infrastructure.base.memory import MemoryStoreBase
17
+ from covalve.infrastructure.base.cache import CacheBase
18
+ from covalve.infrastructure.base.tools import ToolClientBase
19
+ from covalve.infrastructure.base.log import LogBase
20
+ from covalve.infrastructure.base.guardrails import GuardrailBase
21
+
22
+ # hook system
23
+ from covalve.runtime.hook import hooks
24
+ from covalve.runtime.hook.registry import HookOn
25
+ from covalve.runtime.hook.context import ReadOnlyContext
26
+
27
+ # io models
28
+ from covalve.runtime.models.io import (
29
+ OutputSchema,
30
+ OutputStatus,
31
+ MainLLMResponse,
32
+ GenerateCondition,
33
+ )
34
+ from covalve.runtime.models.logs import StateLog
35
+ from covalve.runtime.models.metadata import RuntimeMetadata, QueryIntent
@@ -0,0 +1,12 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Any
3
+
4
+ class CacheBase(ABC):
5
+ @abstractmethod
6
+ async def get(self, key:str) -> str | None: ...
7
+
8
+ @abstractmethod
9
+ async def set(self, key: str, value: Any) -> None: ...
10
+
11
+ @abstractmethod
12
+ async def delete(self, key: str) -> None: ...
@@ -0,0 +1,6 @@
1
+ from abc import ABC, abstractmethod
2
+ from covalve.runtime.models.infra import BackgroundUnit, GuardRailResponse
3
+
4
+ class GuardrailBase(ABC):
5
+ @abstractmethod
6
+ async def validate(self, query:str, background: BackgroundUnit) -> GuardRailResponse:...
@@ -0,0 +1,11 @@
1
+ from abc import ABC, abstractmethod
2
+ from covalve.runtime.models.io import MainLLMResponse,GenerateCondition
3
+ from covalve.runtime.models.context import RuntimeMetadata
4
+
5
+ class LLMBase(ABC):
6
+
7
+ @abstractmethod
8
+ async def analyze(self, context_payload: str) -> RuntimeMetadata: ...
9
+
10
+ @abstractmethod
11
+ async def generate(self, context_payload: str, condition: GenerateCondition) -> MainLLMResponse: ...
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+ from covalve.runtime.models.logs import StateLog
3
+
4
+ class LogBase(ABC):
5
+ @abstractmethod
6
+ async def state_log(self, ctx: StateLog):
7
+ raise NotImplementedError("Log Client is not implemented yet")
@@ -0,0 +1,10 @@
1
+ from abc import ABC, abstractmethod
2
+ from covalve.runtime.models.io import DataContent
3
+ from covalve.runtime.models.infra import BackgroundUnit
4
+
5
+ class MemoryStoreBase(ABC):
6
+ @abstractmethod
7
+ async def save_conv(self, content:DataContent):...
8
+
9
+ @abstractmethod
10
+ async def retrieve_conv(self, session_id:str) -> BackgroundUnit | None:...
@@ -0,0 +1,7 @@
1
+ from abc import ABC, abstractmethod
2
+ from covalve.runtime.models.context import RuntimeMetadata
3
+ from covalve.runtime.models.infra import MCPResponse
4
+
5
+ class ToolClientBase(ABC):
6
+ @abstractmethod
7
+ async def retrieve(self, tool_name: str, metadata:RuntimeMetadata) -> MCPResponse: ...
@@ -0,0 +1,20 @@
1
+
2
+ from typing import Optional
3
+ from dataclasses import dataclass
4
+ from pydantic import BaseModel
5
+ from covalve.infrastructure.base.memory import MemoryStoreBase
6
+ from covalve.infrastructure.base.llm import LLMBase
7
+ from covalve.infrastructure.base.log import LogBase
8
+ from covalve.infrastructure.base.tools import ToolClientBase
9
+ from covalve.infrastructure.base.cache import CacheBase
10
+ from covalve.infrastructure.base.guardrails import GuardrailBase
11
+
12
+
13
+ @dataclass
14
+ class InfrastructureRegistry:
15
+ llm: Optional[LLMBase] = None
16
+ memory: Optional[MemoryStoreBase] = None
17
+ cache: Optional[CacheBase] = None
18
+ tools: Optional[ToolClientBase] = None
19
+ log: Optional[LogBase] = None
20
+ guardrail: Optional[GuardrailBase] = None
@@ -0,0 +1,98 @@
1
+ import hashlib
2
+ import time
3
+ import asyncio
4
+ from datetime import datetime
5
+ from covalve.runtime.models.context import PipelineContext, ArgsCtx, ReturnSchema, STOP, SchemaCollections
6
+ from covalve.infrastructure.contract import InfrastructureRegistry
7
+ from covalve.runtime.models.logs import StateLog
8
+ from covalve.runtime.hook.executor import hook_executor
9
+ from covalve.runtime.hook.registry import HookOn
10
+
11
+
12
+ def _init_context(query, session_id) -> tuple[str, str, PipelineContext]:
13
+ new_session_id = session_id
14
+ now = datetime.now()
15
+ traceId = hashlib.sha256(f"traceId-{query}-{now}".encode('utf-8')).hexdigest()
16
+ if new_session_id is None:
17
+ new_session_id = hashlib.sha256(f"{query}-{now}".encode('utf-8')).hexdigest()
18
+
19
+ new_contex = PipelineContext(is_error=False,query=query,session_id=new_session_id ,current_time=now,traceId=traceId)
20
+ return (traceId, new_session_id, new_contex)
21
+
22
+
23
+ async def _execute_state(states:dict, handlers, args_ctx:ArgsCtx) -> tuple[str, str, PipelineContext, str]:
24
+ error = ""
25
+ try:
26
+ result:ReturnSchema = await handlers(args_ctx)
27
+ running_context: PipelineContext = result.context
28
+ if result.event not in states[args_ctx.state]["transitions"] and not result.event == STOP.HANDLER_ERROR:
29
+ event_emmited = result.event
30
+ current_state = STOP.INVALID_EVENT
31
+ else:
32
+ event_emmited = result.event
33
+ current_state = states[args_ctx.state]["transitions"][event_emmited]["to"]
34
+ except Exception as e:
35
+ event_emmited = STOP.HANDLER_ERROR
36
+ running_context = args_ctx.context
37
+ current_state = STOP.HANDLER_ERROR
38
+ error = str(e)
39
+
40
+ return (event_emmited, current_state, running_context, error)
41
+
42
+
43
+ def _fire_state_log(deps:InfrastructureRegistry, log_data: StateLog) -> None:
44
+ asyncio.create_task(deps.log.state_log(log_data))
45
+
46
+
47
+
48
+ def create_engine(schemaCols:SchemaCollections, handlers:dict, hooks:dict, deps:InfrastructureRegistry):
49
+ core_schema = schemaCols.core_schema
50
+
51
+ async def engine(query, session_id=None):
52
+ stop_state = [core_schema["FINAL"], STOP.INVALID_EVENT, STOP.HANDLER_ERROR, STOP.INTERCEPTOR_ERROR]
53
+ traceId, new_session_id, new_context = _init_context(query, session_id)
54
+ running_context = new_context
55
+
56
+ current_state = core_schema["INITIAL"]
57
+ active_state = core_schema["INITIAL"]
58
+ while current_state not in stop_state:
59
+ error_string = ""
60
+ args_ctx = ArgsCtx(state=current_state, context=running_context, schema=schemaCols)
61
+ handler = handlers[current_state]
62
+
63
+ start = time.perf_counter()
64
+
65
+ hook_result = await hook_executor(HookOn.ENTER,core_schema["states"],current_state,hooks,running_context)
66
+ if hook_result.intercepted:
67
+ current_state = hook_result.to
68
+ error_string = hook_result.error
69
+ event_emmited = hook_result.event
70
+ else:
71
+ event_emmited, current_state, running_context, error_string = await _execute_state(core_schema["states"], handler, args_ctx)
72
+
73
+ hook_result = await hook_executor(HookOn.EXIT,core_schema["states"],active_state,hooks,running_context)
74
+ if hook_result.intercepted:
75
+ current_state = hook_result.to
76
+ error_string = hook_result.error
77
+ event_emmited = hook_result.event
78
+
79
+
80
+ log_data = StateLog(
81
+ session_id=new_session_id,
82
+ traceId=traceId,
83
+ current_state=active_state,
84
+ event= event_emmited,
85
+ error= error_string,
86
+ next_state=current_state,
87
+ time_executed=datetime.now(),
88
+ duration_ms = (time.perf_counter() - start) * 1000
89
+ )
90
+
91
+ _fire_state_log(deps, log_data)
92
+
93
+ active_state = current_state
94
+ return running_context
95
+
96
+ return engine
97
+
98
+
@@ -0,0 +1,43 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema, RuntimeMetadata
2
+ from covalve.infrastructure.contract import InfrastructureRegistry
3
+ from pydantic import ValidationError
4
+ import json
5
+
6
+ def factory_analyzer(deps: InfrastructureRegistry):
7
+ async def handle_analyze(ctx: ArgsCtx) -> ReturnSchema:
8
+ copy_context = ctx.context.model_copy(deep=True)
9
+ prev_summarize = copy_context.background.summarize if copy_context.background else ""
10
+ prev_conv = [conv.model_dump(exclude={'data'}) for conv in copy_context.background.conversation] if ctx.context.background else []
11
+ context_payload = f"""
12
+
13
+ ## Summarize previous conversation
14
+ {prev_summarize}
15
+
16
+ ## Previous Conversation
17
+ {prev_conv}
18
+
19
+ ## Current Query
20
+ {copy_context.query}
21
+
22
+ ## Current Date
23
+ {copy_context.current_time.strftime('%Y-%m-%d')}
24
+ """
25
+
26
+ try:
27
+ metadata: RuntimeMetadata = await deps.llm.analyze(context_payload)
28
+ copy_context.metadata = metadata
29
+ except (ValidationError, json.JSONDecodeError) as e:
30
+ copy_context.error = {"type": "PARSE_ERROR", "detail": str(e)}
31
+ copy_context.last_error_emitted = ctx.state
32
+ return ReturnSchema(event='INTERNAL_ERROR', context=copy_context)
33
+
34
+ confidences = [unit.confidence for unit in metadata.content]
35
+ any_low = any(c < 0.5 for c in confidences)
36
+ mean_low = sum(confidences) / len(confidences) < 0.75
37
+ if any_low or mean_low:
38
+ return ReturnSchema(event='LOW_CONFIDENCE', context=copy_context)
39
+
40
+ return ReturnSchema(event= 'NEXT', context=copy_context)
41
+
42
+ return handle_analyze
43
+
@@ -0,0 +1,14 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema, OutputSchema
2
+ from covalve.runtime.models.io import OutputStatus
3
+ from covalve.infrastructure.contract import InfrastructureRegistry
4
+
5
+ def factory_internal_error(deps:InfrastructureRegistry):
6
+ async def handle_internal_server_error(ctx: ArgsCtx) -> ReturnSchema:
7
+ copy_context = ctx.context.model_copy(deep=True)
8
+ copy_context.response = OutputSchema(
9
+ text="Something went wrong. Please try again later.",
10
+ status=OutputStatus.ERROR,
11
+ traceId=copy_context.traceId
12
+ )
13
+ return ReturnSchema(event="NEXT", context=copy_context)
14
+ return handle_internal_server_error
@@ -0,0 +1,18 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema
2
+ from covalve.infrastructure.contract import InfrastructureRegistry
3
+
4
+ def factory_error_counter(deps:InfrastructureRegistry):
5
+ async def handle_error_counter(ctx: ArgsCtx) -> ReturnSchema:
6
+ copy_context = ctx.context.model_copy(deep=True)
7
+ session_id = copy_context.session_id
8
+ emitter = copy_context.last_error_emitted
9
+ next_event = 'RETRY_TOOLS' if emitter == 'EXECUTE_TOOLS' else 'RETRY_ANALYZE'
10
+ key = f'{session_id}:{emitter}'
11
+ counter = int(await deps.cache.get(key) or 0)
12
+ counter += 1
13
+ await deps.cache.set(key, counter)
14
+ if counter >= 3:
15
+ next_event = 'RETRY_TIMES_OUT'
16
+ return ReturnSchema(event=next_event, context=copy_context)
17
+ return handle_error_counter
18
+
@@ -0,0 +1,20 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema
2
+ from covalve.infrastructure.contract import InfrastructureRegistry
3
+
4
+
5
+ def factory_fallback(deps:InfrastructureRegistry):
6
+ async def handle_fallback(ctx: ArgsCtx) -> ReturnSchema:
7
+ copy_context = ctx.context.model_copy(deep=True)
8
+ content = copy_context.metadata.content if copy_context.metadata else []
9
+ copy_context.is_clarification = True
10
+ confidences = [unit.confidence for unit in content]
11
+
12
+ threshold = 0.5 if any(c < 0.5 for c in confidences) else 0.75
13
+
14
+ copy_context.fallback_content = [
15
+ item for item in content
16
+ if item.confidence < threshold
17
+ ]
18
+
19
+ return ReturnSchema(event="NEXT", context=copy_context)
20
+ return handle_fallback
@@ -0,0 +1,13 @@
1
+ from covalve.infrastructure.contract import InfrastructureRegistry
2
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema
3
+
4
+ def factory_guardrails(deps: InfrastructureRegistry):
5
+ async def handle_guardrails(ctx: ArgsCtx) -> ReturnSchema:
6
+ copy_context = ctx.context.model_copy(deep=True)
7
+ result = await deps.guardrail.validate(copy_context.query,copy_context.background)
8
+ if result.is_rejected:
9
+ copy_context.guardrail_rejection = result.reason
10
+ copy_context.is_clarification = True
11
+ return ReturnSchema(event='OUT_OF_SCOPE', context=copy_context)
12
+ return ReturnSchema(event='NEXT', context=copy_context)
13
+ return handle_guardrails
@@ -0,0 +1,72 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema, OutputSchema
2
+ from covalve.runtime.models.io import OutputStatus, MainLLMResponse, GenerateCondition
3
+ from covalve.infrastructure.contract import InfrastructureRegistry
4
+
5
+ def factory_main_llm(deps: InfrastructureRegistry):
6
+ async def handle_main_llm(ctx: ArgsCtx) -> ReturnSchema:
7
+ copy_context = ctx.context.model_copy(deep=True)
8
+ tools_context = ""
9
+ if copy_context.tools_data:
10
+ for _, data in copy_context.tools_data.items():
11
+ intent_context = next(
12
+ (unit.composition_context for unit in copy_context.metadata.content
13
+ if unit.intent in ['operate', 'lookup', 'validate', 'compare']),
14
+ copy_context.query
15
+ )
16
+ tools_context += f"\n### Data for: '{intent_context}'\n{data}\n"
17
+ is_rejected = True if copy_context.guardrail_rejection else False
18
+ generate_condition = GenerateCondition(
19
+ is_clarification=copy_context.is_clarification,
20
+ is_rejected=is_rejected)
21
+ context_payload = f"""
22
+
23
+ ## Data Hasil Query
24
+ {tools_context}
25
+
26
+ ## Previous Conversation
27
+ {copy_context.background}
28
+
29
+ ## Intent Analysis
30
+ {[unit.model_dump() for unit in copy_context.metadata.content] if copy_context.metadata else []}
31
+
32
+ ## Question
33
+ {copy_context.query}
34
+
35
+ ## Current Date
36
+ {copy_context.current_time.strftime('%Y-%m-%d')}
37
+ """
38
+
39
+ if copy_context.is_clarification:
40
+ clarification_section = f"## Clarification Context\n{copy_context.fallback_content}" if copy_context.fallback_content else ""
41
+ guardrail_section = f"## Out Of Context Reason\n{copy_context.guardrail_rejection}" if copy_context.guardrail_rejection else ""
42
+ context_payload = f"""
43
+
44
+ ## Previous Conversation
45
+ {copy_context.background}
46
+
47
+ {clarification_section}
48
+
49
+ {guardrail_section}
50
+
51
+ ## Question
52
+ {copy_context.query}
53
+
54
+ ## Current Date
55
+ {copy_context.current_time.strftime('%Y-%m-%d')}
56
+
57
+ """
58
+
59
+ result_from_llm: MainLLMResponse = await deps.llm.generate(context_payload, generate_condition)
60
+ copy_context.summarize = result_from_llm.summarize
61
+
62
+ result = OutputSchema(
63
+ text=result_from_llm.text,
64
+ attachment= None,
65
+ status=OutputStatus.CLARIFICATION if copy_context.is_clarification else OutputStatus.SUCCESS,
66
+ traceId=copy_context.traceId
67
+ )
68
+
69
+ copy_context.response = result
70
+ return ReturnSchema(event="NEXT", context=copy_context)
71
+ return handle_main_llm
72
+
@@ -0,0 +1,15 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema, BackgroundUnit
2
+ from covalve.infrastructure.contract import InfrastructureRegistry
3
+
4
+ def factory_retrieve_memmory(deps:InfrastructureRegistry):
5
+ async def handle_retrieve_previous_conversation(ctx: ArgsCtx) -> ReturnSchema:
6
+ copy_context = ctx.context.model_copy(deep=True)
7
+ session_id = copy_context.session_id
8
+ previsous_conv = await deps.memory.retrieve_conv(session_id)
9
+ if previsous_conv is not None:
10
+ copy_context.background = BackgroundUnit(
11
+ summarize=previsous_conv.summarize,
12
+ conversation=previsous_conv.conversation
13
+ )
14
+ return ReturnSchema(event="NEXT", context=copy_context)
15
+ return handle_retrieve_previous_conversation
@@ -0,0 +1,28 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema
2
+ from covalve.runtime.models.io import DataContent
3
+ from covalve.infrastructure.contract import InfrastructureRegistry
4
+ import logging
5
+ logger = logging.getLogger(__name__)
6
+
7
+ def factory_save_data(deps:InfrastructureRegistry):
8
+ async def handle_save_data_to_persistence(ctx: ArgsCtx) -> ReturnSchema:
9
+ session_id = ctx.context.session_id
10
+ data_content = DataContent(
11
+ session_id= session_id,
12
+ metadata= [unit.model_dump() for unit in ctx.context.metadata.content] if ctx.context.metadata else [],
13
+ user= ctx.context.query,
14
+ assistance= ctx.context.response.text if ctx.context.response else "",
15
+ traceId= ctx.context.traceId,
16
+ summarize= ctx.context.summarize,
17
+ data= ctx.context.tools_data
18
+ )
19
+ try:
20
+ await deps.memory.save_conv(data_content)
21
+ except Exception as e:
22
+ logger.warning("failed to save conversation traceId: %s", ctx.context.traceId)
23
+ logger.debug("failed metadata: %s", ctx.context.metadata.model_dump())
24
+ await deps.cache.delete(f"{session_id}:ANALYZE")
25
+ await deps.cache.delete(f"{session_id}:EXECUTE_TOOLS")
26
+
27
+ return ReturnSchema(event="NEXT", context=ctx.context)
28
+ return handle_save_data_to_persistence
@@ -0,0 +1,65 @@
1
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema
2
+ from covalve.infrastructure.contract import InfrastructureRegistry
3
+ import asyncio
4
+ import json
5
+
6
+
7
+
8
+
9
+ def factory_execute_tools(deps:InfrastructureRegistry):
10
+
11
+ def _get_context_for_tool(tool_name: str, metadata_content: list, tools_schema: dict) -> str:
12
+ tool_intents = tools_schema[tool_name]["intent"]
13
+ relevant = [
14
+ unit.composition_context
15
+ for unit in metadata_content
16
+ if unit.intent in tool_intents
17
+ ]
18
+ return " | ".join(relevant) if relevant else ""
19
+
20
+
21
+
22
+ async def handle_execute_tools(ctx: ArgsCtx) -> ReturnSchema:
23
+ tools_schema = ctx.schema_colls.tools_schema
24
+ copy_context = ctx.context.model_copy(deep=True)
25
+ priority_group = copy_context.tool_list
26
+ is_break = False
27
+ event = "NEXT"
28
+ copy_context.tools_data = copy_context.tools_data or {}
29
+ for current_priority in sorted(priority_group):
30
+ tools = priority_group[current_priority]
31
+ tools_to_run = [
32
+ t for t in tools
33
+ if t["name"] not in copy_context.executed_tools.skipped_tools
34
+ and t["name"] not in copy_context.executed_tools.success_tools
35
+ ]
36
+ results = await asyncio.gather(*[
37
+ deps.tools.retrieve(tool["name"], {
38
+ "question":copy_context.metadata.raw_query,
39
+ "context": _get_context_for_tool(
40
+ tool["name"],
41
+ copy_context.metadata.content,
42
+ tools_schema
43
+ )
44
+ }) for tool in tools_to_run
45
+ ], return_exceptions=True)
46
+ for tool, result in zip(tools_to_run, results):
47
+ if isinstance(result, Exception):
48
+ if tool["skippable"]:
49
+ copy_context.executed_tools.skipped_tools.append(tool["name"])
50
+ continue
51
+ is_break = True
52
+ else:
53
+ text = result.content[0].text
54
+ try:
55
+ data = json.loads(text)
56
+ except json.JSONDecodeError:
57
+ data = text
58
+ copy_context.tools_data[tool["name"]] = data
59
+ copy_context.executed_tools.success_tools.append(tool["name"])
60
+ if is_break is True:
61
+ event = "INTERNAL_ERROR"
62
+ copy_context.last_error_emitted = ctx.state
63
+ break
64
+ return ReturnSchema(event=event, context=copy_context)
65
+ return handle_execute_tools
@@ -0,0 +1,18 @@
1
+ from collections import defaultdict
2
+ from covalve.runtime.models.context import ArgsCtx, ReturnSchema
3
+ from covalve.infrastructure.contract import InfrastructureRegistry
4
+
5
+ def factory_tools_mapper(deps: InfrastructureRegistry):
6
+ async def handle_tools_mapper(ctx: ArgsCtx) -> ReturnSchema:
7
+ tools_schema = ctx.schema_colls.tools_schema
8
+ copy_context = ctx.context.model_copy(deep=True)
9
+ content = copy_context.metadata.content
10
+ priority_groups = defaultdict(list)
11
+ for tool_name, tool_config in tools_schema.items():
12
+ for intent in content:
13
+ if intent.intent in tool_config["intent"]:
14
+ priority_groups[tool_config["priority"]].append({"name": tool_name, "skippable": tool_config["skippable"]})
15
+ break
16
+ copy_context.tool_list = dict(priority_groups)
17
+ return ReturnSchema(event="NEXT", context=copy_context)
18
+ return handle_tools_mapper
@@ -0,0 +1,4 @@
1
+ from covalve.runtime.hook.registry import HookRegistry
2
+ from covalve.runtime.hook.context import ReadOnlyContext
3
+
4
+ hooks = HookRegistry()
@@ -0,0 +1,11 @@
1
+ from pydantic import BaseModel, ConfigDict
2
+ from covalve.runtime.models.context import PipelineContext
3
+
4
+ class ReadOnlyContext(PipelineContext):
5
+ model_config = ConfigDict(frozen=True)
6
+
7
+ class HookReturn(BaseModel):
8
+ intercepted: bool
9
+ to: str
10
+ error: str
11
+ event:str
@@ -0,0 +1,32 @@
1
+ from covalve.runtime.models.context import PipelineContext, STOP
2
+ from covalve.runtime.hook.context import ReadOnlyContext,HookReturn
3
+ import asyncio
4
+
5
+
6
+ async def hook_executor(on:str, states, cur_state, hooks: dict, ctx:PipelineContext) -> HookReturn:
7
+ observe_hook = hooks["observer"][on][cur_state]
8
+ intercept_hook = hooks["interceptor"][on][cur_state]
9
+
10
+ copy_context = ReadOnlyContext(**ctx.model_copy(deep=True).model_dump())
11
+
12
+ res = HookReturn(intercepted=False, to="", error="", event="")
13
+
14
+ for fn in observe_hook:
15
+ asyncio.create_task(fn(copy_context))
16
+
17
+ for on_false, fn in intercept_hook:
18
+ try:
19
+ result = await fn(copy_context)
20
+
21
+ if not result:
22
+ res.intercepted = True
23
+ res.to = states[cur_state]["transitions"][on_false]["to"]
24
+ res.event = on_false
25
+ break
26
+ except Exception as e:
27
+ res.intercepted = True
28
+ res.to = STOP.INTERCEPTOR_ERROR
29
+ res.error = str(e)
30
+ res.event = STOP.INTERCEPTOR_ERROR
31
+ return res
32
+
@@ -0,0 +1,43 @@
1
+ from typing import Callable
2
+ from pydantic import BaseModel
3
+ from enum import Enum
4
+
5
+ class HookOn(str, Enum):
6
+ ENTER = "enter"
7
+ EXIT = "exit"
8
+
9
+ class ObserverConfig(BaseModel):
10
+ nodes: list[str]
11
+ on: HookOn
12
+
13
+ class InterceptorConfig(BaseModel):
14
+ node: str
15
+ on: HookOn
16
+ on_false: str
17
+
18
+ class HookRegistry:
19
+ def __init__(self):
20
+ self._observer_registry: list[tuple[ObserverConfig, Callable]] = []
21
+ self._interceptor_registry: list[tuple[InterceptorConfig, Callable]] = []
22
+
23
+ def observer(self, nodes: list[str], on: HookOn):
24
+ def decorator(fn: Callable):
25
+ self._observer_registry.append((ObserverConfig(nodes=nodes, on=on), fn))
26
+ return fn
27
+ return decorator
28
+
29
+ def interceptor(self, node: str, on: HookOn, on_false: str):
30
+ def decorator(fn: Callable):
31
+ self._interceptor_registry.append((InterceptorConfig(nodes=node, on=on, on_false=on_false), fn))
32
+ return fn
33
+ return decorator
34
+
35
+ @property
36
+ def observer_collection(self):
37
+ return self._observer_registry
38
+
39
+ @property
40
+ def interceptor_collection(self):
41
+ return self._interceptor_registry
42
+
43
+