jarviscore-framework 0.1.0__py3-none-any.whl → 0.2.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.
- examples/autoagent_distributed_example.py +211 -0
- examples/custom_profile_decorator.py +134 -0
- examples/custom_profile_wrap.py +168 -0
- examples/customagent_distributed_example.py +362 -0
- examples/customagent_p2p_example.py +347 -0
- jarviscore/__init__.py +60 -15
- jarviscore/adapter/__init__.py +40 -0
- jarviscore/adapter/decorator.py +336 -0
- jarviscore/adapter/wrapper.py +303 -0
- jarviscore/cli/check.py +18 -13
- jarviscore/cli/scaffold.py +178 -0
- jarviscore/cli/smoketest.py +3 -2
- jarviscore/context/__init__.py +40 -0
- jarviscore/context/dependency.py +160 -0
- jarviscore/context/jarvis_context.py +207 -0
- jarviscore/context/memory.py +155 -0
- jarviscore/core/agent.py +44 -1
- jarviscore/core/mesh.py +196 -35
- jarviscore/data/.env.example +146 -0
- jarviscore/data/__init__.py +7 -0
- jarviscore/data/examples/autoagent_distributed_example.py +211 -0
- jarviscore/data/examples/calculator_agent_example.py +77 -0
- jarviscore/data/examples/customagent_distributed_example.py +362 -0
- jarviscore/data/examples/customagent_p2p_example.py +347 -0
- jarviscore/data/examples/multi_agent_workflow.py +132 -0
- jarviscore/data/examples/research_agent_example.py +76 -0
- jarviscore/docs/API_REFERENCE.md +264 -51
- jarviscore/docs/AUTOAGENT_GUIDE.md +198 -0
- jarviscore/docs/CONFIGURATION.md +41 -23
- jarviscore/docs/CUSTOMAGENT_GUIDE.md +415 -0
- jarviscore/docs/GETTING_STARTED.md +113 -17
- jarviscore/docs/TROUBLESHOOTING.md +155 -13
- jarviscore/docs/USER_GUIDE.md +144 -363
- jarviscore/execution/llm.py +23 -16
- jarviscore/orchestration/engine.py +20 -8
- jarviscore/p2p/__init__.py +10 -0
- jarviscore/p2p/coordinator.py +129 -0
- jarviscore/p2p/messages.py +87 -0
- jarviscore/p2p/peer_client.py +576 -0
- jarviscore/p2p/peer_tool.py +268 -0
- jarviscore_framework-0.2.0.dist-info/METADATA +143 -0
- jarviscore_framework-0.2.0.dist-info/RECORD +132 -0
- {jarviscore_framework-0.1.0.dist-info → jarviscore_framework-0.2.0.dist-info}/WHEEL +1 -1
- {jarviscore_framework-0.1.0.dist-info → jarviscore_framework-0.2.0.dist-info}/top_level.txt +1 -0
- test_logs/code_registry/functions/data_generator-558779ed_560ebc37.py +7 -0
- test_logs/code_registry/functions/data_generator-5ed3609e_560ebc37.py +7 -0
- test_logs/code_registry/functions/data_generator-66da0356_43970bb9.py +25 -0
- test_logs/code_registry/functions/data_generator-7a2fac83_583709d9.py +36 -0
- test_logs/code_registry/functions/data_generator-888b670f_aa235863.py +9 -0
- test_logs/code_registry/functions/data_generator-9ca5f642_aa235863.py +9 -0
- test_logs/code_registry/functions/data_generator-bfd90775_560ebc37.py +7 -0
- test_logs/code_registry/functions/data_generator-e95d2f7d_aa235863.py +9 -0
- test_logs/code_registry/functions/data_generator-f60ca8a2_327eb8c2.py +29 -0
- test_logs/code_registry/functions/mathematician-02adf9ee_958658d9.py +19 -0
- test_logs/code_registry/functions/mathematician-0706fb57_5df13441.py +23 -0
- test_logs/code_registry/functions/mathematician-153c9c4a_ba59c918.py +83 -0
- test_logs/code_registry/functions/mathematician-287e61c0_41daa793.py +18 -0
- test_logs/code_registry/functions/mathematician-2967af5a_863c2cc6.py +17 -0
- test_logs/code_registry/functions/mathematician-303ca6d6_5df13441.py +23 -0
- test_logs/code_registry/functions/mathematician-308a4afd_cbf5064d.py +73 -0
- test_logs/code_registry/functions/mathematician-353f16e2_0968bcf5.py +18 -0
- test_logs/code_registry/functions/mathematician-3c22475a_41daa793.py +17 -0
- test_logs/code_registry/functions/mathematician-5bac1029_0968bcf5.py +18 -0
- test_logs/code_registry/functions/mathematician-640f76b2_9198780b.py +19 -0
- test_logs/code_registry/functions/mathematician-752fa7ea_863c2cc6.py +17 -0
- test_logs/code_registry/functions/mathematician-baf9ef39_0968bcf5.py +18 -0
- test_logs/code_registry/functions/mathematician-bc8b2a2f_5df13441.py +23 -0
- test_logs/code_registry/functions/mathematician-c31e4686_41daa793.py +18 -0
- test_logs/code_registry/functions/mathematician-cc84c84c_863c2cc6.py +17 -0
- test_logs/code_registry/functions/mathematician-dd7c7144_9198780b.py +19 -0
- test_logs/code_registry/functions/mathematician-e671c256_41ea4487.py +74 -0
- test_logs/code_registry/functions/report_generator-1a878fcc_18d44bdc.py +47 -0
- test_logs/code_registry/functions/report_generator-25c1c331_cea57d0d.py +35 -0
- test_logs/code_registry/functions/report_generator-37552117_e711c2b9.py +35 -0
- test_logs/code_registry/functions/report_generator-bc662768_e711c2b9.py +35 -0
- test_logs/code_registry/functions/report_generator-d6c0e76b_5e7722ec.py +44 -0
- test_logs/code_registry/functions/report_generator-f270fb02_680529c3.py +44 -0
- test_logs/code_registry/functions/text_processor-11393b14_4370d3ed.py +40 -0
- test_logs/code_registry/functions/text_processor-7d02dfc3_d3b569be.py +37 -0
- test_logs/code_registry/functions/text_processor-8adb5e32_9168c5fe.py +13 -0
- test_logs/code_registry/functions/text_processor-c58ffc19_78b4ceac.py +42 -0
- test_logs/code_registry/functions/text_processor-cd5977b1_9168c5fe.py +13 -0
- test_logs/code_registry/functions/text_processor-ec1c8773_9168c5fe.py +13 -0
- tests/test_01_analyst_standalone.py +124 -0
- tests/test_02_assistant_standalone.py +164 -0
- tests/test_03_analyst_with_framework.py +945 -0
- tests/test_04_assistant_with_framework.py +1002 -0
- tests/test_05_integration.py +1301 -0
- tests/test_06_real_llm_integration.py +760 -0
- tests/test_07_distributed_single_node.py +578 -0
- tests/test_08_distributed_multi_node.py +454 -0
- tests/test_09_distributed_autoagent.py +509 -0
- tests/test_10_distributed_customagent.py +787 -0
- tests/test_context.py +467 -0
- tests/test_decorator.py +622 -0
- tests/test_mesh.py +35 -4
- jarviscore_framework-0.1.0.dist-info/METADATA +0 -136
- jarviscore_framework-0.1.0.dist-info/RECORD +0 -55
- {jarviscore_framework-0.1.0.dist-info → jarviscore_framework-0.2.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@jarvis_agent decorator - Convert any class into a JarvisCore agent.
|
|
3
|
+
|
|
4
|
+
Wraps existing classes (LangChain, CrewAI, raw Python) to work with
|
|
5
|
+
JarvisCore orchestration without requiring them to inherit from CustomAgent.
|
|
6
|
+
"""
|
|
7
|
+
from typing import List, Optional, Type, Any
|
|
8
|
+
import inspect
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from jarviscore.profiles.customagent import CustomAgent
|
|
12
|
+
from jarviscore.context import JarvisContext, create_context
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Common method names to auto-detect
|
|
18
|
+
EXECUTE_METHODS = [
|
|
19
|
+
"run", # Most common
|
|
20
|
+
"invoke", # LangChain
|
|
21
|
+
"execute", # Generic
|
|
22
|
+
"call", # Callable pattern
|
|
23
|
+
"__call__", # Callable objects
|
|
24
|
+
"process", # Processing agents
|
|
25
|
+
"handle", # Handler pattern
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def detect_execute_method(cls: Type) -> Optional[str]:
|
|
30
|
+
"""
|
|
31
|
+
Auto-detect the execute method on a class.
|
|
32
|
+
|
|
33
|
+
Checks for common method names in order of preference.
|
|
34
|
+
Only detects methods defined on the class itself, not inherited
|
|
35
|
+
from object (like __call__ from type).
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
cls: Class to inspect
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Method name or None if not found
|
|
42
|
+
"""
|
|
43
|
+
for method_name in EXECUTE_METHODS:
|
|
44
|
+
# Check if method is defined on the class itself (not inherited from object)
|
|
45
|
+
# We walk the MRO but stop before 'object'
|
|
46
|
+
for klass in cls.__mro__:
|
|
47
|
+
if klass is object:
|
|
48
|
+
break
|
|
49
|
+
if method_name in klass.__dict__:
|
|
50
|
+
attr = getattr(cls, method_name)
|
|
51
|
+
if callable(attr):
|
|
52
|
+
return method_name
|
|
53
|
+
break
|
|
54
|
+
return None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def jarvis_agent(
|
|
58
|
+
role: str,
|
|
59
|
+
capabilities: List[str],
|
|
60
|
+
execute_method: Optional[str] = None
|
|
61
|
+
):
|
|
62
|
+
"""
|
|
63
|
+
Decorator to convert any class into a JarvisCore agent.
|
|
64
|
+
|
|
65
|
+
The decorated class can use any framework (LangChain, CrewAI, raw Python).
|
|
66
|
+
JarvisCore provides orchestration (data handoff, dependencies, memory).
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
role: Agent role identifier (used for step routing)
|
|
70
|
+
capabilities: List of capabilities (used for step matching)
|
|
71
|
+
execute_method: Method name to call for execution (auto-detected if not provided)
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Wrapped class that extends CustomAgent
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
# Simple agent
|
|
78
|
+
@jarvis_agent(role="processor", capabilities=["data_processing"])
|
|
79
|
+
class DataProcessor:
|
|
80
|
+
def run(self, data):
|
|
81
|
+
return {"processed": data * 2}
|
|
82
|
+
|
|
83
|
+
# Agent with context access
|
|
84
|
+
@jarvis_agent(role="aggregator", capabilities=["aggregation"])
|
|
85
|
+
class Aggregator:
|
|
86
|
+
def run(self, task, ctx: JarvisContext):
|
|
87
|
+
prev = ctx.previous("step1")
|
|
88
|
+
return {"result": prev}
|
|
89
|
+
|
|
90
|
+
# Agent with custom method name
|
|
91
|
+
@jarvis_agent(role="researcher", capabilities=["research"], execute_method="invoke")
|
|
92
|
+
class Researcher:
|
|
93
|
+
def invoke(self, query):
|
|
94
|
+
return {"findings": search(query)}
|
|
95
|
+
|
|
96
|
+
Usage:
|
|
97
|
+
mesh = Mesh(mode="autonomous")
|
|
98
|
+
mesh.add(DataProcessor)
|
|
99
|
+
await mesh.start()
|
|
100
|
+
results = await mesh.workflow("pipeline", [...])
|
|
101
|
+
"""
|
|
102
|
+
def decorator(cls: Type) -> Type:
|
|
103
|
+
# Detect execute method if not provided
|
|
104
|
+
method_name = execute_method or detect_execute_method(cls)
|
|
105
|
+
if not method_name:
|
|
106
|
+
raise ValueError(
|
|
107
|
+
f"Could not detect execute method on {cls.__name__}. "
|
|
108
|
+
f"Please specify execute_method parameter or add one of: {EXECUTE_METHODS}"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
# Verify method exists
|
|
112
|
+
if not hasattr(cls, method_name):
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"{cls.__name__} has no method '{method_name}'"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
# Check if method expects context parameter
|
|
118
|
+
method = getattr(cls, method_name)
|
|
119
|
+
sig = inspect.signature(method)
|
|
120
|
+
params = list(sig.parameters.keys())
|
|
121
|
+
|
|
122
|
+
# Remove 'self' from params
|
|
123
|
+
if params and params[0] == 'self':
|
|
124
|
+
params = params[1:]
|
|
125
|
+
|
|
126
|
+
expects_context = 'ctx' in params or 'context' in params
|
|
127
|
+
|
|
128
|
+
logger.debug(
|
|
129
|
+
f"Wrapping {cls.__name__}: method={method_name}, "
|
|
130
|
+
f"expects_context={expects_context}, params={params}"
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Create wrapped CustomAgent subclass
|
|
134
|
+
class WrappedAgent(CustomAgent):
|
|
135
|
+
"""Wrapped agent created by @jarvis_agent decorator."""
|
|
136
|
+
|
|
137
|
+
# Override class attributes from decorator
|
|
138
|
+
# (Can't use nonlocal role/capabilities directly due to scoping)
|
|
139
|
+
pass
|
|
140
|
+
|
|
141
|
+
# Set class attributes (must do this after class creation)
|
|
142
|
+
WrappedAgent.role = role
|
|
143
|
+
WrappedAgent.capabilities = capabilities
|
|
144
|
+
|
|
145
|
+
# Store metadata for introspection
|
|
146
|
+
WrappedAgent._wrapped_class = cls
|
|
147
|
+
WrappedAgent._execute_method = method_name
|
|
148
|
+
WrappedAgent._expects_context = expects_context
|
|
149
|
+
WrappedAgent._original_params = params
|
|
150
|
+
|
|
151
|
+
# Override __init__
|
|
152
|
+
original_init = WrappedAgent.__init__
|
|
153
|
+
|
|
154
|
+
def new_init(self, agent_id=None, **kwargs):
|
|
155
|
+
# Call CustomAgent init
|
|
156
|
+
CustomAgent.__init__(self, agent_id)
|
|
157
|
+
|
|
158
|
+
# Instantiate the wrapped class
|
|
159
|
+
try:
|
|
160
|
+
self._instance = cls(**kwargs) if kwargs else cls()
|
|
161
|
+
except TypeError:
|
|
162
|
+
# Class might not accept kwargs
|
|
163
|
+
self._instance = cls()
|
|
164
|
+
|
|
165
|
+
self._logger.debug(f"Created wrapped instance of {cls.__name__}")
|
|
166
|
+
|
|
167
|
+
WrappedAgent.__init__ = new_init
|
|
168
|
+
|
|
169
|
+
# Override setup
|
|
170
|
+
async def new_setup(self):
|
|
171
|
+
await CustomAgent.setup(self)
|
|
172
|
+
|
|
173
|
+
# Call wrapped class setup if it exists
|
|
174
|
+
if hasattr(self._instance, 'setup'):
|
|
175
|
+
setup_fn = self._instance.setup
|
|
176
|
+
if inspect.iscoroutinefunction(setup_fn):
|
|
177
|
+
await setup_fn()
|
|
178
|
+
else:
|
|
179
|
+
setup_fn()
|
|
180
|
+
|
|
181
|
+
self._logger.debug(f"Setup complete for wrapped {cls.__name__}")
|
|
182
|
+
|
|
183
|
+
WrappedAgent.setup = new_setup
|
|
184
|
+
|
|
185
|
+
# Override execute_task
|
|
186
|
+
async def new_execute_task(self, task: dict) -> dict:
|
|
187
|
+
"""Execute by calling the wrapped class's method."""
|
|
188
|
+
# Get the execute method from instance
|
|
189
|
+
method = getattr(self._instance, method_name)
|
|
190
|
+
|
|
191
|
+
# Get context if method expects it
|
|
192
|
+
ctx = None
|
|
193
|
+
if expects_context:
|
|
194
|
+
# Use pre-injected context from WorkflowEngine if available
|
|
195
|
+
ctx = task.get('_jarvis_context')
|
|
196
|
+
|
|
197
|
+
# Fallback: build context manually (for standalone usage)
|
|
198
|
+
if ctx is None:
|
|
199
|
+
memory_dict = {}
|
|
200
|
+
dep_manager = None
|
|
201
|
+
|
|
202
|
+
if self._mesh:
|
|
203
|
+
engine = getattr(self._mesh, '_workflow_engine', None)
|
|
204
|
+
if engine:
|
|
205
|
+
memory_dict = engine.memory
|
|
206
|
+
dep_manager = getattr(engine, 'dependency_manager', None)
|
|
207
|
+
|
|
208
|
+
ctx = create_context(
|
|
209
|
+
workflow_id=task.get('context', {}).get('workflow_id', 'unknown'),
|
|
210
|
+
step_id=task.get('context', {}).get('step_id', task.get('id', 'unknown')),
|
|
211
|
+
task=task.get('task', ''),
|
|
212
|
+
params=task.get('params', {}),
|
|
213
|
+
memory_dict=memory_dict,
|
|
214
|
+
dependency_manager=dep_manager
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Prepare arguments based on method signature
|
|
218
|
+
args = self._prepare_args(task, ctx, params)
|
|
219
|
+
|
|
220
|
+
# Call the method
|
|
221
|
+
try:
|
|
222
|
+
result = method(*args)
|
|
223
|
+
|
|
224
|
+
# Handle async methods
|
|
225
|
+
if inspect.isawaitable(result):
|
|
226
|
+
result = await result
|
|
227
|
+
|
|
228
|
+
except Exception as e:
|
|
229
|
+
self._logger.error(f"Error in {cls.__name__}.{method_name}: {e}")
|
|
230
|
+
return {
|
|
231
|
+
'status': 'failure',
|
|
232
|
+
'error': str(e),
|
|
233
|
+
'agent': self.agent_id
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
# Normalize result to expected format
|
|
237
|
+
return self._normalize_result(result)
|
|
238
|
+
|
|
239
|
+
WrappedAgent.execute_task = new_execute_task
|
|
240
|
+
|
|
241
|
+
# Helper to prepare arguments
|
|
242
|
+
def prepare_args(self, task: dict, ctx: Optional[JarvisContext], param_names: List[str]) -> tuple:
|
|
243
|
+
"""Prepare arguments for the wrapped method."""
|
|
244
|
+
args = []
|
|
245
|
+
|
|
246
|
+
# Get params, handling both dict and non-dict cases
|
|
247
|
+
task_params = task.get('params', {})
|
|
248
|
+
params_is_dict = isinstance(task_params, dict)
|
|
249
|
+
|
|
250
|
+
for param in param_names:
|
|
251
|
+
if param in ('ctx', 'context'):
|
|
252
|
+
args.append(ctx)
|
|
253
|
+
elif param == 'task':
|
|
254
|
+
args.append(task.get('task', task))
|
|
255
|
+
elif param == 'data':
|
|
256
|
+
# Try to get data from params or context
|
|
257
|
+
if params_is_dict:
|
|
258
|
+
args.append(task_params.get('data', task_params))
|
|
259
|
+
else:
|
|
260
|
+
args.append(task_params)
|
|
261
|
+
elif param == 'params':
|
|
262
|
+
args.append(task_params)
|
|
263
|
+
elif param in ('input', 'query', 'text', 'message'):
|
|
264
|
+
# Common input parameter names
|
|
265
|
+
if params_is_dict:
|
|
266
|
+
args.append(task.get('task', task_params.get(param, '')))
|
|
267
|
+
else:
|
|
268
|
+
args.append(task.get('task', task_params))
|
|
269
|
+
else:
|
|
270
|
+
# Try to get from params, fall back to task dict
|
|
271
|
+
if params_is_dict and param in task_params:
|
|
272
|
+
args.append(task_params[param])
|
|
273
|
+
elif param in task:
|
|
274
|
+
args.append(task[param])
|
|
275
|
+
else:
|
|
276
|
+
# Pass whole params as fallback
|
|
277
|
+
args.append(task_params)
|
|
278
|
+
break # Only do this once
|
|
279
|
+
|
|
280
|
+
# If no args were determined, pass the task/params
|
|
281
|
+
if not args:
|
|
282
|
+
if 'params' in task:
|
|
283
|
+
args.append(task_params)
|
|
284
|
+
else:
|
|
285
|
+
args.append(task)
|
|
286
|
+
|
|
287
|
+
return tuple(args)
|
|
288
|
+
|
|
289
|
+
WrappedAgent._prepare_args = prepare_args
|
|
290
|
+
|
|
291
|
+
# Helper to normalize result
|
|
292
|
+
def normalize_result(self, result: Any) -> dict:
|
|
293
|
+
"""Normalize result to expected format."""
|
|
294
|
+
if isinstance(result, dict):
|
|
295
|
+
if 'status' not in result:
|
|
296
|
+
return {
|
|
297
|
+
'status': 'success',
|
|
298
|
+
'output': result,
|
|
299
|
+
'agent': self.agent_id
|
|
300
|
+
}
|
|
301
|
+
if 'agent' not in result:
|
|
302
|
+
result['agent'] = self.agent_id
|
|
303
|
+
return result
|
|
304
|
+
else:
|
|
305
|
+
return {
|
|
306
|
+
'status': 'success',
|
|
307
|
+
'output': result,
|
|
308
|
+
'agent': self.agent_id
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
WrappedAgent._normalize_result = normalize_result
|
|
312
|
+
|
|
313
|
+
# Override teardown
|
|
314
|
+
async def new_teardown(self):
|
|
315
|
+
# Call wrapped class teardown if it exists
|
|
316
|
+
if hasattr(self._instance, 'teardown'):
|
|
317
|
+
teardown_fn = self._instance.teardown
|
|
318
|
+
if inspect.iscoroutinefunction(teardown_fn):
|
|
319
|
+
await teardown_fn()
|
|
320
|
+
else:
|
|
321
|
+
teardown_fn()
|
|
322
|
+
|
|
323
|
+
await CustomAgent.teardown(self)
|
|
324
|
+
self._logger.debug(f"Teardown complete for wrapped {cls.__name__}")
|
|
325
|
+
|
|
326
|
+
WrappedAgent.teardown = new_teardown
|
|
327
|
+
|
|
328
|
+
# Copy class metadata
|
|
329
|
+
WrappedAgent.__name__ = cls.__name__
|
|
330
|
+
WrappedAgent.__qualname__ = cls.__qualname__
|
|
331
|
+
WrappedAgent.__doc__ = cls.__doc__ or f"Wrapped agent from {cls.__name__}"
|
|
332
|
+
WrappedAgent.__module__ = cls.__module__
|
|
333
|
+
|
|
334
|
+
return WrappedAgent
|
|
335
|
+
|
|
336
|
+
return decorator
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
wrap() function - Wrap an existing instance as a JarvisCore agent.
|
|
3
|
+
|
|
4
|
+
Alternative to @jarvis_agent decorator when you have an existing instance
|
|
5
|
+
rather than a class definition.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
from jarviscore import wrap, Mesh
|
|
9
|
+
|
|
10
|
+
# Your existing agent instance
|
|
11
|
+
my_langchain_agent = LangChainAgent(llm=my_llm)
|
|
12
|
+
|
|
13
|
+
# Wrap it for JarvisCore
|
|
14
|
+
wrapped = wrap(
|
|
15
|
+
my_langchain_agent,
|
|
16
|
+
role="researcher",
|
|
17
|
+
capabilities=["research", "analysis"],
|
|
18
|
+
execute_method="invoke"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
mesh = Mesh(mode="autonomous")
|
|
22
|
+
mesh.add(wrapped)
|
|
23
|
+
await mesh.start()
|
|
24
|
+
"""
|
|
25
|
+
from typing import List, Optional, Any
|
|
26
|
+
import inspect
|
|
27
|
+
import logging
|
|
28
|
+
|
|
29
|
+
from jarviscore.profiles.customagent import CustomAgent
|
|
30
|
+
from jarviscore.context import JarvisContext, create_context
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
# Common method names to auto-detect
|
|
35
|
+
EXECUTE_METHODS = [
|
|
36
|
+
"run", # Most common
|
|
37
|
+
"invoke", # LangChain
|
|
38
|
+
"execute", # Generic
|
|
39
|
+
"call", # Callable pattern
|
|
40
|
+
"__call__", # Callable objects
|
|
41
|
+
"process", # Processing agents
|
|
42
|
+
"handle", # Handler pattern
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def detect_execute_method(instance: Any) -> Optional[str]:
|
|
47
|
+
"""
|
|
48
|
+
Auto-detect the execute method on an instance.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
instance: Object instance to inspect
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
Method name or None if not found
|
|
55
|
+
"""
|
|
56
|
+
for method_name in EXECUTE_METHODS:
|
|
57
|
+
if hasattr(instance, method_name):
|
|
58
|
+
attr = getattr(instance, method_name)
|
|
59
|
+
if callable(attr) and method_name != '__call__':
|
|
60
|
+
return method_name
|
|
61
|
+
elif method_name == '__call__':
|
|
62
|
+
# Check if __call__ is defined on the class itself
|
|
63
|
+
for klass in type(instance).__mro__:
|
|
64
|
+
if klass is object:
|
|
65
|
+
break
|
|
66
|
+
if '__call__' in klass.__dict__:
|
|
67
|
+
return method_name
|
|
68
|
+
return None
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def wrap(
|
|
72
|
+
instance: Any,
|
|
73
|
+
role: str,
|
|
74
|
+
capabilities: List[str],
|
|
75
|
+
execute_method: Optional[str] = None
|
|
76
|
+
) -> CustomAgent:
|
|
77
|
+
"""
|
|
78
|
+
Wrap an existing instance as a JarvisCore agent.
|
|
79
|
+
|
|
80
|
+
Use this when you have an already-instantiated object (like a LangChain
|
|
81
|
+
agent or custom class instance) that you want to use with JarvisCore.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
instance: The object instance to wrap
|
|
85
|
+
role: Agent role identifier (used for step routing)
|
|
86
|
+
capabilities: List of capabilities (used for step matching)
|
|
87
|
+
execute_method: Method name to call for execution (auto-detected if not provided)
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
A CustomAgent instance wrapping the original object
|
|
91
|
+
|
|
92
|
+
Example:
|
|
93
|
+
# Wrap a LangChain agent
|
|
94
|
+
from langchain.agents import AgentExecutor
|
|
95
|
+
|
|
96
|
+
langchain_agent = AgentExecutor(agent=my_agent, tools=my_tools)
|
|
97
|
+
|
|
98
|
+
wrapped = wrap(
|
|
99
|
+
langchain_agent,
|
|
100
|
+
role="assistant",
|
|
101
|
+
capabilities=["chat", "tools"],
|
|
102
|
+
execute_method="invoke"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
mesh.add(wrapped)
|
|
106
|
+
|
|
107
|
+
# Wrap a simple Python object
|
|
108
|
+
class MyProcessor:
|
|
109
|
+
def run(self, data):
|
|
110
|
+
return {"processed": data * 2}
|
|
111
|
+
|
|
112
|
+
processor = MyProcessor()
|
|
113
|
+
wrapped = wrap(processor, role="processor", capabilities=["processing"])
|
|
114
|
+
|
|
115
|
+
# Wrap with context access
|
|
116
|
+
class MyAggregator:
|
|
117
|
+
def run(self, task, ctx: JarvisContext):
|
|
118
|
+
prev = ctx.previous("step1")
|
|
119
|
+
return {"aggregated": prev}
|
|
120
|
+
|
|
121
|
+
aggregator = MyAggregator()
|
|
122
|
+
wrapped = wrap(aggregator, role="aggregator", capabilities=["aggregation"])
|
|
123
|
+
"""
|
|
124
|
+
# Detect execute method if not provided
|
|
125
|
+
method_name = execute_method or detect_execute_method(instance)
|
|
126
|
+
if not method_name:
|
|
127
|
+
raise ValueError(
|
|
128
|
+
f"Could not detect execute method on {type(instance).__name__}. "
|
|
129
|
+
f"Please specify execute_method parameter or add one of: {EXECUTE_METHODS}"
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Verify method exists
|
|
133
|
+
if not hasattr(instance, method_name):
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"{type(instance).__name__} has no method '{method_name}'"
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Check if method expects context parameter
|
|
139
|
+
method = getattr(instance, method_name)
|
|
140
|
+
sig = inspect.signature(method)
|
|
141
|
+
params = list(sig.parameters.keys())
|
|
142
|
+
|
|
143
|
+
# Remove 'self' from params (shouldn't be there for bound methods, but just in case)
|
|
144
|
+
if params and params[0] == 'self':
|
|
145
|
+
params = params[1:]
|
|
146
|
+
|
|
147
|
+
expects_context = 'ctx' in params or 'context' in params
|
|
148
|
+
|
|
149
|
+
logger.debug(
|
|
150
|
+
f"Wrapping instance {type(instance).__name__}: method={method_name}, "
|
|
151
|
+
f"expects_context={expects_context}, params={params}"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# Create a CustomAgent subclass dynamically
|
|
155
|
+
class WrappedInstanceAgent(CustomAgent):
|
|
156
|
+
"""Agent wrapping an existing instance via wrap()."""
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
# Set class attributes
|
|
160
|
+
WrappedInstanceAgent.role = role
|
|
161
|
+
WrappedInstanceAgent.capabilities = capabilities
|
|
162
|
+
|
|
163
|
+
# Store metadata
|
|
164
|
+
WrappedInstanceAgent._wrapped_instance = instance
|
|
165
|
+
WrappedInstanceAgent._execute_method = method_name
|
|
166
|
+
WrappedInstanceAgent._expects_context = expects_context
|
|
167
|
+
WrappedInstanceAgent._original_params = params
|
|
168
|
+
|
|
169
|
+
# Create the agent instance
|
|
170
|
+
agent = WrappedInstanceAgent.__new__(WrappedInstanceAgent)
|
|
171
|
+
|
|
172
|
+
# Initialize CustomAgent parts
|
|
173
|
+
CustomAgent.__init__(agent)
|
|
174
|
+
|
|
175
|
+
# Store the wrapped instance
|
|
176
|
+
agent._instance = instance
|
|
177
|
+
|
|
178
|
+
# Override execute_task
|
|
179
|
+
async def execute_task(self, task: dict) -> dict:
|
|
180
|
+
"""Execute by calling the wrapped instance's method."""
|
|
181
|
+
method = getattr(self._instance, method_name)
|
|
182
|
+
|
|
183
|
+
# Get context if method expects it
|
|
184
|
+
ctx = None
|
|
185
|
+
if expects_context:
|
|
186
|
+
# Use pre-injected context from WorkflowEngine if available
|
|
187
|
+
ctx = task.get('_jarvis_context')
|
|
188
|
+
|
|
189
|
+
# Fallback: build context manually (for standalone usage)
|
|
190
|
+
if ctx is None:
|
|
191
|
+
memory_dict = {}
|
|
192
|
+
dep_manager = None
|
|
193
|
+
|
|
194
|
+
if self._mesh:
|
|
195
|
+
engine = getattr(self._mesh, '_workflow_engine', None)
|
|
196
|
+
if engine:
|
|
197
|
+
memory_dict = engine.memory
|
|
198
|
+
dep_manager = getattr(engine, 'dependency_manager', None)
|
|
199
|
+
|
|
200
|
+
ctx = create_context(
|
|
201
|
+
workflow_id=task.get('context', {}).get('workflow_id', 'unknown'),
|
|
202
|
+
step_id=task.get('context', {}).get('step_id', task.get('id', 'unknown')),
|
|
203
|
+
task=task.get('task', ''),
|
|
204
|
+
params=task.get('params', {}),
|
|
205
|
+
memory_dict=memory_dict,
|
|
206
|
+
dependency_manager=dep_manager
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Prepare arguments
|
|
210
|
+
args = _prepare_args(task, ctx, params)
|
|
211
|
+
|
|
212
|
+
# Call the method
|
|
213
|
+
try:
|
|
214
|
+
result = method(*args)
|
|
215
|
+
|
|
216
|
+
# Handle async methods
|
|
217
|
+
if inspect.isawaitable(result):
|
|
218
|
+
result = await result
|
|
219
|
+
|
|
220
|
+
except Exception as e:
|
|
221
|
+
logger.error(f"Error in {type(instance).__name__}.{method_name}: {e}")
|
|
222
|
+
return {
|
|
223
|
+
'status': 'failure',
|
|
224
|
+
'error': str(e),
|
|
225
|
+
'agent': self.agent_id
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
# Normalize result
|
|
229
|
+
return _normalize_result(self, result)
|
|
230
|
+
|
|
231
|
+
# Bind the method to the agent
|
|
232
|
+
import types
|
|
233
|
+
agent.execute_task = types.MethodType(execute_task, agent)
|
|
234
|
+
|
|
235
|
+
# Copy class name for better debugging
|
|
236
|
+
WrappedInstanceAgent.__name__ = f"Wrapped_{type(instance).__name__}"
|
|
237
|
+
WrappedInstanceAgent.__qualname__ = f"Wrapped_{type(instance).__name__}"
|
|
238
|
+
|
|
239
|
+
logger.debug(f"Created wrapped agent: role={role}, instance={type(instance).__name__}")
|
|
240
|
+
|
|
241
|
+
return agent
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _prepare_args(task: dict, ctx: Optional[JarvisContext], param_names: List[str]) -> tuple:
|
|
245
|
+
"""Prepare arguments for the wrapped method."""
|
|
246
|
+
args = []
|
|
247
|
+
|
|
248
|
+
task_params = task.get('params', {})
|
|
249
|
+
params_is_dict = isinstance(task_params, dict)
|
|
250
|
+
|
|
251
|
+
for param in param_names:
|
|
252
|
+
if param in ('ctx', 'context'):
|
|
253
|
+
args.append(ctx)
|
|
254
|
+
elif param == 'task':
|
|
255
|
+
args.append(task.get('task', task))
|
|
256
|
+
elif param == 'data':
|
|
257
|
+
if params_is_dict:
|
|
258
|
+
args.append(task_params.get('data', task_params))
|
|
259
|
+
else:
|
|
260
|
+
args.append(task_params)
|
|
261
|
+
elif param == 'params':
|
|
262
|
+
args.append(task_params)
|
|
263
|
+
elif param in ('input', 'query', 'text', 'message'):
|
|
264
|
+
if params_is_dict:
|
|
265
|
+
args.append(task.get('task', task_params.get(param, '')))
|
|
266
|
+
else:
|
|
267
|
+
args.append(task.get('task', task_params))
|
|
268
|
+
else:
|
|
269
|
+
if params_is_dict and param in task_params:
|
|
270
|
+
args.append(task_params[param])
|
|
271
|
+
elif param in task:
|
|
272
|
+
args.append(task[param])
|
|
273
|
+
else:
|
|
274
|
+
args.append(task_params)
|
|
275
|
+
break
|
|
276
|
+
|
|
277
|
+
if not args:
|
|
278
|
+
if 'params' in task:
|
|
279
|
+
args.append(task_params)
|
|
280
|
+
else:
|
|
281
|
+
args.append(task)
|
|
282
|
+
|
|
283
|
+
return tuple(args)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _normalize_result(agent: CustomAgent, result: Any) -> dict:
|
|
287
|
+
"""Normalize result to expected format."""
|
|
288
|
+
if isinstance(result, dict):
|
|
289
|
+
if 'status' not in result:
|
|
290
|
+
return {
|
|
291
|
+
'status': 'success',
|
|
292
|
+
'output': result,
|
|
293
|
+
'agent': agent.agent_id
|
|
294
|
+
}
|
|
295
|
+
if 'agent' not in result:
|
|
296
|
+
result['agent'] = agent.agent_id
|
|
297
|
+
return result
|
|
298
|
+
else:
|
|
299
|
+
return {
|
|
300
|
+
'status': 'success',
|
|
301
|
+
'output': result,
|
|
302
|
+
'agent': agent.agent_id
|
|
303
|
+
}
|
jarviscore/cli/check.py
CHANGED
|
@@ -222,14 +222,17 @@ class HealthChecker:
|
|
|
222
222
|
return False
|
|
223
223
|
|
|
224
224
|
async def _test_gemini(self) -> bool:
|
|
225
|
-
"""Test Gemini connectivity."""
|
|
225
|
+
"""Test Gemini connectivity using the new google.genai SDK."""
|
|
226
226
|
try:
|
|
227
|
-
|
|
227
|
+
from google import genai
|
|
228
228
|
|
|
229
|
-
genai.
|
|
230
|
-
|
|
229
|
+
client = genai.Client(api_key=os.getenv('GEMINI_API_KEY'))
|
|
230
|
+
model_name = os.getenv('GEMINI_MODEL', 'gemini-2.0-flash')
|
|
231
231
|
|
|
232
|
-
response = await
|
|
232
|
+
response = await client.aio.models.generate_content(
|
|
233
|
+
model=model_name,
|
|
234
|
+
contents="Reply with just 'OK'"
|
|
235
|
+
)
|
|
233
236
|
return 'OK' in response.text.upper()
|
|
234
237
|
|
|
235
238
|
except Exception:
|
|
@@ -291,13 +294,15 @@ class HealthChecker:
|
|
|
291
294
|
print("\n" + "="*70)
|
|
292
295
|
print(" Next Steps")
|
|
293
296
|
print("="*70)
|
|
294
|
-
print("\n1.
|
|
297
|
+
print("\n1. Initialize project (creates .env.example and examples):")
|
|
298
|
+
print(" python -m jarviscore.cli.scaffold --examples")
|
|
299
|
+
print("\n2. Configure your environment:")
|
|
295
300
|
print(" cp .env.example .env")
|
|
296
|
-
print("
|
|
297
|
-
print("
|
|
298
|
-
print("
|
|
299
|
-
print("
|
|
300
|
-
print("
|
|
301
|
+
print(" # Edit .env and add one of:")
|
|
302
|
+
print(" # CLAUDE_API_KEY=sk-ant-...")
|
|
303
|
+
print(" # AZURE_API_KEY=...")
|
|
304
|
+
print(" # GEMINI_API_KEY=...")
|
|
305
|
+
print(" # LLM_ENDPOINT=http://localhost:8000 (for local vLLM)")
|
|
301
306
|
print("\n3. Run health check again:")
|
|
302
307
|
print(" python -m jarviscore.cli.check --validate-llm")
|
|
303
308
|
print("\n4. Try the smoke test:")
|
|
@@ -309,8 +314,8 @@ class HealthChecker:
|
|
|
309
314
|
print("\n✓ All checks passed! Ready to use JarvisCore.\n")
|
|
310
315
|
print("Next steps:")
|
|
311
316
|
print(" 1. Run smoke test: python -m jarviscore.cli.smoketest")
|
|
312
|
-
print(" 2.
|
|
313
|
-
print(" 3.
|
|
317
|
+
print(" 2. Get examples: python -m jarviscore.cli.scaffold --examples")
|
|
318
|
+
print(" 3. Try example: python examples/calculator_agent_example.py")
|
|
314
319
|
print()
|
|
315
320
|
|
|
316
321
|
return True
|