mirror of
https://github.com/crewAIInc/crewAI.git
synced 2025-12-18 13:28:31 +00:00
Compare commits
8 Commits
bugfix/all
...
bugfix/asy
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
89f7435373 | ||
|
|
ad030d5eec | ||
|
|
00b6ce94dc | ||
|
|
b50772a38b | ||
|
|
96a7e8038f | ||
|
|
ec050e5d33 | ||
|
|
e2ce65fc5b | ||
|
|
14503bc43b |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -21,4 +21,5 @@ crew_tasks_output.json
|
|||||||
.mypy_cache
|
.mypy_cache
|
||||||
.ruff_cache
|
.ruff_cache
|
||||||
.venv
|
.venv
|
||||||
agentops.log
|
agentops.log
|
||||||
|
test_flow.html
|
||||||
@@ -10,6 +10,8 @@ This notebook demonstrates how to integrate **Langfuse** with **CrewAI** using O
|
|||||||
|
|
||||||
> **What is Langfuse?** [Langfuse](https://langfuse.com) is an open-source LLM engineering platform. It provides tracing and monitoring capabilities for LLM applications, helping developers debug, analyze, and optimize their AI systems. Langfuse integrates with various tools and frameworks via native integrations, OpenTelemetry, and APIs/SDKs.
|
> **What is Langfuse?** [Langfuse](https://langfuse.com) is an open-source LLM engineering platform. It provides tracing and monitoring capabilities for LLM applications, helping developers debug, analyze, and optimize their AI systems. Langfuse integrates with various tools and frameworks via native integrations, OpenTelemetry, and APIs/SDKs.
|
||||||
|
|
||||||
|
[](https://langfuse.com/watch-demo)
|
||||||
|
|
||||||
## Get Started
|
## Get Started
|
||||||
|
|
||||||
We'll walk through a simple example of using CrewAI and integrating it with Langfuse via OpenTelemetry using OpenLit.
|
We'll walk through a simple example of using CrewAI and integrating it with Langfuse via OpenTelemetry using OpenLit.
|
||||||
|
|||||||
@@ -31,11 +31,11 @@ class OutputConverter(BaseModel, ABC):
|
|||||||
)
|
)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_pydantic(self, current_attempt=1):
|
def to_pydantic(self, current_attempt=1) -> BaseModel:
|
||||||
"""Convert text to pydantic."""
|
"""Convert text to pydantic."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def to_json(self, current_attempt=1):
|
def to_json(self, current_attempt=1) -> dict:
|
||||||
"""Convert text to json."""
|
"""Convert text to json."""
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -713,16 +713,35 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
raise TypeError(f"State must be dict or BaseModel, got {type(self._state)}")
|
raise TypeError(f"State must be dict or BaseModel, got {type(self._state)}")
|
||||||
|
|
||||||
def kickoff(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
|
def kickoff(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
|
||||||
"""Start the flow execution.
|
"""
|
||||||
|
Start the flow execution in a synchronous context.
|
||||||
|
|
||||||
|
This method wraps kickoff_async so that all state initialization and event
|
||||||
|
emission is handled in the asynchronous method.
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def run_flow():
|
||||||
|
return await self.kickoff_async(inputs)
|
||||||
|
|
||||||
|
return asyncio.run(run_flow())
|
||||||
|
|
||||||
|
@init_flow_main_trace
|
||||||
|
async def kickoff_async(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
|
||||||
|
"""
|
||||||
|
Start the flow execution asynchronously.
|
||||||
|
|
||||||
|
This method performs state restoration (if an 'id' is provided and persistence is available)
|
||||||
|
and updates the flow state with any additional inputs. It then emits the FlowStartedEvent,
|
||||||
|
logs the flow startup, and executes all start methods. Once completed, it emits the
|
||||||
|
FlowFinishedEvent and returns the final output.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
inputs: Optional dictionary containing input values and potentially a state ID to restore
|
inputs: Optional dictionary containing input values and/or a state ID for restoration.
|
||||||
"""
|
|
||||||
# Handle state restoration if ID is provided in inputs
|
|
||||||
if inputs and "id" in inputs and self._persistence is not None:
|
|
||||||
restore_uuid = inputs["id"]
|
|
||||||
stored_state = self._persistence.load_state(restore_uuid)
|
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The final output from the flow, which is the result of the last executed method.
|
||||||
|
"""
|
||||||
|
if inputs:
|
||||||
# Override the id in the state if it exists in inputs
|
# Override the id in the state if it exists in inputs
|
||||||
if "id" in inputs:
|
if "id" in inputs:
|
||||||
if isinstance(self._state, dict):
|
if isinstance(self._state, dict):
|
||||||
@@ -730,24 +749,27 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
elif isinstance(self._state, BaseModel):
|
elif isinstance(self._state, BaseModel):
|
||||||
setattr(self._state, "id", inputs["id"])
|
setattr(self._state, "id", inputs["id"])
|
||||||
|
|
||||||
if stored_state:
|
# If persistence is enabled, attempt to restore the stored state using the provided id.
|
||||||
self._log_flow_event(
|
if "id" in inputs and self._persistence is not None:
|
||||||
f"Loading flow state from memory for UUID: {restore_uuid}",
|
restore_uuid = inputs["id"]
|
||||||
color="yellow",
|
stored_state = self._persistence.load_state(restore_uuid)
|
||||||
)
|
if stored_state:
|
||||||
# Restore the state
|
self._log_flow_event(
|
||||||
self._restore_state(stored_state)
|
f"Loading flow state from memory for UUID: {restore_uuid}",
|
||||||
else:
|
color="yellow",
|
||||||
self._log_flow_event(
|
)
|
||||||
f"No flow state found for UUID: {restore_uuid}", color="red"
|
self._restore_state(stored_state)
|
||||||
)
|
else:
|
||||||
|
self._log_flow_event(
|
||||||
|
f"No flow state found for UUID: {restore_uuid}", color="red"
|
||||||
|
)
|
||||||
|
|
||||||
# Apply any additional inputs after restoration
|
# Update state with any additional inputs (ignoring the 'id' key)
|
||||||
filtered_inputs = {k: v for k, v in inputs.items() if k != "id"}
|
filtered_inputs = {k: v for k, v in inputs.items() if k != "id"}
|
||||||
if filtered_inputs:
|
if filtered_inputs:
|
||||||
self._initialize_state(filtered_inputs)
|
self._initialize_state(filtered_inputs)
|
||||||
|
|
||||||
# Start flow execution
|
# Emit FlowStartedEvent and log the start of the flow.
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
self,
|
self,
|
||||||
FlowStartedEvent(
|
FlowStartedEvent(
|
||||||
@@ -760,27 +782,18 @@ class Flow(Generic[T], metaclass=FlowMeta):
|
|||||||
f"Flow started with ID: {self.flow_id}", color="bold_magenta"
|
f"Flow started with ID: {self.flow_id}", color="bold_magenta"
|
||||||
)
|
)
|
||||||
|
|
||||||
if inputs is not None and "id" not in inputs:
|
|
||||||
self._initialize_state(inputs)
|
|
||||||
|
|
||||||
async def run_flow():
|
|
||||||
return await self.kickoff_async()
|
|
||||||
|
|
||||||
return asyncio.run(run_flow())
|
|
||||||
|
|
||||||
@init_flow_main_trace
|
|
||||||
async def kickoff_async(self, inputs: Optional[Dict[str, Any]] = None) -> Any:
|
|
||||||
if not self._start_methods:
|
if not self._start_methods:
|
||||||
raise ValueError("No start method defined")
|
raise ValueError("No start method defined")
|
||||||
|
|
||||||
|
# Execute all start methods concurrently.
|
||||||
tasks = [
|
tasks = [
|
||||||
self._execute_start_method(start_method)
|
self._execute_start_method(start_method)
|
||||||
for start_method in self._start_methods
|
for start_method in self._start_methods
|
||||||
]
|
]
|
||||||
await asyncio.gather(*tasks)
|
await asyncio.gather(*tasks)
|
||||||
|
|
||||||
final_output = self._method_outputs[-1] if self._method_outputs else None
|
final_output = self._method_outputs[-1] if self._method_outputs else None
|
||||||
|
|
||||||
|
# Emit FlowFinishedEvent after all processing is complete.
|
||||||
crewai_event_bus.emit(
|
crewai_event_bus.emit(
|
||||||
self,
|
self,
|
||||||
FlowFinishedEvent(
|
FlowFinishedEvent(
|
||||||
|
|||||||
@@ -26,9 +26,9 @@ from crewai.utilities.events.tool_usage_events import ToolExecutionErrorEvent
|
|||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore", UserWarning)
|
warnings.simplefilter("ignore", UserWarning)
|
||||||
import litellm
|
import litellm
|
||||||
from litellm import Choices, get_supported_openai_params
|
from litellm import Choices
|
||||||
from litellm.types.utils import ModelResponse
|
from litellm.types.utils import ModelResponse
|
||||||
from litellm.utils import supports_response_schema
|
from litellm.utils import get_supported_openai_params, supports_response_schema
|
||||||
|
|
||||||
|
|
||||||
from crewai.traces.unified_trace_controller import trace_llm_call
|
from crewai.traces.unified_trace_controller import trace_llm_call
|
||||||
@@ -449,7 +449,7 @@ class LLM:
|
|||||||
def supports_function_calling(self) -> bool:
|
def supports_function_calling(self) -> bool:
|
||||||
try:
|
try:
|
||||||
params = get_supported_openai_params(model=self.model)
|
params = get_supported_openai_params(model=self.model)
|
||||||
return "response_format" in params
|
return params is not None and "tools" in params
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to get supported params: {str(e)}")
|
logging.error(f"Failed to get supported params: {str(e)}")
|
||||||
return False
|
return False
|
||||||
@@ -457,7 +457,7 @@ class LLM:
|
|||||||
def supports_stop_words(self) -> bool:
|
def supports_stop_words(self) -> bool:
|
||||||
try:
|
try:
|
||||||
params = get_supported_openai_params(model=self.model)
|
params = get_supported_openai_params(model=self.model)
|
||||||
return "stop" in params
|
return params is not None and "stop" in params
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logging.error(f"Failed to get supported params: {str(e)}")
|
logging.error(f"Failed to get supported params: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|||||||
@@ -20,11 +20,11 @@ class ConverterError(Exception):
|
|||||||
class Converter(OutputConverter):
|
class Converter(OutputConverter):
|
||||||
"""Class that converts text into either pydantic or json."""
|
"""Class that converts text into either pydantic or json."""
|
||||||
|
|
||||||
def to_pydantic(self, current_attempt=1):
|
def to_pydantic(self, current_attempt=1) -> BaseModel:
|
||||||
"""Convert text to pydantic."""
|
"""Convert text to pydantic."""
|
||||||
try:
|
try:
|
||||||
if self.llm.supports_function_calling():
|
if self.llm.supports_function_calling():
|
||||||
return self._create_instructor().to_pydantic()
|
result = self._create_instructor().to_pydantic()
|
||||||
else:
|
else:
|
||||||
response = self.llm.call(
|
response = self.llm.call(
|
||||||
[
|
[
|
||||||
@@ -32,18 +32,40 @@ class Converter(OutputConverter):
|
|||||||
{"role": "user", "content": self.text},
|
{"role": "user", "content": self.text},
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
return self.model.model_validate_json(response)
|
try:
|
||||||
|
# Try to directly validate the response JSON
|
||||||
|
result = self.model.model_validate_json(response)
|
||||||
|
except ValidationError:
|
||||||
|
# If direct validation fails, attempt to extract valid JSON
|
||||||
|
result = handle_partial_json(response, self.model, False, None)
|
||||||
|
# Ensure result is a BaseModel instance
|
||||||
|
if not isinstance(result, BaseModel):
|
||||||
|
if isinstance(result, dict):
|
||||||
|
result = self.model.parse_obj(result)
|
||||||
|
elif isinstance(result, str):
|
||||||
|
try:
|
||||||
|
parsed = json.loads(result)
|
||||||
|
result = self.model.parse_obj(parsed)
|
||||||
|
except Exception as parse_err:
|
||||||
|
raise ConverterError(
|
||||||
|
f"Failed to convert partial JSON result into Pydantic: {parse_err}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise ConverterError(
|
||||||
|
"handle_partial_json returned an unexpected type."
|
||||||
|
)
|
||||||
|
return result
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
if current_attempt < self.max_attempts:
|
if current_attempt < self.max_attempts:
|
||||||
return self.to_pydantic(current_attempt + 1)
|
return self.to_pydantic(current_attempt + 1)
|
||||||
raise ConverterError(
|
raise ConverterError(
|
||||||
f"Failed to convert text into a Pydantic model due to the following validation error: {e}"
|
f"Failed to convert text into a Pydantic model due to validation error: {e}"
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if current_attempt < self.max_attempts:
|
if current_attempt < self.max_attempts:
|
||||||
return self.to_pydantic(current_attempt + 1)
|
return self.to_pydantic(current_attempt + 1)
|
||||||
raise ConverterError(
|
raise ConverterError(
|
||||||
f"Failed to convert text into a Pydantic model due to the following error: {e}"
|
f"Failed to convert text into a Pydantic model due to error: {e}"
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_json(self, current_attempt=1):
|
def to_json(self, current_attempt=1):
|
||||||
@@ -197,11 +219,15 @@ def get_conversion_instructions(model: Type[BaseModel], llm: Any) -> str:
|
|||||||
if llm.supports_function_calling():
|
if llm.supports_function_calling():
|
||||||
model_schema = PydanticSchemaParser(model=model).get_schema()
|
model_schema = PydanticSchemaParser(model=model).get_schema()
|
||||||
instructions += (
|
instructions += (
|
||||||
f"\n\nThe JSON should follow this schema:\n```json\n{model_schema}\n```"
|
f"\n\nOutput ONLY the valid JSON and nothing else.\n\n"
|
||||||
|
f"The JSON must follow this schema exactly:\n```json\n{model_schema}\n```"
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
model_description = generate_model_description(model)
|
model_description = generate_model_description(model)
|
||||||
instructions += f"\n\nThe JSON should follow this format:\n{model_description}"
|
instructions += (
|
||||||
|
f"\n\nOutput ONLY the valid JSON and nothing else.\n\n"
|
||||||
|
f"The JSON must follow this format exactly:\n{model_description}"
|
||||||
|
)
|
||||||
return instructions
|
return instructions
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,9 @@
|
|||||||
interactions:
|
interactions:
|
||||||
- request:
|
- request:
|
||||||
body: '{"model": "llama3.2:3b", "prompt": "### User:\nName: Alice Llama, Age:
|
body: '{"model": "llama3.2:3b", "prompt": "### System:\nPlease convert the following
|
||||||
30\n\n### System:\nProduce JSON OUTPUT ONLY! Adhere to this format {\"name\":
|
text into valid JSON.\n\nOutput ONLY the valid JSON and nothing else.\n\nThe
|
||||||
\"function_name\", \"arguments\":{\"argument_name\": \"argument_value\"}} The
|
JSON must follow this format exactly:\n{\n \"name\": str,\n \"age\": int\n}\n\n###
|
||||||
following functions are available to you:\n{''type'': ''function'', ''function'':
|
User:\nName: Alice Llama, Age: 30\n\n", "options": {"stop": []}, "stream": false}'
|
||||||
{''name'': ''SimpleModel'', ''description'': ''Correctly extracted `SimpleModel`
|
|
||||||
with all the required parameters with correct types'', ''parameters'': {''properties'':
|
|
||||||
{''name'': {''title'': ''Name'', ''type'': ''string''}, ''age'': {''title'':
|
|
||||||
''Age'', ''type'': ''integer''}}, ''required'': [''age'', ''name''], ''type'':
|
|
||||||
''object''}}}\n\n\n", "options": {}, "stream": false, "format": "json"}'
|
|
||||||
headers:
|
headers:
|
||||||
accept:
|
accept:
|
||||||
- '*/*'
|
- '*/*'
|
||||||
@@ -17,23 +12,23 @@ interactions:
|
|||||||
connection:
|
connection:
|
||||||
- keep-alive
|
- keep-alive
|
||||||
content-length:
|
content-length:
|
||||||
- '657'
|
- '321'
|
||||||
host:
|
host:
|
||||||
- localhost:11434
|
- localhost:11434
|
||||||
user-agent:
|
user-agent:
|
||||||
- litellm/1.57.4
|
- litellm/1.60.2
|
||||||
method: POST
|
method: POST
|
||||||
uri: http://localhost:11434/api/generate
|
uri: http://localhost:11434/api/generate
|
||||||
response:
|
response:
|
||||||
content: '{"model":"llama3.2:3b","created_at":"2025-01-15T20:47:11.926411Z","response":"{\"name\":
|
content: '{"model":"llama3.2:3b","created_at":"2025-02-21T02:57:55.059392Z","response":"{\"name\":
|
||||||
\"SimpleModel\", \"arguments\":{\"name\": \"Alice Llama\", \"age\": 30}}","done":true,"done_reason":"stop","context":[128006,9125,128007,271,38766,1303,33025,2696,25,6790,220,2366,18,271,128009,128006,882,128007,271,14711,2724,512,678,25,30505,445,81101,11,13381,25,220,966,271,14711,744,512,1360,13677,4823,32090,27785,0,2467,6881,311,420,3645,5324,609,794,330,1723,1292,498,330,16774,23118,14819,1292,794,330,14819,3220,32075,578,2768,5865,527,2561,311,499,512,13922,1337,1232,364,1723,518,364,1723,1232,5473,609,1232,364,16778,1747,518,364,4789,1232,364,34192,398,28532,1595,16778,1747,63,449,682,279,2631,5137,449,4495,4595,518,364,14105,1232,5473,13495,1232,5473,609,1232,5473,2150,1232,364,678,518,364,1337,1232,364,928,25762,364,425,1232,5473,2150,1232,364,17166,518,364,1337,1232,364,11924,8439,2186,364,6413,1232,2570,425,518,364,609,4181,364,1337,1232,364,1735,23742,3818,128009,128006,78191,128007,271,5018,609,794,330,16778,1747,498,330,16774,23118,609,794,330,62786,445,81101,498,330,425,794,220,966,3500],"total_duration":3374470708,"load_duration":1075750500,"prompt_eval_count":167,"prompt_eval_duration":1871000000,"eval_count":24,"eval_duration":426000000}'
|
\"Alice Llama\", \"age\": 30}","done":true,"done_reason":"stop","context":[128006,9125,128007,271,38766,1303,33025,2696,25,6790,220,2366,18,271,128009,128006,882,128007,271,14711,744,512,5618,5625,279,2768,1495,1139,2764,4823,382,5207,27785,279,2764,4823,323,4400,775,382,791,4823,2011,1833,420,3645,7041,512,517,220,330,609,794,610,345,220,330,425,794,528,198,633,14711,2724,512,678,25,30505,445,81101,11,13381,25,220,966,271,128009,128006,78191,128007,271,5018,609,794,330,62786,445,81101,498,330,425,794,220,966,92],"total_duration":4675906000,"load_duration":836091458,"prompt_eval_count":82,"prompt_eval_duration":3561000000,"eval_count":15,"eval_duration":275000000}'
|
||||||
headers:
|
headers:
|
||||||
Content-Length:
|
Content-Length:
|
||||||
- '1263'
|
- '761'
|
||||||
Content-Type:
|
Content-Type:
|
||||||
- application/json; charset=utf-8
|
- application/json; charset=utf-8
|
||||||
Date:
|
Date:
|
||||||
- Wed, 15 Jan 2025 20:47:12 GMT
|
- Fri, 21 Feb 2025 02:57:55 GMT
|
||||||
http_version: HTTP/1.1
|
http_version: HTTP/1.1
|
||||||
status_code: 200
|
status_code: 200
|
||||||
- request:
|
- request:
|
||||||
@@ -52,7 +47,7 @@ interactions:
|
|||||||
host:
|
host:
|
||||||
- localhost:11434
|
- localhost:11434
|
||||||
user-agent:
|
user-agent:
|
||||||
- litellm/1.57.4
|
- litellm/1.60.2
|
||||||
method: POST
|
method: POST
|
||||||
uri: http://localhost:11434/api/show
|
uri: http://localhost:11434/api/show
|
||||||
response:
|
response:
|
||||||
@@ -228,7 +223,7 @@ interactions:
|
|||||||
Reporting violations of the Acceptable Use Policy or unlicensed uses of Llama
|
Reporting violations of the Acceptable Use Policy or unlicensed uses of Llama
|
||||||
3.2: LlamaUseReport@meta.com\",\"modelfile\":\"# Modelfile generated by \\\"ollama
|
3.2: LlamaUseReport@meta.com\",\"modelfile\":\"# Modelfile generated by \\\"ollama
|
||||||
show\\\"\\n# To build a new Modelfile based on this, replace FROM with:\\n#
|
show\\\"\\n# To build a new Modelfile based on this, replace FROM with:\\n#
|
||||||
FROM llama3.2:3b\\n\\nFROM /Users/brandonhancock/.ollama/models/blobs/sha256-dde5aa3fc5ffc17176b5e8bdc82f587b24b2678c6c66101bf7da77af9f7ccdff\\nTEMPLATE
|
FROM llama3.2:3b\\n\\nFROM /Users/joaomoura/.ollama/models/blobs/sha256-dde5aa3fc5ffc17176b5e8bdc82f587b24b2678c6c66101bf7da77af9f7ccdff\\nTEMPLATE
|
||||||
\\\"\\\"\\\"\\u003c|start_header_id|\\u003esystem\\u003c|end_header_id|\\u003e\\n\\nCutting
|
\\\"\\\"\\\"\\u003c|start_header_id|\\u003esystem\\u003c|end_header_id|\\u003e\\n\\nCutting
|
||||||
Knowledge Date: December 2023\\n\\n{{ if .System }}{{ .System }}\\n{{- end }}\\n{{-
|
Knowledge Date: December 2023\\n\\n{{ if .System }}{{ .System }}\\n{{- end }}\\n{{-
|
||||||
if .Tools }}When you receive a tool call response, use the output to format
|
if .Tools }}When you receive a tool call response, use the output to format
|
||||||
@@ -441,12 +436,12 @@ interactions:
|
|||||||
.Content }}\\n{{- end }}{{ if not $last }}\\u003c|eot_id|\\u003e{{ end }}\\n{{-
|
.Content }}\\n{{- end }}{{ if not $last }}\\u003c|eot_id|\\u003e{{ end }}\\n{{-
|
||||||
else if eq .Role \\\"tool\\\" }}\\u003c|start_header_id|\\u003eipython\\u003c|end_header_id|\\u003e\\n\\n{{
|
else if eq .Role \\\"tool\\\" }}\\u003c|start_header_id|\\u003eipython\\u003c|end_header_id|\\u003e\\n\\n{{
|
||||||
.Content }}\\u003c|eot_id|\\u003e{{ if $last }}\\u003c|start_header_id|\\u003eassistant\\u003c|end_header_id|\\u003e\\n\\n{{
|
.Content }}\\u003c|eot_id|\\u003e{{ if $last }}\\u003c|start_header_id|\\u003eassistant\\u003c|end_header_id|\\u003e\\n\\n{{
|
||||||
end }}\\n{{- end }}\\n{{- end }}\",\"details\":{\"parent_model\":\"\",\"format\":\"gguf\",\"family\":\"llama\",\"families\":[\"llama\"],\"parameter_size\":\"3.2B\",\"quantization_level\":\"Q4_K_M\"},\"model_info\":{\"general.architecture\":\"llama\",\"general.basename\":\"Llama-3.2\",\"general.file_type\":15,\"general.finetune\":\"Instruct\",\"general.languages\":[\"en\",\"de\",\"fr\",\"it\",\"pt\",\"hi\",\"es\",\"th\"],\"general.parameter_count\":3212749888,\"general.quantization_version\":2,\"general.size_label\":\"3B\",\"general.tags\":[\"facebook\",\"meta\",\"pytorch\",\"llama\",\"llama-3\",\"text-generation\"],\"general.type\":\"model\",\"llama.attention.head_count\":24,\"llama.attention.head_count_kv\":8,\"llama.attention.key_length\":128,\"llama.attention.layer_norm_rms_epsilon\":0.00001,\"llama.attention.value_length\":128,\"llama.block_count\":28,\"llama.context_length\":131072,\"llama.embedding_length\":3072,\"llama.feed_forward_length\":8192,\"llama.rope.dimension_count\":128,\"llama.rope.freq_base\":500000,\"llama.vocab_size\":128256,\"tokenizer.ggml.bos_token_id\":128000,\"tokenizer.ggml.eos_token_id\":128009,\"tokenizer.ggml.merges\":null,\"tokenizer.ggml.model\":\"gpt2\",\"tokenizer.ggml.pre\":\"llama-bpe\",\"tokenizer.ggml.token_type\":null,\"tokenizer.ggml.tokens\":null},\"modified_at\":\"2024-12-31T11:53:14.529771974-05:00\"}"
|
end }}\\n{{- end }}\\n{{- end }}\",\"details\":{\"parent_model\":\"\",\"format\":\"gguf\",\"family\":\"llama\",\"families\":[\"llama\"],\"parameter_size\":\"3.2B\",\"quantization_level\":\"Q4_K_M\"},\"model_info\":{\"general.architecture\":\"llama\",\"general.basename\":\"Llama-3.2\",\"general.file_type\":15,\"general.finetune\":\"Instruct\",\"general.languages\":[\"en\",\"de\",\"fr\",\"it\",\"pt\",\"hi\",\"es\",\"th\"],\"general.parameter_count\":3212749888,\"general.quantization_version\":2,\"general.size_label\":\"3B\",\"general.tags\":[\"facebook\",\"meta\",\"pytorch\",\"llama\",\"llama-3\",\"text-generation\"],\"general.type\":\"model\",\"llama.attention.head_count\":24,\"llama.attention.head_count_kv\":8,\"llama.attention.key_length\":128,\"llama.attention.layer_norm_rms_epsilon\":0.00001,\"llama.attention.value_length\":128,\"llama.block_count\":28,\"llama.context_length\":131072,\"llama.embedding_length\":3072,\"llama.feed_forward_length\":8192,\"llama.rope.dimension_count\":128,\"llama.rope.freq_base\":500000,\"llama.vocab_size\":128256,\"tokenizer.ggml.bos_token_id\":128000,\"tokenizer.ggml.eos_token_id\":128009,\"tokenizer.ggml.merges\":null,\"tokenizer.ggml.model\":\"gpt2\",\"tokenizer.ggml.pre\":\"llama-bpe\",\"tokenizer.ggml.token_type\":null,\"tokenizer.ggml.tokens\":null},\"modified_at\":\"2025-02-20T18:55:09.150577031-08:00\"}"
|
||||||
headers:
|
headers:
|
||||||
Content-Type:
|
Content-Type:
|
||||||
- application/json; charset=utf-8
|
- application/json; charset=utf-8
|
||||||
Date:
|
Date:
|
||||||
- Wed, 15 Jan 2025 20:47:12 GMT
|
- Fri, 21 Feb 2025 02:57:55 GMT
|
||||||
Transfer-Encoding:
|
Transfer-Encoding:
|
||||||
- chunked
|
- chunked
|
||||||
http_version: HTTP/1.1
|
http_version: HTTP/1.1
|
||||||
@@ -467,7 +462,7 @@ interactions:
|
|||||||
host:
|
host:
|
||||||
- localhost:11434
|
- localhost:11434
|
||||||
user-agent:
|
user-agent:
|
||||||
- litellm/1.57.4
|
- litellm/1.60.2
|
||||||
method: POST
|
method: POST
|
||||||
uri: http://localhost:11434/api/show
|
uri: http://localhost:11434/api/show
|
||||||
response:
|
response:
|
||||||
@@ -643,7 +638,7 @@ interactions:
|
|||||||
Reporting violations of the Acceptable Use Policy or unlicensed uses of Llama
|
Reporting violations of the Acceptable Use Policy or unlicensed uses of Llama
|
||||||
3.2: LlamaUseReport@meta.com\",\"modelfile\":\"# Modelfile generated by \\\"ollama
|
3.2: LlamaUseReport@meta.com\",\"modelfile\":\"# Modelfile generated by \\\"ollama
|
||||||
show\\\"\\n# To build a new Modelfile based on this, replace FROM with:\\n#
|
show\\\"\\n# To build a new Modelfile based on this, replace FROM with:\\n#
|
||||||
FROM llama3.2:3b\\n\\nFROM /Users/brandonhancock/.ollama/models/blobs/sha256-dde5aa3fc5ffc17176b5e8bdc82f587b24b2678c6c66101bf7da77af9f7ccdff\\nTEMPLATE
|
FROM llama3.2:3b\\n\\nFROM /Users/joaomoura/.ollama/models/blobs/sha256-dde5aa3fc5ffc17176b5e8bdc82f587b24b2678c6c66101bf7da77af9f7ccdff\\nTEMPLATE
|
||||||
\\\"\\\"\\\"\\u003c|start_header_id|\\u003esystem\\u003c|end_header_id|\\u003e\\n\\nCutting
|
\\\"\\\"\\\"\\u003c|start_header_id|\\u003esystem\\u003c|end_header_id|\\u003e\\n\\nCutting
|
||||||
Knowledge Date: December 2023\\n\\n{{ if .System }}{{ .System }}\\n{{- end }}\\n{{-
|
Knowledge Date: December 2023\\n\\n{{ if .System }}{{ .System }}\\n{{- end }}\\n{{-
|
||||||
if .Tools }}When you receive a tool call response, use the output to format
|
if .Tools }}When you receive a tool call response, use the output to format
|
||||||
@@ -856,12 +851,12 @@ interactions:
|
|||||||
.Content }}\\n{{- end }}{{ if not $last }}\\u003c|eot_id|\\u003e{{ end }}\\n{{-
|
.Content }}\\n{{- end }}{{ if not $last }}\\u003c|eot_id|\\u003e{{ end }}\\n{{-
|
||||||
else if eq .Role \\\"tool\\\" }}\\u003c|start_header_id|\\u003eipython\\u003c|end_header_id|\\u003e\\n\\n{{
|
else if eq .Role \\\"tool\\\" }}\\u003c|start_header_id|\\u003eipython\\u003c|end_header_id|\\u003e\\n\\n{{
|
||||||
.Content }}\\u003c|eot_id|\\u003e{{ if $last }}\\u003c|start_header_id|\\u003eassistant\\u003c|end_header_id|\\u003e\\n\\n{{
|
.Content }}\\u003c|eot_id|\\u003e{{ if $last }}\\u003c|start_header_id|\\u003eassistant\\u003c|end_header_id|\\u003e\\n\\n{{
|
||||||
end }}\\n{{- end }}\\n{{- end }}\",\"details\":{\"parent_model\":\"\",\"format\":\"gguf\",\"family\":\"llama\",\"families\":[\"llama\"],\"parameter_size\":\"3.2B\",\"quantization_level\":\"Q4_K_M\"},\"model_info\":{\"general.architecture\":\"llama\",\"general.basename\":\"Llama-3.2\",\"general.file_type\":15,\"general.finetune\":\"Instruct\",\"general.languages\":[\"en\",\"de\",\"fr\",\"it\",\"pt\",\"hi\",\"es\",\"th\"],\"general.parameter_count\":3212749888,\"general.quantization_version\":2,\"general.size_label\":\"3B\",\"general.tags\":[\"facebook\",\"meta\",\"pytorch\",\"llama\",\"llama-3\",\"text-generation\"],\"general.type\":\"model\",\"llama.attention.head_count\":24,\"llama.attention.head_count_kv\":8,\"llama.attention.key_length\":128,\"llama.attention.layer_norm_rms_epsilon\":0.00001,\"llama.attention.value_length\":128,\"llama.block_count\":28,\"llama.context_length\":131072,\"llama.embedding_length\":3072,\"llama.feed_forward_length\":8192,\"llama.rope.dimension_count\":128,\"llama.rope.freq_base\":500000,\"llama.vocab_size\":128256,\"tokenizer.ggml.bos_token_id\":128000,\"tokenizer.ggml.eos_token_id\":128009,\"tokenizer.ggml.merges\":null,\"tokenizer.ggml.model\":\"gpt2\",\"tokenizer.ggml.pre\":\"llama-bpe\",\"tokenizer.ggml.token_type\":null,\"tokenizer.ggml.tokens\":null},\"modified_at\":\"2024-12-31T11:53:14.529771974-05:00\"}"
|
end }}\\n{{- end }}\\n{{- end }}\",\"details\":{\"parent_model\":\"\",\"format\":\"gguf\",\"family\":\"llama\",\"families\":[\"llama\"],\"parameter_size\":\"3.2B\",\"quantization_level\":\"Q4_K_M\"},\"model_info\":{\"general.architecture\":\"llama\",\"general.basename\":\"Llama-3.2\",\"general.file_type\":15,\"general.finetune\":\"Instruct\",\"general.languages\":[\"en\",\"de\",\"fr\",\"it\",\"pt\",\"hi\",\"es\",\"th\"],\"general.parameter_count\":3212749888,\"general.quantization_version\":2,\"general.size_label\":\"3B\",\"general.tags\":[\"facebook\",\"meta\",\"pytorch\",\"llama\",\"llama-3\",\"text-generation\"],\"general.type\":\"model\",\"llama.attention.head_count\":24,\"llama.attention.head_count_kv\":8,\"llama.attention.key_length\":128,\"llama.attention.layer_norm_rms_epsilon\":0.00001,\"llama.attention.value_length\":128,\"llama.block_count\":28,\"llama.context_length\":131072,\"llama.embedding_length\":3072,\"llama.feed_forward_length\":8192,\"llama.rope.dimension_count\":128,\"llama.rope.freq_base\":500000,\"llama.vocab_size\":128256,\"tokenizer.ggml.bos_token_id\":128000,\"tokenizer.ggml.eos_token_id\":128009,\"tokenizer.ggml.merges\":null,\"tokenizer.ggml.model\":\"gpt2\",\"tokenizer.ggml.pre\":\"llama-bpe\",\"tokenizer.ggml.token_type\":null,\"tokenizer.ggml.tokens\":null},\"modified_at\":\"2025-02-20T18:55:09.150577031-08:00\"}"
|
||||||
headers:
|
headers:
|
||||||
Content-Type:
|
Content-Type:
|
||||||
- application/json; charset=utf-8
|
- application/json; charset=utf-8
|
||||||
Date:
|
Date:
|
||||||
- Wed, 15 Jan 2025 20:47:12 GMT
|
- Fri, 21 Feb 2025 02:57:55 GMT
|
||||||
Transfer-Encoding:
|
Transfer-Encoding:
|
||||||
- chunked
|
- chunked
|
||||||
http_version: HTTP/1.1
|
http_version: HTTP/1.1
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import json
|
import json
|
||||||
|
import os
|
||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
from unittest.mock import MagicMock, Mock, patch
|
from unittest.mock import MagicMock, Mock, patch
|
||||||
|
|
||||||
@@ -220,10 +221,13 @@ def test_get_conversion_instructions_gpt():
|
|||||||
supports_function_calling.return_value = True
|
supports_function_calling.return_value = True
|
||||||
instructions = get_conversion_instructions(SimpleModel, llm)
|
instructions = get_conversion_instructions(SimpleModel, llm)
|
||||||
model_schema = PydanticSchemaParser(model=SimpleModel).get_schema()
|
model_schema = PydanticSchemaParser(model=SimpleModel).get_schema()
|
||||||
assert (
|
expected_instructions = (
|
||||||
instructions
|
"Please convert the following text into valid JSON.\n\n"
|
||||||
== f"Please convert the following text into valid JSON.\n\nThe JSON should follow this schema:\n```json\n{model_schema}\n```"
|
"Output ONLY the valid JSON and nothing else.\n\n"
|
||||||
|
"The JSON must follow this schema exactly:\n```json\n"
|
||||||
|
f"{model_schema}\n```"
|
||||||
)
|
)
|
||||||
|
assert instructions == expected_instructions
|
||||||
|
|
||||||
|
|
||||||
def test_get_conversion_instructions_non_gpt():
|
def test_get_conversion_instructions_non_gpt():
|
||||||
@@ -346,12 +350,17 @@ def test_convert_with_instructions():
|
|||||||
assert output.age == 30
|
assert output.age == 30
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
# Skip tests that call external APIs when running in CI/CD
|
||||||
|
skip_external_api = pytest.mark.skipif(
|
||||||
|
os.getenv("CI") is not None, reason="Skipping tests that call external API in CI/CD"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_external_api
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"], record_mode="once")
|
||||||
def test_converter_with_llama3_2_model():
|
def test_converter_with_llama3_2_model():
|
||||||
llm = LLM(model="ollama/llama3.2:3b", base_url="http://localhost:11434")
|
llm = LLM(model="ollama/llama3.2:3b", base_url="http://localhost:11434")
|
||||||
|
|
||||||
sample_text = "Name: Alice Llama, Age: 30"
|
sample_text = "Name: Alice Llama, Age: 30"
|
||||||
|
|
||||||
instructions = get_conversion_instructions(SimpleModel, llm)
|
instructions = get_conversion_instructions(SimpleModel, llm)
|
||||||
converter = Converter(
|
converter = Converter(
|
||||||
llm=llm,
|
llm=llm,
|
||||||
@@ -359,19 +368,17 @@ def test_converter_with_llama3_2_model():
|
|||||||
model=SimpleModel,
|
model=SimpleModel,
|
||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
)
|
)
|
||||||
|
|
||||||
output = converter.to_pydantic()
|
output = converter.to_pydantic()
|
||||||
|
|
||||||
assert isinstance(output, SimpleModel)
|
assert isinstance(output, SimpleModel)
|
||||||
assert output.name == "Alice Llama"
|
assert output.name == "Alice Llama"
|
||||||
assert output.age == 30
|
assert output.age == 30
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
@skip_external_api
|
||||||
|
@pytest.mark.vcr(filter_headers=["authorization"], record_mode="once")
|
||||||
def test_converter_with_llama3_1_model():
|
def test_converter_with_llama3_1_model():
|
||||||
llm = LLM(model="ollama/llama3.1", base_url="http://localhost:11434")
|
llm = LLM(model="ollama/llama3.1", base_url="http://localhost:11434")
|
||||||
sample_text = "Name: Alice Llama, Age: 30"
|
sample_text = "Name: Alice Llama, Age: 30"
|
||||||
|
|
||||||
instructions = get_conversion_instructions(SimpleModel, llm)
|
instructions = get_conversion_instructions(SimpleModel, llm)
|
||||||
converter = Converter(
|
converter = Converter(
|
||||||
llm=llm,
|
llm=llm,
|
||||||
@@ -379,14 +386,19 @@ def test_converter_with_llama3_1_model():
|
|||||||
model=SimpleModel,
|
model=SimpleModel,
|
||||||
instructions=instructions,
|
instructions=instructions,
|
||||||
)
|
)
|
||||||
|
|
||||||
output = converter.to_pydantic()
|
output = converter.to_pydantic()
|
||||||
|
|
||||||
assert isinstance(output, SimpleModel)
|
assert isinstance(output, SimpleModel)
|
||||||
assert output.name == "Alice Llama"
|
assert output.name == "Alice Llama"
|
||||||
assert output.age == 30
|
assert output.age == 30
|
||||||
|
|
||||||
|
|
||||||
|
# Skip tests that call external APIs when running in CI/CD
|
||||||
|
skip_external_api = pytest.mark.skipif(
|
||||||
|
os.getenv("CI") is not None, reason="Skipping tests that call external API in CI/CD"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@skip_external_api
|
||||||
@pytest.mark.vcr(filter_headers=["authorization"])
|
@pytest.mark.vcr(filter_headers=["authorization"])
|
||||||
def test_converter_with_nested_model():
|
def test_converter_with_nested_model():
|
||||||
llm = LLM(model="gpt-4o-mini")
|
llm = LLM(model="gpt-4o-mini")
|
||||||
@@ -563,7 +575,7 @@ def test_converter_with_ambiguous_input():
|
|||||||
with pytest.raises(ConverterError) as exc_info:
|
with pytest.raises(ConverterError) as exc_info:
|
||||||
output = converter.to_pydantic()
|
output = converter.to_pydantic()
|
||||||
|
|
||||||
assert "validation error" in str(exc_info.value).lower()
|
assert "failed to convert text into a pydantic model" in str(exc_info.value).lower()
|
||||||
|
|
||||||
|
|
||||||
# Tests for function calling support
|
# Tests for function calling support
|
||||||
|
|||||||
Reference in New Issue
Block a user