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.
- covalve/__init__.py +35 -0
- covalve/infrastructure/base/cache.py +12 -0
- covalve/infrastructure/base/guardrails.py +6 -0
- covalve/infrastructure/base/llm.py +11 -0
- covalve/infrastructure/base/log.py +7 -0
- covalve/infrastructure/base/memory.py +10 -0
- covalve/infrastructure/base/tools.py +7 -0
- covalve/infrastructure/contract.py +20 -0
- covalve/runtime/engine.py +98 -0
- covalve/runtime/executor/analyze_query.py +43 -0
- covalve/runtime/executor/error.py +14 -0
- covalve/runtime/executor/error_counter.py +18 -0
- covalve/runtime/executor/fallback.py +20 -0
- covalve/runtime/executor/guardrail.py +13 -0
- covalve/runtime/executor/main_llm.py +72 -0
- covalve/runtime/executor/retrieve_conv.py +15 -0
- covalve/runtime/executor/save_data.py +28 -0
- covalve/runtime/executor/tools_executor.py +65 -0
- covalve/runtime/executor/tools_mapper.py +18 -0
- covalve/runtime/hook/__init__.py +4 -0
- covalve/runtime/hook/context.py +11 -0
- covalve/runtime/hook/executor.py +32 -0
- covalve/runtime/hook/registry.py +43 -0
- covalve/runtime/init.py +76 -0
- covalve/runtime/models/context.py +60 -0
- covalve/runtime/models/infra.py +25 -0
- covalve/runtime/models/io.py +36 -0
- covalve/runtime/models/logs.py +14 -0
- covalve/runtime/models/metadata.py +33 -0
- covalve/runtime/pipeline.py +31 -0
- covalve/runtime/registry.py +23 -0
- covalve/runtime/validator/graph_traversal.py +44 -0
- covalve/schemas/__init__.py +0 -0
- covalve/schemas/schema.json +56 -0
- covalve-0.1.0.dist-info/METADATA +298 -0
- covalve-0.1.0.dist-info/RECORD +38 -0
- covalve-0.1.0.dist-info/WHEEL +4 -0
- 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,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,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,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
|
+
|