diff --git a/Makefile b/Makefile index 8f07afa90..6c1ff7502 100644 --- a/Makefile +++ b/Makefile @@ -137,6 +137,7 @@ push-test-agent: buildx-create build-kagent-adk echo "Building FROM DOCKER_REGISTRY=$(DOCKER_REGISTRY)/$(DOCKER_REPO)/kagent-adk:$(VERSION)" $(DOCKER_BUILDER) build --push $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(DOCKER_REGISTRY)/kebab:latest -f go/test/e2e/agents/kebab/Dockerfile ./go/test/e2e/agents/kebab kubectl apply --namespace kagent --context kind-$(KIND_CLUSTER_NAME) -f go/test/e2e/agents/kebab/agent.yaml + $(DOCKER_BUILDER) build --push $(BUILD_ARGS) $(TOOLS_IMAGE_BUILD_ARGS) -t $(DOCKER_REGISTRY)/poem-flow:latest -f python/samples/crewai/poem_flow/Dockerfile ./python .PHONY: create-kind-cluster create-kind-cluster: diff --git a/go/internal/database/client.go b/go/internal/database/client.go index e51606133..7e67948e6 100644 --- a/go/internal/database/client.go +++ b/go/internal/database/client.go @@ -2,6 +2,7 @@ package database import ( "encoding/json" + "errors" "fmt" "slices" "time" @@ -57,6 +58,13 @@ type Client interface { StoreCheckpointWrites(writes []*LangGraphCheckpointWrite) error ListCheckpoints(userID, threadID, checkpointNS string, checkpointID *string, limit int) ([]*LangGraphCheckpointTuple, error) DeleteCheckpoint(userID, threadID string) error + + // CrewAI methods + StoreCrewAIMemory(memory *CrewAIAgentMemory) error + SearchCrewAIMemoryByTask(userID, threadID, taskDescription string, limit int) ([]*CrewAIAgentMemory, error) + ResetCrewAIMemory(userID, threadID string) error + StoreCrewAIFlowState(state *CrewAIFlowState) error + GetCrewAIFlowState(userID, threadID string) (*CrewAIFlowState, error) } type LangGraphCheckpointTuple struct { @@ -578,3 +586,83 @@ func (c *clientImpl) DeleteCheckpoint(userID, threadID string) error { }) } + +// CrewAI methods + +// StoreCrewAIMemory stores CrewAI agent memory +func (c *clientImpl) StoreCrewAIMemory(memory *CrewAIAgentMemory) error { + err := save(c.db, memory) + if err != nil { + return fmt.Errorf("failed to store CrewAI agent memory: %w", err) + } + return nil +} + +// SearchCrewAIMemoryByTask searches CrewAI agent memory by task description across all agents for a session +func (c *clientImpl) SearchCrewAIMemoryByTask(userID, threadID, taskDescription string, limit int) ([]*CrewAIAgentMemory, error) { + var memories []*CrewAIAgentMemory + + // Search for task_description within the JSON memory_data field + // Using JSON_EXTRACT or JSON_UNQUOTE for MySQL/PostgreSQL, or simple LIKE for SQLite + // Sort by created_at DESC, then by score ASC (if score exists in JSON) + query := c.db.Where( + "user_id = ? AND thread_id = ? AND (memory_data LIKE ? OR JSON_EXTRACT(memory_data, '$.task_description') LIKE ?)", + userID, threadID, "%"+taskDescription+"%", "%"+taskDescription+"%", + ).Order("created_at DESC, JSON_EXTRACT(memory_data, '$.score') ASC") + + // Apply limit + if limit > 0 { + query = query.Limit(limit) + } + + err := query.Find(&memories).Error + if err != nil { + return nil, fmt.Errorf("failed to search CrewAI agent memory by task: %w", err) + } + + return memories, nil +} + +// ResetCrewAIMemory deletes all CrewAI agent memory for a session +func (c *clientImpl) ResetCrewAIMemory(userID, threadID string) error { + result := c.db.Where( + "user_id = ? AND thread_id = ?", + userID, threadID, + ).Delete(&CrewAIAgentMemory{}) + + if result.Error != nil { + return fmt.Errorf("failed to reset CrewAI agent memory: %w", result.Error) + } + + return nil +} + +// StoreCrewAIFlowState stores CrewAI flow state +func (c *clientImpl) StoreCrewAIFlowState(state *CrewAIFlowState) error { + err := save(c.db, state) + if err != nil { + return fmt.Errorf("failed to store CrewAI flow state: %w", err) + } + return nil +} + +// GetCrewAIFlowState retrieves the most recent CrewAI flow state +func (c *clientImpl) GetCrewAIFlowState(userID, threadID string) (*CrewAIFlowState, error) { + var state CrewAIFlowState + + // Get the most recent state by ordering by created_at DESC + // Thread_id is equivalent to flow_uuid used by CrewAI because in each session there is only one flow + err := c.db.Where( + "user_id = ? AND thread_id = ?", + userID, threadID, + ).Order("created_at DESC").First(&state).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil // Return nil for not found, as expected by the Python client + } + return nil, fmt.Errorf("failed to get CrewAI flow state: %w", err) + } + + return &state, nil +} diff --git a/go/internal/database/fake/client.go b/go/internal/database/fake/client.go index e0d69c0ca..18dcca471 100644 --- a/go/internal/database/fake/client.go +++ b/go/internal/database/fake/client.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "sort" + "strings" "sync" "github.com/kagent-dev/kagent/go/api/v1alpha2" @@ -26,6 +27,8 @@ type InMemoryFakeClient struct { pushNotifications map[string]*protocol.TaskPushNotificationConfig // key: taskID checkpoints map[string]*database.LangGraphCheckpoint // key: user_id:thread_id:checkpoint_ns:checkpoint_id checkpointWrites map[string][]*database.LangGraphCheckpointWrite // key: user_id:thread_id:checkpoint_ns:checkpoint_id + crewaiMemory map[string][]*database.CrewAIAgentMemory // key: user_id:thread_id:agent_id + crewaiFlowStates map[string]*database.CrewAIFlowState // key: user_id:thread_id nextFeedbackID int } @@ -43,6 +46,8 @@ func NewClient() database.Client { pushNotifications: make(map[string]*protocol.TaskPushNotificationConfig), checkpoints: make(map[string]*database.LangGraphCheckpoint), checkpointWrites: make(map[string][]*database.LangGraphCheckpointWrite), + crewaiMemory: make(map[string][]*database.CrewAIAgentMemory), + crewaiFlowStates: make(map[string]*database.CrewAIFlowState), nextFeedbackID: 1, } } @@ -724,3 +729,143 @@ func (c *InMemoryFakeClient) ListWrites(userID, threadID, checkpointNS, checkpoi return writes[start:end], nil } + +// CrewAI methods + +// StoreCrewAIMemory stores CrewAI agent memory +func (c *InMemoryFakeClient) StoreCrewAIMemory(memory *database.CrewAIAgentMemory) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.crewaiMemory == nil { + c.crewaiMemory = make(map[string][]*database.CrewAIAgentMemory) + } + + key := fmt.Sprintf("%s:%s", memory.UserID, memory.ThreadID) + c.crewaiMemory[key] = append(c.crewaiMemory[key], memory) + + return nil +} + +// SearchCrewAIMemoryByTask searches CrewAI agent memory by task description across all agents for a session +func (c *InMemoryFakeClient) SearchCrewAIMemoryByTask(userID, threadID, taskDescription string, limit int) ([]*database.CrewAIAgentMemory, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + if c.crewaiMemory == nil { + return []*database.CrewAIAgentMemory{}, nil + } + + var allMemories []*database.CrewAIAgentMemory + + // Search across all agents for this user/thread + for key, memories := range c.crewaiMemory { + // Key format is "user_id:thread_id" + if strings.HasPrefix(key, userID+":"+threadID) { + for _, memory := range memories { + // Parse the JSON memory data and search for task_description + var memoryData map[string]interface{} + if err := json.Unmarshal([]byte(memory.MemoryData), &memoryData); err == nil { + if taskDesc, ok := memoryData["task_description"].(string); ok { + if strings.Contains(strings.ToLower(taskDesc), strings.ToLower(taskDescription)) { + allMemories = append(allMemories, memory) + } + } + } + // Fallback to simple string search if JSON parsing fails + if len(allMemories) == 0 && strings.Contains(strings.ToLower(memory.MemoryData), strings.ToLower(taskDescription)) { + allMemories = append(allMemories, memory) + } + } + } + } + + // Sort by created_at DESC, then by score ASC (if score exists in JSON) + sort.Slice(allMemories, func(i, j int) bool { + // First sort by created_at DESC (most recent first) + if !allMemories[i].CreatedAt.Equal(allMemories[j].CreatedAt) { + return allMemories[i].CreatedAt.After(allMemories[j].CreatedAt) + } + + // If created_at is equal, sort by score ASC + var scoreI, scoreJ float64 + var memoryDataI, memoryDataJ map[string]interface{} + + if err := json.Unmarshal([]byte(allMemories[i].MemoryData), &memoryDataI); err == nil { + if score, ok := memoryDataI["score"].(float64); ok { + scoreI = score + } + } + + if err := json.Unmarshal([]byte(allMemories[j].MemoryData), &memoryDataJ); err == nil { + if score, ok := memoryDataJ["score"].(float64); ok { + scoreJ = score + } + } + + return scoreI < scoreJ + }) + + // Apply limit + if limit > 0 && len(allMemories) > limit { + allMemories = allMemories[:limit] + } + + return allMemories, nil +} + +// ResetCrewAIMemory deletes all CrewAI agent memory for a session +func (c *InMemoryFakeClient) ResetCrewAIMemory(userID, threadID string) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.crewaiMemory == nil { + return nil + } + + // Find and delete all memory entries for this user/thread combination + keysToDelete := make([]string, 0) + for key := range c.crewaiMemory { + // Key format is "user_id:thread_id" + if strings.HasPrefix(key, userID+":"+threadID) { + keysToDelete = append(keysToDelete, key) + } + } + + // Delete the entries + for _, key := range keysToDelete { + delete(c.crewaiMemory, key) + } + + return nil +} + +// StoreCrewAIFlowState stores CrewAI flow state +func (c *InMemoryFakeClient) StoreCrewAIFlowState(state *database.CrewAIFlowState) error { + c.mu.Lock() + defer c.mu.Unlock() + + if c.crewaiFlowStates == nil { + c.crewaiFlowStates = make(map[string]*database.CrewAIFlowState) + } + + key := fmt.Sprintf("%s:%s", state.UserID, state.ThreadID) + c.crewaiFlowStates[key] = state + + return nil +} + +// GetCrewAIFlowState retrieves CrewAI flow state +func (c *InMemoryFakeClient) GetCrewAIFlowState(userID, threadID string) (*database.CrewAIFlowState, error) { + c.mu.RLock() + defer c.mu.RUnlock() + + if c.crewaiFlowStates == nil { + return nil, nil + } + + key := fmt.Sprintf("%s:%s", userID, threadID) + state := c.crewaiFlowStates[key] + + return state, nil +} diff --git a/go/internal/database/manager.go b/go/internal/database/manager.go index ba9fe2dea..1a48b1792 100644 --- a/go/internal/database/manager.go +++ b/go/internal/database/manager.go @@ -102,6 +102,8 @@ func (m *Manager) Initialize() error { &ToolServer{}, &LangGraphCheckpoint{}, &LangGraphCheckpointWrite{}, + &CrewAIAgentMemory{}, + &CrewAIFlowState{}, ) if err != nil { @@ -130,6 +132,8 @@ func (m *Manager) Reset(recreateTables bool) error { &ToolServer{}, &LangGraphCheckpoint{}, &LangGraphCheckpointWrite{}, + &CrewAIAgentMemory{}, + &CrewAIFlowState{}, ) if err != nil { diff --git a/go/internal/database/models.go b/go/internal/database/models.go index b56449d8c..065b96ce5 100644 --- a/go/internal/database/models.go +++ b/go/internal/database/models.go @@ -179,6 +179,29 @@ type LangGraphCheckpointWrite struct { DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` } +// CrewAIAgentMemory represents long-term memory for CrewAI agents +type CrewAIAgentMemory struct { + UserID string `gorm:"primaryKey;not null" json:"user_id"` + ThreadID string `gorm:"primaryKey;not null" json:"thread_id"` + CreatedAt time.Time `gorm:"autoCreateTime;index:idx_crewai_memory_list" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + // MemoryData contains JSON serialized memory data including task_description, score, metadata, datetime + MemoryData string `gorm:"type:text;not null" json:"memory_data"` +} + +// CrewAIFlowState represents flow state for CrewAI flows +type CrewAIFlowState struct { + UserID string `gorm:"primaryKey;not null" json:"user_id"` + ThreadID string `gorm:"primaryKey;not null" json:"thread_id"` + MethodName string `gorm:"primaryKey;not null" json:"method_name"` + CreatedAt time.Time `gorm:"autoCreateTime;index:idx_crewai_flow_state_list" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"deleted_at"` + // StateData contains JSON serialized flow state data + StateData string `gorm:"type:text;not null" json:"state_data"` +} + // TableName methods to match Python table names func (Agent) TableName() string { return "agent" } func (Event) TableName() string { return "event" } @@ -190,3 +213,5 @@ func (Tool) TableName() string { return "tool" } func (ToolServer) TableName() string { return "toolserver" } func (LangGraphCheckpoint) TableName() string { return "lg_checkpoint" } func (LangGraphCheckpointWrite) TableName() string { return "lg_checkpoint_write" } +func (CrewAIAgentMemory) TableName() string { return "crewai_agent_memory" } +func (CrewAIFlowState) TableName() string { return "crewai_flow_state" } diff --git a/go/internal/httpserver/handlers/crewai.go b/go/internal/httpserver/handlers/crewai.go new file mode 100644 index 000000000..f42fc7c51 --- /dev/null +++ b/go/internal/httpserver/handlers/crewai.go @@ -0,0 +1,300 @@ +package handlers + +import ( + "encoding/json" + "net/http" + "strconv" + + "github.com/kagent-dev/kagent/go/internal/database" + "github.com/kagent-dev/kagent/go/internal/httpserver/errors" + "github.com/kagent-dev/kagent/go/pkg/client/api" + ctrllog "sigs.k8s.io/controller-runtime/pkg/log" +) + +// CrewAIHandler handles CrewAI-related requests +type CrewAIHandler struct { + *Base +} + +// NewCrewAIHandler creates a new CrewAIHandler +func NewCrewAIHandler(base *Base) *CrewAIHandler { + return &CrewAIHandler{Base: base} +} + +// CrewAI request/response types following the Python Pydantic models + +// KagentMemoryPayload represents memory payload data from Python +type KagentMemoryPayload struct { + ThreadID string `json:"thread_id"` + UserID string `json:"user_id"` + MemoryData map[string]interface{} `json:"memory_data"` +} + +// KagentMemoryResponse represents memory response data +type KagentMemoryResponse struct { + Data []KagentMemoryPayload `json:"data"` +} + +// KagentFlowStatePayload represents flow state payload data +type KagentFlowStatePayload struct { + ThreadID string `json:"thread_id"` + MethodName string `json:"method_name"` + StateData map[string]interface{} `json:"state_data"` +} + +// KagentFlowStateResponse represents flow state response data +type KagentFlowStateResponse struct { + Data KagentFlowStatePayload `json:"data"` +} + +// HandleStoreMemory handles POST /api/crewai/memory requests +func (h *CrewAIHandler) HandleStoreMemory(w ErrorResponseWriter, r *http.Request) { + log := ctrllog.FromContext(r.Context()).WithName("crewai-handler").WithValues("operation", "store-memory") + + userID, err := GetUserID(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Failed to get user ID", err)) + return + } + log = log.WithValues("userID", userID) + + var req KagentMemoryPayload + if err := DecodeJSONBody(r, &req); err != nil { + w.RespondWithError(errors.NewBadRequestError("Invalid request body", err)) + return + } + + // Validate required fields + if req.ThreadID == "" { + w.RespondWithError(errors.NewBadRequestError("thread_id is required", nil)) + return + } + + log = log.WithValues( + "threadID", req.ThreadID, + "userID", userID, + ) + + // Serialize memory data to JSON string + memoryDataJSON, err := json.Marshal(req.MemoryData) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Failed to serialize memory data", err)) + return + } + + // Create memory model + memory := &database.CrewAIAgentMemory{ + UserID: userID, + ThreadID: req.ThreadID, + MemoryData: string(memoryDataJSON), + } + + // Store memory + if err := h.DatabaseService.StoreCrewAIMemory(memory); err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to store CrewAI memory", err)) + return + } + + log.Info("Successfully stored CrewAI memory") + data := api.NewResponse(struct{}{}, "Successfully stored CrewAI memory", false) + RespondWithJSON(w, http.StatusCreated, data) +} + +// HandleGetMemory handles GET /api/crewai/memory requests +func (h *CrewAIHandler) HandleGetMemory(w ErrorResponseWriter, r *http.Request) { + log := ctrllog.FromContext(r.Context()).WithName("crewai-handler").WithValues("operation", "list-memory") + + userID, err := GetUserID(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Failed to get user ID", err)) + return + } + threadID := r.URL.Query().Get("thread_id") + if threadID == "" { + w.RespondWithError(errors.NewBadRequestError("thread_id is required", nil)) + return + } + + taskDescription := r.URL.Query().Get("q") // query parameter for task description search + + limit := 0 + if limitStr := r.URL.Query().Get("limit"); limitStr != "" { + if parsedLimit, err := strconv.Atoi(limitStr); err == nil { + limit = parsedLimit + } + } + + log = log.WithValues("userID", userID, "threadID", threadID, "taskDescription", taskDescription, "limit", limit) + + var memories []*database.CrewAIAgentMemory + + // If task description is provided, search by task across all agents + // Otherwise, list memories for a specific agent + if taskDescription != "" { + log.V(1).Info("Searching CrewAI memory by task description") + memories, err = h.DatabaseService.SearchCrewAIMemoryByTask(userID, threadID, taskDescription, limit) + } else { + w.RespondWithError(errors.NewBadRequestError("Either agent_id or q (task description) parameter is required", nil)) + return + } + if err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to list CrewAI memory", err)) + return + } + + // Convert to response format + memoryPayloads := make([]KagentMemoryPayload, len(memories)) + for i, memory := range memories { + var memoryData map[string]interface{} + if err := json.Unmarshal([]byte(memory.MemoryData), &memoryData); err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to parse memory data", err)) + return + } + + memoryPayloads[i] = KagentMemoryPayload{ + ThreadID: memory.ThreadID, + UserID: memory.UserID, + MemoryData: memoryData, + } + } + + log.Info("Successfully listed CrewAI memory", "count", len(memoryPayloads)) + data := api.NewResponse(memoryPayloads, "Successfully listed CrewAI memory", false) + RespondWithJSON(w, http.StatusOK, data) +} + +// HandleResetMemory handles DELETE /api/crewai/memory requests +func (h *CrewAIHandler) HandleResetMemory(w ErrorResponseWriter, r *http.Request) { + log := ctrllog.FromContext(r.Context()).WithName("crewai-handler").WithValues("operation", "reset-memory") + + userID, err := GetUserID(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Failed to get user ID", err)) + return + } + + threadID := r.URL.Query().Get("thread_id") + if threadID == "" { + w.RespondWithError(errors.NewBadRequestError("thread_id is required", nil)) + return + } + + log = log.WithValues("userID", userID, "threadID", threadID) + + log.V(1).Info("Resetting CrewAI memory") + err = h.DatabaseService.ResetCrewAIMemory(userID, threadID) + if err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to reset CrewAI memory", err)) + return + } + + log.Info("Successfully reset CrewAI memory") + data := api.NewResponse(struct{}{}, "Successfully reset CrewAI memory", false) + RespondWithJSON(w, http.StatusOK, data) +} + +// HandleStoreFlowState handles POST /api/crewai/flows/state requests +func (h *CrewAIHandler) HandleStoreFlowState(w ErrorResponseWriter, r *http.Request) { + log := ctrllog.FromContext(r.Context()).WithName("crewai-handler").WithValues("operation", "store-flow-state") + + userID, err := GetUserID(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Failed to get user ID", err)) + return + } + log = log.WithValues("userID", userID) + + var req KagentFlowStatePayload + if err := DecodeJSONBody(r, &req); err != nil { + w.RespondWithError(errors.NewBadRequestError("Invalid request body", err)) + return + } + + // Validate required fields + if req.ThreadID == "" { + w.RespondWithError(errors.NewBadRequestError("thread_id is required", nil)) + return + } + if req.MethodName == "" { + w.RespondWithError(errors.NewBadRequestError("method_name is required", nil)) + return + } + + log = log.WithValues( + "threadID", req.ThreadID, + "methodName", req.MethodName, + ) + + // Serialize state data to JSON string + stateDataJSON, err := json.Marshal(req.StateData) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Failed to serialize state data", err)) + return + } + + // Create flow state model + state := &database.CrewAIFlowState{ + UserID: userID, + ThreadID: req.ThreadID, + MethodName: req.MethodName, + StateData: string(stateDataJSON), + } + + // Store flow state + if err := h.DatabaseService.StoreCrewAIFlowState(state); err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to store CrewAI flow state", err)) + return + } + + log.Info("Successfully stored CrewAI flow state") + data := api.NewResponse(struct{}{}, "Successfully stored CrewAI flow state", false) + RespondWithJSON(w, http.StatusCreated, data) +} + +// HandleGetFlowState handles GET /api/crewai/flows/state requests +func (h *CrewAIHandler) HandleGetFlowState(w ErrorResponseWriter, r *http.Request) { + log := ctrllog.FromContext(r.Context()).WithName("crewai-handler").WithValues("operation", "get-flow-state") + + userID, err := GetUserID(r) + if err != nil { + w.RespondWithError(errors.NewBadRequestError("Failed to get user ID", err)) + return + } + + threadID := r.URL.Query().Get("thread_id") + if threadID == "" { + w.RespondWithError(errors.NewBadRequestError("thread_id is required", nil)) + return + } + + log = log.WithValues("userID", userID, "threadID", threadID) + + log.V(1).Info("Getting CrewAI flow state") + state, err := h.DatabaseService.GetCrewAIFlowState(userID, threadID) + if err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to get CrewAI flow state", err)) + return + } + + if state == nil { + w.RespondWithError(errors.NewNotFoundError("Flow state not found", nil)) + return + } + + // Convert to response format + var stateData map[string]interface{} + if err := json.Unmarshal([]byte(state.StateData), &stateData); err != nil { + w.RespondWithError(errors.NewInternalServerError("Failed to parse state data", err)) + return + } + + statePayload := KagentFlowStatePayload{ + ThreadID: state.ThreadID, + MethodName: state.MethodName, + StateData: stateData, + } + + log.Info("Successfully retrieved CrewAI flow state") + data := api.NewResponse(statePayload, "Successfully retrieved CrewAI flow state", false) + RespondWithJSON(w, http.StatusOK, data) +} diff --git a/go/internal/httpserver/handlers/handlers.go b/go/internal/httpserver/handlers/handlers.go index 412633a73..aea95633b 100644 --- a/go/internal/httpserver/handlers/handlers.go +++ b/go/internal/httpserver/handlers/handlers.go @@ -23,6 +23,7 @@ type Handlers struct { Namespaces *NamespacesHandler Tasks *TasksHandler Checkpoints *CheckpointsHandler + CrewAI *CrewAIHandler } // Base holds common dependencies for all handlers @@ -56,5 +57,6 @@ func NewHandlers(kubeClient client.Client, defaultModelConfig types.NamespacedNa Namespaces: NewNamespacesHandler(base, watchedNamespaces), Tasks: NewTasksHandler(base), Checkpoints: NewCheckpointsHandler(base), + CrewAI: NewCrewAIHandler(base), } } diff --git a/go/internal/httpserver/server.go b/go/internal/httpserver/server.go index d35f9c7ef..5da674ea4 100644 --- a/go/internal/httpserver/server.go +++ b/go/internal/httpserver/server.go @@ -36,6 +36,7 @@ const ( APIPathA2A = "/api/a2a" APIPathFeedback = "/api/feedback" APIPathLangGraph = "/api/langgraph" + APIPathCrewAI = "/api/crewai" ) var defaultModelConfig = types.NamespacedName{ @@ -209,6 +210,13 @@ func (s *HTTPServer) setupRoutes() { s.router.HandleFunc(APIPathLangGraph+"/checkpoints/writes", adaptHandler(s.handlers.Checkpoints.HandlePutWrites)).Methods(http.MethodPost) s.router.HandleFunc(APIPathLangGraph+"/checkpoints/{thread_id}", adaptHandler(s.handlers.Checkpoints.HandleDeleteThread)).Methods(http.MethodDelete) + // CrewAI + s.router.HandleFunc(APIPathCrewAI+"/memory", adaptHandler(s.handlers.CrewAI.HandleStoreMemory)).Methods(http.MethodPost) + s.router.HandleFunc(APIPathCrewAI+"/memory", adaptHandler(s.handlers.CrewAI.HandleGetMemory)).Methods(http.MethodGet) + s.router.HandleFunc(APIPathCrewAI+"/memory", adaptHandler(s.handlers.CrewAI.HandleResetMemory)).Methods(http.MethodDelete) + s.router.HandleFunc(APIPathCrewAI+"/flows/state", adaptHandler(s.handlers.CrewAI.HandleStoreFlowState)).Methods(http.MethodPost) + s.router.HandleFunc(APIPathCrewAI+"/flows/state", adaptHandler(s.handlers.CrewAI.HandleGetFlowState)).Methods(http.MethodGet) + // A2A s.router.PathPrefix(APIPathA2A + "/{namespace}/{name}").Handler(s.config.A2AHandler) diff --git a/go/test/e2e/invoke_api_test.go b/go/test/e2e/invoke_api_test.go index c9ea485c8..c95c4da73 100644 --- a/go/test/e2e/invoke_api_test.go +++ b/go/test/e2e/invoke_api_test.go @@ -12,6 +12,7 @@ import ( "testing" "time" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8s_runtime "k8s.io/apimachinery/pkg/runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -113,45 +114,83 @@ func setupA2AClient(t *testing.T) *a2aclient.A2AClient { return a2aClient } +// extractTextFromArtifacts extracts all text content from task artifacts +func extractTextFromArtifacts(taskResult *protocol.Task) string { + var text strings.Builder + for _, artifact := range taskResult.Artifacts { + for _, part := range artifact.Parts { + if textPart, ok := part.(*protocol.TextPart); ok { + text.WriteString(textPart.Text) + } + } + } + return text.String() +} + // runSyncTest runs a synchronous message test -func runSyncTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expectedText string) { +// useArtifacts: if true, check artifacts; if false or nil, check history; +// contextID: optional context ID to maintain conversation context +func runSyncTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expectedText string, useArtifacts *bool, contextID ...string) *protocol.Task { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - msg, err := a2aClient.SendMessage(ctx, protocol.SendMessageParams{ - Message: protocol.Message{ - Kind: protocol.KindMessage, - Role: protocol.MessageRoleUser, - Parts: []protocol.Part{protocol.NewTextPart(userMessage)}, - }, - }) + msg := protocol.Message{ + Kind: protocol.KindMessage, + Role: protocol.MessageRoleUser, + Parts: []protocol.Part{protocol.NewTextPart(userMessage)}, + } + + // If contextID is provided, set it to maintain conversation context + if len(contextID) > 0 && contextID[0] != "" { + msg.ContextID = &contextID[0] + } + + result, err := a2aClient.SendMessage(ctx, protocol.SendMessageParams{Message: msg}) require.NoError(t, err) - taskResult, ok := msg.Result.(*protocol.Task) + taskResult, ok := result.Result.(*protocol.Task) require.True(t, ok) - text := a2a.ExtractText(taskResult.History[len(taskResult.History)-1]) - jsn, err := json.Marshal(taskResult) - require.NoError(t, err) - require.Contains(t, text, expectedText, string(jsn)) + + // Extract text based on useArtifacts flag + if useArtifacts != nil && *useArtifacts { + // Check artifacts (used by CrewAI flows) + text := extractTextFromArtifacts(taskResult) + require.Contains(t, text, expectedText) + } else { + // Check history (used by declarative agents) - default + text := a2a.ExtractText(taskResult.History[len(taskResult.History)-1]) + jsn, err := json.Marshal(taskResult) + require.NoError(t, err) + require.Contains(t, text, expectedText, string(jsn)) + } + + return taskResult } // runStreamingTest runs a streaming message test -func runStreamingTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expectedText string) { +// If contextID is provided, it will be included in the message to maintain conversation context +// Checks the full JSON output to support both artifacts and history from different agent types +func runStreamingTest(t *testing.T, a2aClient *a2aclient.A2AClient, userMessage, expectedText string, contextID ...string) { ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() - msg, err := a2aClient.StreamMessage(ctx, protocol.SendMessageParams{ - Message: protocol.Message{ - Kind: protocol.KindMessage, - Role: protocol.MessageRoleUser, - Parts: []protocol.Part{protocol.NewTextPart(userMessage)}, - }, - }) + msg := protocol.Message{ + Kind: protocol.KindMessage, + Role: protocol.MessageRoleUser, + Parts: []protocol.Part{protocol.NewTextPart(userMessage)}, + } + + // If contextID is provided, set it to maintain conversation context + if len(contextID) > 0 && contextID[0] != "" { + msg.ContextID = &contextID[0] + } + + stream, err := a2aClient.StreamMessage(ctx, protocol.SendMessageParams{Message: msg}) require.NoError(t, err) resultList := []protocol.StreamingMessageEvent{} var text string - for event := range msg { + for event := range stream { msgResult, ok := event.Result.(*protocol.TaskStatusUpdateEvent) if !ok { continue @@ -287,7 +326,7 @@ func TestE2EInvokeInlineAgent(t *testing.T) { // Run tests t.Run("sync_invocation", func(t *testing.T) { - runSyncTest(t, a2aClient, "List all nodes in the cluster", "kagent-control-plane") + runSyncTest(t, a2aClient, "List all nodes in the cluster", "kagent-control-plane", nil) }) t.Run("streaming_invocation", func(t *testing.T) { @@ -303,7 +342,7 @@ func TestE2EInvokeExternalAgent(t *testing.T) { // Run tests t.Run("sync_invocation", func(t *testing.T) { - runSyncTest(t, a2aClient, "What can you do?", "kebab") + runSyncTest(t, a2aClient, "What can you do?", "kebab", nil) }) t.Run("streaming_invocation", func(t *testing.T) { @@ -315,7 +354,7 @@ func TestE2EInvokeExternalAgent(t *testing.T) { authClient, err := a2aclient.NewA2AClient(a2aURL, a2aclient.WithAPIKeyAuth("user@example.com", "x-user-id")) require.NoError(t, err) - runSyncTest(t, authClient, "What can you do?", "kebab for user@example.com") + runSyncTest(t, authClient, "What can you do?", "kebab for user@example.com", nil) }) } @@ -359,10 +398,129 @@ func TestE2EInvokeDeclarativeAgentWithMcpServerTool(t *testing.T) { // Run tests t.Run("sync_invocation", func(t *testing.T) { - runSyncTest(t, a2aClient, "add 3 and 5", "8") + runSyncTest(t, a2aClient, "add 3 and 5", "8", nil) }) t.Run("streaming_invocation", func(t *testing.T) { runStreamingTest(t, a2aClient, "add 3 and 5", "8") }) } + +// This function generates a CrewAI agent that uses a mock LLM server +// Assumes that the image is built and pushed to registry, the agent can be found in python/samples/crewai/poem_flow +func generateCrewAIAgent(baseURL string) *v1alpha2.Agent { + return &v1alpha2.Agent{ + ObjectMeta: metav1.ObjectMeta{ + Name: "poem-flow-test", + Namespace: "kagent", + }, + Spec: v1alpha2.AgentSpec{ + Description: "A flow that uses a crew to generate a poem.", + Type: v1alpha2.AgentType_BYO, + BYO: &v1alpha2.BYOAgentSpec{ + Deployment: &v1alpha2.ByoDeploymentSpec{ + Image: "localhost:5001/poem-flow:latest", + SharedDeploymentSpec: v1alpha2.SharedDeploymentSpec{ + Env: []corev1.EnvVar{ + { + Name: "OPENAI_API_KEY", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "kagent-openai", + }, + Key: "OPENAI_API_KEY", + }, + }, + }, + // Inject the mock server's URL, CrewAI uses this environment variable + { + Name: "OPENAI_API_BASE", + Value: baseURL + "/v1", + }, + }, + }, + }, + }, + }, + } +} + +func TestE2EInvokeCrewAIAgent(t *testing.T) { + mockllmCfg, err := mockllm.LoadConfigFromFile("mocks/invoke_crewai_agent.json", mocks) + require.NoError(t, err) + + server := mockllm.NewServer(mockllmCfg) + baseURL, err := server.Start() + baseURL = buildK8sURL(baseURL) + require.NoError(t, err) + defer server.Stop() //nolint:errcheck + + cfg, err := config.GetConfig() + require.NoError(t, err) + + scheme := k8s_runtime.NewScheme() + err = v1alpha2.AddToScheme(scheme) + require.NoError(t, err) + err = corev1.AddToScheme(scheme) + require.NoError(t, err) + + cli, err := client.New(cfg, client.Options{ + Scheme: scheme, + }) + require.NoError(t, err) + + // Clean up any leftover agent from a previous failed run + _ = cli.Delete(t.Context(), &v1alpha2.Agent{ObjectMeta: metav1.ObjectMeta{Name: "poem-flow-test", Namespace: "kagent"}}) + + // Generate the CrewAI agent and inject the mock server's URL + agent := generateCrewAIAgent(baseURL) + + // Create the agent on the cluster + err = cli.Create(t.Context(), agent) + require.NoError(t, err) + + defer func() { + cli.Delete(t.Context(), agent) //nolint:errcheck + }() + + // Wait for the agent to become Ready + args := []string{ + "wait", + "--for", + "condition=Ready", + "--timeout=1m", + "agents.kagent.dev", + agent.Name, + "-n", + agent.Namespace, + } + + cmd := exec.CommandContext(t.Context(), "kubectl", args...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + require.NoError(t, cmd.Run()) + + // Give the agent pod extra time to fully initialize its A2A endpoint + time.Sleep(5 * time.Second) + + // Setup A2A client + a2aURL := a2aUrl(agent.Namespace, agent.Name) + a2aClient, err := a2aclient.NewA2AClient(a2aURL) + require.NoError(t, err) + + t.Run("two_turn_conversation", func(t *testing.T) { + // First turn: Generate initial poem + // Use artifacts only (true) for CrewAI flows + useArtifacts := true + taskResult1 := runSyncTest(t, a2aClient, "Generate a poem about CrewAI", "CrewAI is awesome, it makes coding fun.", &useArtifacts) + + // Second turn: Continue poem (tests persistence) + // Use the same ContextID to maintain conversation context + runSyncTest(t, a2aClient, "Continue the poem", "In harmony with the code, it flows so smooth.", &useArtifacts, taskResult1.ContextID) + }) + + t.Run("streaming_invocation", func(t *testing.T) { + runStreamingTest(t, a2aClient, "Generate a poem about CrewAI", "CrewAI is awesome, it makes coding fun.") + }) +} diff --git a/go/test/e2e/mocks/invoke_crewai_agent.json b/go/test/e2e/mocks/invoke_crewai_agent.json new file mode 100644 index 000000000..f4d912840 --- /dev/null +++ b/go/test/e2e/mocks/invoke_crewai_agent.json @@ -0,0 +1,56 @@ +{ + "openai": [ + { + "name": "continue_poem_request", + "match": { + "match_type": "contains", + "message": { + "content": "Continue this poem", + "role": "user" + } + }, + "response": { + "id": "chatcmpl-poem-2", + "object": "chat.completion", + "created": 1677652289, + "model": "gpt-4.1-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "In harmony with the code, it flows so smooth.\nBuilding AI agents, it's the truth.\nCrewAI rocks, day by day.\nMaking development a joyful play." + }, + "finish_reason": "stop" + } + ] + } + }, + { + "name": "generate_poem_request", + "match": { + "match_type": "contains", + "message": { + "content": "Write a poem about how CrewAI is awesome.", + "role": "user" + } + }, + "response": { + "id": "chatcmpl-poem-1", + "object": "chat.completion", + "created": 1677652288, + "model": "gpt-4.1-mini", + "choices": [ + { + "index": 0, + "message": { + "role": "assistant", + "content": "CrewAI is awesome, it makes coding fun.\nWith agents and tools, the work is done.\nBuilding AI flows, it's a breeze.\nIn the world of code, it brings ease." + }, + "finish_reason": "stop" + } + ] + } + } + ] +} diff --git a/python/Makefile b/python/Makefile index 7037b88c3..7aca8b1bd 100644 --- a/python/Makefile +++ b/python/Makefile @@ -40,4 +40,4 @@ research-crew-sample: .PHONY: poem-flow-sample poem-flow-sample: - docker build . -f samples/crewai/poem_flow/Dockerfile --tag localhost:5001/poem-flow:latest --push \ No newline at end of file + docker build . -f samples/crewai/poem_flow/Dockerfile --tag localhost:5001/poem-flow:latest --push diff --git a/python/packages/kagent-core/src/kagent/core/tracing/_utils.py b/python/packages/kagent-core/src/kagent/core/tracing/_utils.py index a7477d1e2..6e52337cd 100644 --- a/python/packages/kagent-core/src/kagent/core/tracing/_utils.py +++ b/python/packages/kagent-core/src/kagent/core/tracing/_utils.py @@ -27,7 +27,6 @@ def configure(fastapi_app: FastAPI | None = None): # Configure tracing if enabled if tracing_enabled: logging.info("Enabling tracing") - tracer_provider = TracerProvider(resource=resource) # Check new env var first, fall back to old one for backward compatibility trace_endpoint = os.getenv("OTEL_TRACING_EXPORTER_OTLP_ENDPOINT") or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") logging.info("Trace endpoint: %s", trace_endpoint or "") @@ -35,8 +34,20 @@ def configure(fastapi_app: FastAPI | None = None): processor = BatchSpanProcessor(OTLPSpanExporter(endpoint=trace_endpoint)) else: processor = BatchSpanProcessor(OTLPSpanExporter()) - tracer_provider.add_span_processor(processor) - trace.set_tracer_provider(tracer_provider) + + # Check if a TracerProvider already exists (e.g., set by CrewAI) + current_provider = trace.get_tracer_provider() + if isinstance(current_provider, TracerProvider): + # TracerProvider already exists, just add our processor to it + current_provider.add_span_processor(processor) + logging.info("Added OTLP processor to existing TracerProvider") + else: + # No provider set, create new one + tracer_provider = TracerProvider(resource=resource) + tracer_provider.add_span_processor(processor) + trace.set_tracer_provider(tracer_provider) + logging.info("Created new TracerProvider") + HTTPXClientInstrumentor().instrument() if fastapi_app: FastAPIInstrumentor().instrument_app(fastapi_app) diff --git a/python/packages/kagent-crewai/README.md b/python/packages/kagent-crewai/README.md index f29a1600a..463d387eb 100644 --- a/python/packages/kagent-crewai/README.md +++ b/python/packages/kagent-crewai/README.md @@ -1,12 +1,14 @@ # KAgent CrewAI Integration -This package provides CrewAI integration for KAgent with A2A (Agent-to-Agent) server support. +This package provides CrewAI integration for KAgent with A2A (Agent-to-Agent) server support and session-aware memory storage. ## Features - **A2A Server Integration**: Compatible with KAgent's Agent-to-Agent protocol - **Event Streaming**: Real-time streaming of crew execution events - **FastAPI Integration**: Ready-to-deploy web server for agent execution +- **Session-aware Memory**: Store and retrieve agent memories scoped by session ID +- **Flow State Persistence**: Save and restore CrewAI Flow states to KAgent backend ## Quick Start @@ -30,6 +32,40 @@ fastapi_app = app.build() uvicorn.run(fastapi_app, host="0.0.0.0", port=8080) ``` +## User Guide + +### Creating Tasks + +For this version, tasks should either accept a single `input` parameter (string) or no parameters at all. Future versions will allow JSON / structured input where you can replace multiple values in your task to make it more flexible. + +For example, you can create a task like follow with yaml (see CrewAI docs) and when triggered from the A2A client, the `input` field will be populated with the input text if provided. + +```yaml +research_task: + description: > + Research topics on {input} and provide a summary. +``` + +This is equivalent of `crew.kickoff(inputs={"input": "your input text"})` when triggering agents manually. + +### Session-aware Memory + +#### CrewAI Crews + +Session scoped memory is implemented using the `LongTermMemory` interface in CrewAI. If you wish to share memories between agents, you must interact with them in the same session to share long term memory so they can search and access the previous conversation history (because agent ID is volatile, we must use session ID). You can enable this by setting `memory=True` when creating your CrewAI crew. Note that this memory is also scoped by user ID so different users will not see each other's memories. + +Our KAgent backend is designed to handle long term memory saving and retrieval with the identical logic as `LTMSQLiteStorage` which is used by default for `LongTermMemory` in CrewAI, with the addition of session and user scoping. It will search the LTM items based on the task description and return the most relevant items (sorted and limited). + +> Note that when you set `memory=True`, you are responsible to ensure that short term and entity memory are configured properly (e.g. with `OPENAI_API_KEY` or set your own providers). The KAgent CrewAI integration only handles long term memory. + +#### CrewAI Flows + +In flow mode, we implement memory similar to checkpointing in LangGraph so that the flow state is persisted to the KAgent backend after each method finishes execution. We consider each session to be a single flow execution, so you can reuse state within the same session by enabling `@persist()` for flow or methods. We do not manage `LongTermMemory` for crews inside a flow since flow is designed to be very customizable. You are responsible for implementing your own memory management for all the crew you use in the flow. + +### Tracing + +To enable tracing, follow [this guide](https://kagent.dev/docs/kagent/getting-started/tracing#installing-kagent) on Kagent docs. Once you have Jaeger (or any OTLP-compatible backend) running and the kagent settings updated, your CrewAI agent will automatically send traces to the configured backend. + ## Architecture The package mirrors the structure of `kagent-adk` and `kagent-langgraph` but uses CrewAI for multi-agent orchestration: @@ -37,11 +73,8 @@ The package mirrors the structure of `kagent-adk` and `kagent-langgraph` but use - **CrewAIAgentExecutor**: Executes CrewAI workflows within A2A protocol - **KAgentApp**: FastAPI application builder with A2A integration - **Event Converters**: Translates CrewAI events into A2A events for streaming. +- **Session-aware Memory**: Custom persistence backend scoped by session ID and user ID, works with Crew and Flow mode by leveraging memory and state persistence. ## Deployment The uses the same deployment approach as other KAgent A2A applications (ADK / LangGraph). You can refer to `samples/crewai/` for examples. - -## Note - -Due to the current design of the package, your tasks in CrewAI should expect a `input` parameter which contains the input text if available. We will support JSON input for more native CrewAI integration in the future. You can check out an example in `samples/crewai/research-crew/src/research_crew/config/tasks.yaml`. diff --git a/python/packages/kagent-crewai/pyproject.toml b/python/packages/kagent-crewai/pyproject.toml index 601cdf6b8..ab1af73a7 100644 --- a/python/packages/kagent-crewai/pyproject.toml +++ b/python/packages/kagent-crewai/pyproject.toml @@ -15,8 +15,10 @@ dependencies = [ "pydantic>=2.0.0", "typing-extensions>=4.0.0", "uvicorn>=0.20.0", - "a2a-sdk>=0.3.1", + "a2a-sdk[http-server]>=0.3.1", "kagent-core", + "opentelemetry-instrumentation-crewai>=0.47.3", + "google-genai>=1.21.1" ] [project.optional-dependencies] diff --git a/python/packages/kagent-crewai/src/kagent/crewai/__init__.py b/python/packages/kagent-crewai/src/kagent/crewai/__init__.py new file mode 100644 index 000000000..63755a986 --- /dev/null +++ b/python/packages/kagent-crewai/src/kagent/crewai/__init__.py @@ -0,0 +1,4 @@ +from ._a2a import KAgentApp + +__all__ = ["KAgentApp"] +__version__ = "0.1.0" diff --git a/python/packages/kagent-crewai/src/kagent/crewai/_a2a.py b/python/packages/kagent-crewai/src/kagent/crewai/_a2a.py index e048d907f..73adc1fc3 100644 --- a/python/packages/kagent-crewai/src/kagent/crewai/_a2a.py +++ b/python/packages/kagent-crewai/src/kagent/crewai/_a2a.py @@ -1,5 +1,6 @@ import faulthandler import logging +import os from typing import Union import httpx @@ -8,6 +9,7 @@ from a2a.types import AgentCard from fastapi import FastAPI, Request from fastapi.responses import PlainTextResponse +from opentelemetry.instrumentation.crewai import CrewAIInstrumentor from crewai import Crew, Flow from kagent.core import KAgentConfig, configure_tracing @@ -54,6 +56,7 @@ def build(self) -> FastAPI: crew=self._crew, app_name=self.config.app_name, config=self.executor_config, + http_client=http_client, ) task_store = KAgentTaskStore(http_client) @@ -78,6 +81,10 @@ def build(self) -> FastAPI: if self.tracing: configure_tracing(app) + # Setup crewAI instrumentor separately as core configure does not include it + tracing_enabled = os.getenv("OTEL_TRACING_ENABLED", "false").lower() == "true" + if tracing_enabled: + CrewAIInstrumentor().instrument() app.add_route("/health", methods=["GET"], route=def_health_check) app.add_route("/thread_dump", methods=["GET"], route=thread_dump) diff --git a/python/packages/kagent-crewai/src/kagent/crewai/_executor.py b/python/packages/kagent-crewai/src/kagent/crewai/_executor.py index 069455acc..bcabe4b4a 100644 --- a/python/packages/kagent-crewai/src/kagent/crewai/_executor.py +++ b/python/packages/kagent-crewai/src/kagent/crewai/_executor.py @@ -3,6 +3,7 @@ from datetime import datetime, timezone from typing import Union, override +import httpx from a2a.server.agent_execution import AgentExecutor from a2a.server.agent_execution.context import RequestContext from a2a.server.events.event_queue import EventQueue @@ -21,8 +22,11 @@ from pydantic import BaseModel from crewai import Crew, Flow +from crewai.memory import LongTermMemory from ._listeners import A2ACrewAIListener +from ._memory import KagentMemoryStorage +from ._state import KagentFlowPersistence logger = logging.getLogger(__name__) @@ -38,11 +42,13 @@ def __init__( crew: Union[Crew, Flow], app_name: str, config: CrewAIAgentExecutorConfig | None = None, + http_client: httpx.AsyncClient, ): super().__init__() self._crew = crew self.app_name = app_name self._config = config or CrewAIAgentExecutorConfig() + self._http_client = http_client @override async def cancel(self, context: RequestContext, event_queue: EventQueue): @@ -101,12 +107,37 @@ async def execute( user_input = context.get_user_input() inputs = {"input": user_input} if user_input else {} + session_id = getattr(context, "session_id", context.context_id) + user_id = getattr(context, "user_id", "admin@kagent.dev") + if isinstance(self._crew, Flow): flow_class = type(self._crew) + persistence = KagentFlowPersistence( + thread_id=session_id, + user_id=user_id, + base_url=str(self._http_client.base_url), + ) flow_instance = flow_class() - result = await flow_instance.kickoff_async(inputs=inputs) + flow_instance.persistence = persistence + + # setting "id" in flow input will enable reusing persisted flow state + # if no flow state is persisted or if persistence is not enabled, this works like a normal kickoff + inputs["id"] = session_id + + # output_text will be None if the last method in the flow does not return anything but updates the state instead + output_text = await flow_instance.kickoff_async(inputs=inputs) + result_text = output_text or flow_instance.state.model_dump_json() else: + if self._crew.memory: + self._crew.long_term_memory = LongTermMemory( + KagentMemoryStorage( + thread_id=session_id, + user_id=user_id, + base_url=str(self._http_client.base_url), + ) + ) result = await self._crew.kickoff_async(inputs=inputs) + result_text = str(result.raw or "No response was generated.") await event_queue.enqueue_event( TaskArtifactUpdateEvent( @@ -115,7 +146,7 @@ async def execute( context_id=context.context_id, artifact=Artifact( artifact_id=str(uuid.uuid4()), - parts=[Part(TextPart(text=str(result.raw)))], + parts=[Part(TextPart(text=result_text))], ), ) ) diff --git a/python/packages/kagent-crewai/src/kagent/crewai/_memory.py b/python/packages/kagent-crewai/src/kagent/crewai/_memory.py new file mode 100644 index 000000000..aef7d1628 --- /dev/null +++ b/python/packages/kagent-crewai/src/kagent/crewai/_memory.py @@ -0,0 +1,110 @@ +import logging +from typing import Any, Dict, List + +import httpx +from pydantic import BaseModel + + +class KagentMemoryPayload(BaseModel): + thread_id: str + user_id: str + memory_data: Dict[str, Any] + + +class KagentMemoryResponse(BaseModel): + data: List[KagentMemoryPayload] + + +class KagentMemoryStorage: + """ + KagentMemoryStorage is a custom storage class for CrewAI's LongTermMemory. + It persists memory items to the Kagent backend, scoped by thread_id and user_id. + """ + + def __init__(self, thread_id: str, user_id: str, base_url: str): + self.thread_id = thread_id + self.user_id = user_id + self.base_url = base_url + + def save(self, task_description: str, metadata: dict, timestamp: str, score: float) -> None: + """ + Saves a memory item to the Kagent backend. + The agent_id is expected to be in the metadata. + """ + url = f"{self.base_url}/api/crewai/memory" + payload = KagentMemoryPayload( + thread_id=self.thread_id, + user_id=self.user_id, + memory_data={ + "task_description": task_description, + "score": score, + "metadata": metadata, + "datetime": timestamp, + }, + ) + + logging.info(f"Saving memory to Kagent backend: {payload}") + + try: + with httpx.Client() as client: + response = client.post(url, json=payload.model_dump(), headers={"X-User-ID": self.user_id}) + response.raise_for_status() + except httpx.HTTPError as e: + logging.error(f"Error saving memory to Kagent backend: {e}") + raise + + def load(self, task_description: str, latest_n: int) -> List[Dict[str, Any]] | None: + """ + Loads memory items from the Kagent backend. + Returns memory items matching the task description, up to latest_n items. + """ + url = f"{self.base_url}/api/crewai/memory" + # Use task_description as the query parameter to search across all agents for this session + params = {"q": task_description, "limit": latest_n, "thread_id": self.thread_id} + + logging.debug(f"Loading memory from Kagent backend with params: {params}") + try: + with httpx.Client() as client: + response = client.get(url, params=params, headers={"X-User-ID": self.user_id}) + response.raise_for_status() + + # Parse response and convert to the format expected by the original interface + memory_response = KagentMemoryResponse.model_validate_json(response.text) + if not memory_response.data: + return None + + # Convert to the format expected by LongTermMemory: list of dicts with metadata, datetime, score + results = [] + for item in memory_response.data: + memory_data = item.memory_data + # The memory_data contains: task_description, score, metadata, datetime + # We want to return items in the format that LongTermMemory expects + results.append( + { + "metadata": memory_data.get("metadata", {}), + "datetime": memory_data.get("datetime", ""), + "score": memory_data.get("score", 0.0), + } + ) + + return results if results else None + except httpx.HTTPError as e: + logging.error(f"Error loading memory from Kagent backend: {e}") + return None + + def reset(self) -> None: + """ + Resets the memory storage by deleting all memories for this session. + """ + url = f"{self.base_url}/api/crewai/memory" + params = {"thread_id": self.thread_id} + + logging.info(f"Resetting memory for session {self.thread_id}") + try: + with httpx.Client() as client: + response = client.delete(url, params=params, headers={"X-User-ID": self.user_id}) + response.raise_for_status() + logging.info(f"Successfully reset memory for session {self.thread_id}") + except httpx.HTTPError as e: + logging.error(f"Error resetting memory for session {self.thread_id}: {e}") + raise diff --git a/python/packages/kagent-crewai/src/kagent/crewai/_state.py b/python/packages/kagent-crewai/src/kagent/crewai/_state.py new file mode 100644 index 000000000..d86aed92d --- /dev/null +++ b/python/packages/kagent-crewai/src/kagent/crewai/_state.py @@ -0,0 +1,70 @@ +import logging +from typing import Any, Dict, Optional, Union + +import httpx +from pydantic import BaseModel, Field + +from crewai.flow.persistence import FlowPersistence + + +class KagentFlowStatePayload(BaseModel): + thread_id: str + flow_uuid: str + method_name: str + state_data: Dict[str, Any] + + +class KagentFlowStateResponse(BaseModel): + data: KagentFlowStatePayload + + +class KagentFlowPersistence(FlowPersistence): + """ + KagentFlowPersistence is a custom persistence class for CrewAI Flows. + It saves and loads the flow state to the Kagent backend. + """ + + def __init__(self, thread_id: str, user_id: str, base_url: str): + self.thread_id = thread_id + self.user_id = user_id + self.base_url = base_url + + def init_db(self) -> None: + """This is handled by the Kagent backend, so no action is needed here.""" + pass + + def save_state(self, flow_uuid: str, method_name: str, state_data: Union[Dict[str, Any], BaseModel]) -> None: + """Saves the flow state to the Kagent backend.""" + url = f"{self.base_url}/api/crewai/flows/state" + payload = KagentFlowStatePayload( + thread_id=self.thread_id, + flow_uuid=flow_uuid, + method_name=method_name, + state_data=state_data.model_dump() if isinstance(state_data, BaseModel) else state_data, + ) + logging.info(f"Saving flow state to Kagent backend: {payload}") + + try: + with httpx.Client() as client: + response = client.post(url, json=payload.model_dump(), headers={"X-User-ID": self.user_id}) + response.raise_for_status() + except httpx.HTTPError as e: + logging.error(f"Error saving flow state to Kagent backend: {e}") + raise + + def load_state(self, flow_uuid: str) -> Optional[Dict[str, Any]]: + """Loads the flow state from the Kagent backend.""" + url = f"{self.base_url}/api/crewai/flows/state" + params = {"thread_id": self.thread_id, "flow_uuid": flow_uuid} + logging.info(f"Loading flow state from Kagent backend with params: {params}") + + try: + with httpx.Client() as client: + response = client.get(url, params=params, headers={"X-User-ID": self.user_id}) + if response.status_code == 404: + return None + response.raise_for_status() + return KagentFlowStateResponse.model_validate_json(response.text).data.state_data + except httpx.HTTPError as e: + logging.error(f"Error loading flow state from Kagent backend: {e}") + return None diff --git a/python/samples/crewai/poem_flow/Dockerfile b/python/samples/crewai/poem_flow/Dockerfile index 7cc1c03bc..e29ddc1e2 100644 --- a/python/samples/crewai/poem_flow/Dockerfile +++ b/python/samples/crewai/poem_flow/Dockerfile @@ -19,11 +19,12 @@ COPY .python-version .python-version COPY uv.lock uv.lock # Install dependencies -RUN uv venv && uv sync --locked --no-dev --package kagent-crewai +RUN uv venv && uv sync --locked --no-dev --package poem-flow # Set environment variables ENV PORT=8080 ENV VIRTUAL_ENV=/app/.venv +ENV PATH="/app/.venv/bin:$PATH" # Expose port EXPOSE 8080 @@ -33,4 +34,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Run the application -CMD ["uv", "run", "samples/crewai/poem_flow/src/poem_flow/main.py"] +CMD ["python", "samples/crewai/poem_flow/src/poem_flow/main.py"] diff --git a/python/samples/crewai/poem_flow/README.md b/python/samples/crewai/poem_flow/README.md index a6418f5eb..234768edf 100644 --- a/python/samples/crewai/poem_flow/README.md +++ b/python/samples/crewai/poem_flow/README.md @@ -4,6 +4,8 @@ This sample demonstrates how to use the `kagent-crewai` toolkit to run a CrewAI This example is generated directly from the `crewai create flow poem_flow` command. +If you wish to use the memory persistence integration with KAgent, edit `poem_flow.py` and set `@persist()` on the flow or methods you want to persist. + ## Quick Start 1. **Build the agent image**: @@ -19,8 +21,8 @@ This example is generated directly from the `crewai create flow poem_flow` comma 2. **Create secrets for API keys**: ```bash - kubectl create secret generic kagent-google -n kagent \ - --from-literal=GOOGLE_API_KEY=$GOOGLE_API_KEY \ + kubectl create secret generic kagent-openai -n kagent \ + --from-literal=OPENAI_API_KEY=$OPENAI_API_KEY \ --dry-run=client -o yaml | kubectl apply -f - ``` @@ -47,7 +49,7 @@ When interacting with the agent, you do not need to provide any input because th ```bash export KAGENT_URL=http://localhost:8080 - export GEMINI_API_KEY="sk-..." + export OPENAI_API_KEY="..." ``` 3. **Run the agent server**: diff --git a/python/samples/crewai/poem_flow/agent.yaml b/python/samples/crewai/poem_flow/agent.yaml index c77901c0f..051279c3c 100644 --- a/python/samples/crewai/poem_flow/agent.yaml +++ b/python/samples/crewai/poem_flow/agent.yaml @@ -10,9 +10,8 @@ spec: deployment: image: localhost:5001/poem-flow:latest env: - # Note that CrewAI LLM expects GEMINI_API_KEY instead of GOOGLE_API_KEY - - name: GEMINI_API_KEY + - name: OPENAI_API_KEY valueFrom: secretKeyRef: - name: kagent-google - key: GOOGLE_API_KEY + name: kagent-openai + key: OPENAI_API_KEY diff --git a/python/samples/crewai/poem_flow/pyproject.toml b/python/samples/crewai/poem_flow/pyproject.toml index 119cb24d3..d681717f7 100644 --- a/python/samples/crewai/poem_flow/pyproject.toml +++ b/python/samples/crewai/poem_flow/pyproject.toml @@ -9,12 +9,6 @@ dependencies = [ "crewai[tools]>=0.193.2,<1.0.0" ] -[project.scripts] -kickoff = "poem_flow.main:kickoff" -run_crew = "poem_flow.main:kickoff" -plot = "poem_flow.main:plot" -poem_flow = "poem_flow.main:main" - [build-system] requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" diff --git a/python/samples/crewai/poem_flow/src/poem_flow/crews/poem_crew/config/agents.yaml b/python/samples/crewai/poem_flow/src/poem_flow/crews/poem_crew/config/agents.yaml index e16f1a9e4..bfdff0c11 100644 --- a/python/samples/crewai/poem_flow/src/poem_flow/crews/poem_crew/config/agents.yaml +++ b/python/samples/crewai/poem_flow/src/poem_flow/crews/poem_crew/config/agents.yaml @@ -9,4 +9,3 @@ poem_writer: in a beautiful and engaging way. Known for your ability to craft poems that resonate with readers, you bring a unique perspective and artistic flair to every piece you write. - llm: gemini/gemini-2.0-flash diff --git a/python/samples/crewai/poem_flow/src/poem_flow/main.py b/python/samples/crewai/poem_flow/src/poem_flow/main.py index b31da7570..225309f5e 100644 --- a/python/samples/crewai/poem_flow/src/poem_flow/main.py +++ b/python/samples/crewai/poem_flow/src/poem_flow/main.py @@ -5,7 +5,7 @@ from random import randint import uvicorn -from crewai.flow import Flow, listen, start +from crewai.flow import Flow, listen, persist, start from kagent.crewai import KAgentApp from pydantic import BaseModel @@ -22,25 +22,45 @@ class PoemState(BaseModel): poem: str = "" +# The persist decorator will persist all the flow method states to KAgent backend +# Alternatively, you can persist only certain methods by adding @persist to those methods +@persist(verbose=True) class PoemFlow(Flow[PoemState]): @start() def generate_sentence_count(self): + logging.info(f"Flow starting. Initial state: {self.state}") logging.info("Generating sentence count") self.state.sentence_count = randint(1, 5) @listen(generate_sentence_count) def generate_poem(self): logging.info("Generating poem") - result = PoemCrew().crew().kickoff(inputs={"sentence_count": self.state.sentence_count}) - - logging.info("Poem generated", result.raw) - self.state.poem = result.raw + poem_crew = PoemCrew().crew() + + if self.state.poem: + logging.info("Continuing existing poem...") + continuation_task = poem_crew.tasks[0] + continuation_task.description = f"""Continue this poem about how CrewAI is awesome, adding {self.state.sentence_count} more sentences. +Keep the tone funny and light-hearted. Respond only with the new lines of the poem, do not repeat the existing poem. + +EXISTING POEM: +--- +{self.state.poem} +---""" + continuation_task.expected_output = f"The next {self.state.sentence_count} sentences of the poem." + result = poem_crew.kickoff() + self.state.poem += f"\n{result.raw}" + else: + logging.info("Starting a new poem...") + result = poem_crew.kickoff(inputs={"sentence_count": self.state.sentence_count}) + self.state.poem = result.raw + + logging.info(f"Poem state is now:\n{self.state.poem}") @listen(generate_poem) def save_poem(self): logging.info("Saving poem") - with open("output/poem.txt", "w") as f: - f.write(self.state.poem) + return self.state.poem # These two methods are for the script that crewai CLI uses diff --git a/python/samples/crewai/research-crew/Dockerfile b/python/samples/crewai/research-crew/Dockerfile index 7a0103403..8fe33d48a 100644 --- a/python/samples/crewai/research-crew/Dockerfile +++ b/python/samples/crewai/research-crew/Dockerfile @@ -19,11 +19,12 @@ COPY .python-version .python-version COPY uv.lock uv.lock # Install dependencies -RUN uv venv && uv sync --locked --no-dev --package kagent-crewai +RUN uv venv && uv sync --locked --no-dev --package research-crew # Set environment variables ENV PORT=8080 ENV VIRTUAL_ENV=/app/.venv +ENV PATH="/app/.venv/bin:$PATH" # Expose port EXPOSE 8080 @@ -33,4 +34,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/health || exit 1 # Run the application -CMD ["uv", "run", "samples/crewai/research-crew/src/research_crew/main.py"] +CMD ["python", "samples/crewai/research-crew/src/research_crew/main.py"] diff --git a/python/samples/crewai/research-crew/README.md b/python/samples/crewai/research-crew/README.md index 2fb614fa9..0dd4f7ee5 100644 --- a/python/samples/crewai/research-crew/README.md +++ b/python/samples/crewai/research-crew/README.md @@ -4,6 +4,8 @@ This sample demonstrates how to use the `kagent-crewai` toolkit to run a CrewAI It follows the standard CrewAI project structure and developer experience, allowing you to define your agents and tasks in Python. +If you wish to use the memory persistence integration with KAgent, edit `crew.py` and set `memory=True` when creating the crew. + ## Features - Research crew with multiple specialized agents @@ -31,8 +33,8 @@ It follows the standard CrewAI project structure and developer experience, allow 3. **Create secrets for API keys**: ```bash - kubectl create secret generic kagent-google -n kagent \ - --from-literal=GOOGLE_API_KEY=$GOOGLE_API_KEY \ + kubectl create secret generic kagent-openai -n kagent \ + --from-literal=OPENAI_API_KEY=$OPENAI_API_KEY \ --dry-run=client -o yaml | kubectl apply -f - kubectl create secret generic kagent-serper -n kagent \ @@ -42,9 +44,9 @@ It follows the standard CrewAI project structure and developer experience, allow 4. **Deploy the agent**: - ```bash - kubectl apply -f agent.yaml - ``` +```bash +kubectl apply -f agent.yaml +``` ## Local Development @@ -61,7 +63,7 @@ It follows the standard CrewAI project structure and developer experience, allow ```bash export KAGENT_URL=http://localhost:8080 - export GEMINI_API_KEY="sk-..." + export OPENAI_API_KEY="sk-..." export SERPER_API_KEY="..." ``` diff --git a/python/samples/crewai/research-crew/agent.yaml b/python/samples/crewai/research-crew/agent.yaml index 742cf0825..c821fe01a 100644 --- a/python/samples/crewai/research-crew/agent.yaml +++ b/python/samples/crewai/research-crew/agent.yaml @@ -10,12 +10,11 @@ spec: deployment: image: localhost:5001/research-crew:latest env: - # Note that CrewAI LLM expects GEMINI_API_KEY instead of GOOGLE_API_KEY - - name: GEMINI_API_KEY + - name: OPENAI_API_KEY valueFrom: secretKeyRef: - name: kagent-google - key: GOOGLE_API_KEY + name: kagent-openai + key: OPENAI_API_KEY - name: SERPER_API_KEY valueFrom: secretKeyRef: diff --git a/python/samples/crewai/research-crew/pyproject.toml b/python/samples/crewai/research-crew/pyproject.toml index a9cab8db0..4c38d8a8f 100644 --- a/python/samples/crewai/research-crew/pyproject.toml +++ b/python/samples/crewai/research-crew/pyproject.toml @@ -13,8 +13,5 @@ dependencies = [ requires = ["setuptools>=61.0", "wheel"] build-backend = "setuptools.build_meta" -[project.scripts] -research-crew = "research_crew.main:main" - [tool.uv.sources] kagent-crewai = { workspace = true } diff --git a/python/samples/crewai/research-crew/src/research_crew/config/agents.yaml b/python/samples/crewai/research-crew/src/research_crew/config/agents.yaml index 4cc079f56..969d9141d 100644 --- a/python/samples/crewai/research-crew/src/research_crew/config/agents.yaml +++ b/python/samples/crewai/research-crew/src/research_crew/config/agents.yaml @@ -10,7 +10,6 @@ researcher: finding relevant information from various sources. You excel at organizing information in a clear and structured manner, making complex topics accessible to others. - llm: gemini/gemini-2.0-flash analyst: role: > @@ -23,4 +22,3 @@ analyst: and technical writing. You have a talent for identifying patterns and extracting meaningful insights from research data, then communicating those insights effectively through well-crafted reports. - llm: gemini/gemini-2.0-flash \ No newline at end of file diff --git a/python/samples/crewai/research-crew/src/research_crew/crew.py b/python/samples/crewai/research-crew/src/research_crew/crew.py index 5d7d2a841..784eaec07 100644 --- a/python/samples/crewai/research-crew/src/research_crew/crew.py +++ b/python/samples/crewai/research-crew/src/research_crew/crew.py @@ -50,4 +50,6 @@ def crew(self) -> Crew: tasks=self.tasks, process=Process.sequential, verbose=True, + # NOTE: Uncomment this line to enable long term memory across agents in the same session + # memory=True, ) diff --git a/python/uv.lock b/python/uv.lock index b3dd7f090..bb6762294 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -38,6 +38,13 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/cb/03315694ea2f9536c796767da0a1dd6d0b7c48df4badbab19c2c9c570741/a2a_sdk-0.3.3-py3-none-any.whl", hash = "sha256:d433c7f13a7a4b426e3aef798f9ef6eff0d3aea68c9a595634af865111b5c7c2", size = 135159, upload-time = "2025-08-25T18:32:08.18Z" }, ] +[package.optional-dependencies] +http-server = [ + { name = "fastapi" }, + { name = "sse-starlette" }, + { name = "starlette" }, +] + [[package]] name = "absolufy-imports" version = "0.3.1" @@ -421,38 +428,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, ] -[[package]] -name = "chroma-hnswlib" -version = "0.7.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/73/09/10d57569e399ce9cbc5eee2134996581c957f63a9addfa6ca657daf006b8/chroma_hnswlib-0.7.6.tar.gz", hash = "sha256:4dce282543039681160259d29fcde6151cc9106c6461e0485f57cdccd83059b7", size = 32256, upload-time = "2024-07-22T20:19:29.259Z" } - [[package]] name = "chromadb" -version = "0.5.23" +version = "1.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "bcrypt" }, { name = "build" }, - { name = "chroma-hnswlib" }, - { name = "fastapi" }, { name = "grpcio" }, { name = "httpx" }, { name = "importlib-resources" }, + { name = "jsonschema" }, { name = "kubernetes" }, { name = "mmh3" }, { name = "numpy" }, { name = "onnxruntime" }, { name = "opentelemetry-api" }, { name = "opentelemetry-exporter-otlp-proto-grpc" }, - { name = "opentelemetry-instrumentation-fastapi" }, { name = "opentelemetry-sdk" }, { name = "orjson" }, { name = "overrides" }, { name = "posthog" }, + { name = "pybase64" }, { name = "pydantic" }, { name = "pypika" }, { name = "pyyaml" }, @@ -464,9 +461,13 @@ dependencies = [ { name = "typing-extensions" }, { name = "uvicorn", extra = ["standard"] }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/64/28daa773f784bcd18de944fe26ed301de844d6ee17188e26a9d6b4baf122/chromadb-0.5.23.tar.gz", hash = "sha256:360a12b9795c5a33cb1f839d14410ccbde662ef1accd36153b0ae22312edabd1", size = 33700455, upload-time = "2024-12-05T06:31:19.81Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7f/48/11851dddeadad6abe36ee071fedc99b5bdd2c324df3afa8cb952ae02798b/chromadb-1.1.1.tar.gz", hash = "sha256:ebfce0122753e306a76f1e291d4ddaebe5f01b5979b97ae0bc80b1d4024ff223", size = 1338109, upload-time = "2025-10-05T02:49:14.834Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/92/8c/a9eb95a28e6c35a0122417976a9d435eeaceb53f596a8973e33b3dd4cfac/chromadb-0.5.23-py3-none-any.whl", hash = "sha256:ffe5bdd7276d12cb682df0d38a13aa37573e6a3678e71889ac45f539ae05ad7e", size = 628347, upload-time = "2024-12-05T06:31:17.231Z" }, + { url = "https://files.pythonhosted.org/packages/39/59/0d881a9b7eb63d8d2446cf67fcbb53fb8ae34991759d2b6024a067e90a9a/chromadb-1.1.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:27fe0e25ef0f83fb09c30355ab084fe6f246808a7ea29e8c19e85cf45785b90d", size = 19175479, upload-time = "2025-10-05T02:49:12.525Z" }, + { url = "https://files.pythonhosted.org/packages/94/4f/5a9fa317c84c98e70af48f74b00aa25589626c03a0428b4381b2095f3d73/chromadb-1.1.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:95aed58869683f12e7dcbf68b039fe5f576dbe9d1b86b8f4d014c9d077ccafd2", size = 18267188, upload-time = "2025-10-05T02:49:09.236Z" }, + { url = "https://files.pythonhosted.org/packages/45/1a/02defe2f1c8d1daedb084bbe85f5b6083510a3ba192ed57797a3649a4310/chromadb-1.1.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06776dad41389a00e7d63d936c3a15c179d502becaf99f75745ee11b062c9b6a", size = 18855754, upload-time = "2025-10-05T02:49:03.299Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/80be82717e5dc19839af24558494811b6f2af2b261a8f21c51b872193b09/chromadb-1.1.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bba0096a7f5e975875ead23a91c0d41d977fbd3767f60d3305a011b0ace7afd3", size = 19893681, upload-time = "2025-10-05T02:49:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/2d/6e/956e62975305a4e31daf6114a73b3b0683a8f36f8d70b20aabd466770edb/chromadb-1.1.1-cp39-abi3-win_amd64.whl", hash = "sha256:a77aa026a73a18181fd89bbbdb86191c9a82fd42aa0b549ff18d8cae56394c8b", size = 19844042, upload-time = "2025-10-05T02:49:16.925Z" }, ] [[package]] @@ -513,7 +514,7 @@ wheels = [ [[package]] name = "crewai" -version = "0.193.2" +version = "0.201.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "appdirs" }, @@ -525,7 +526,6 @@ dependencies = [ { name = "json5" }, { name = "jsonref" }, { name = "litellm" }, - { name = "onnxruntime" }, { name = "openai" }, { name = "openpyxl" }, { name = "opentelemetry-api" }, @@ -534,6 +534,7 @@ dependencies = [ { name = "pdfplumber" }, { name = "portalocker" }, { name = "pydantic" }, + { name = "pydantic-settings" }, { name = "pyjwt" }, { name = "python-dotenv" }, { name = "pyvis" }, @@ -543,9 +544,9 @@ dependencies = [ { name = "tomli-w" }, { name = "uv" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/ac/329d62f5abfb24ffb096c041469ba24190feca1d5651084d5c332939b33f/crewai-0.193.2.tar.gz", hash = "sha256:239f1d299bbf493e76778434f6476604b585e1f228e2c75d39983a39a1522275", size = 6563484, upload-time = "2025-09-20T21:09:16.553Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c6/85/fee06c332662b025762b89431f232b564a8b078ccd9eb935f0d2ed264eb9/crewai-0.201.1.tar.gz", hash = "sha256:8ed336a7c31c8eb2beb312a94e31c6b8ca54dc5178a76413bfcb5707eb5481c6", size = 6596906, upload-time = "2025-09-26T16:57:53.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/81/699782ddfe3c18f6954355f4ec53d73d9c88a88bc5433f590163549a0fbf/crewai-0.193.2-py3-none-any.whl", hash = "sha256:cad4d6a5f32e902a390ca3fc84698839e7720c1ae7acdba002da9a18405a01c8", size = 431559, upload-time = "2025-09-20T21:09:08.676Z" }, + { url = "https://files.pythonhosted.org/packages/e2/5e/1f9696284c3d5af770b9ea3bfa5ce096d08a94cdc999f9182ca33d5ac888/crewai-0.201.1-py3-none-any.whl", hash = "sha256:798cb882da1d113b0322a574b9ae4b893821fd42a952f9ebcb239d66a68ee5de", size = 472588, upload-time = "2025-09-26T16:57:51.671Z" }, ] [package.optional-dependencies] @@ -555,20 +556,14 @@ tools = [ [[package]] name = "crewai-tools" -version = "0.73.0" +version = "0.75.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, - { name = "chromadb" }, - { name = "click" }, { name = "crewai" }, { name = "docker" }, { name = "lancedb" }, - { name = "openai" }, - { name = "portalocker" }, - { name = "pydantic" }, { name = "pypdf" }, - { name = "pyright" }, { name = "python-docx" }, { name = "pytube" }, { name = "requests" }, @@ -576,9 +571,9 @@ dependencies = [ { name = "tiktoken" }, { name = "youtube-transcript-api" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5f/79/34cb1ad8dd658e514f4ced34b31b73024b2c25b6b6a28d91ee929b82e997/crewai_tools-0.73.0.tar.gz", hash = "sha256:6d0c1b065306f7a1dbfeff9e967800100cfdb4c883593f2a92aab95248951aa0", size = 2199671, upload-time = "2025-09-19T02:43:14.075Z" } +sdist = { url = "https://files.pythonhosted.org/packages/98/16/c897ed771235c6da4a6bc0c6f0baa12edc2b42d2a300447cd84588363f71/crewai_tools-0.75.0.tar.gz", hash = "sha256:9fffc498c93d35b7d19064caa74c6c8293001018f5aa144fb95f8168f2e75f49", size = 1134823, upload-time = "2025-09-26T18:19:39.252Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e6/1d/866a8ac760246dfe5242be4a60151101f3ec9a9c2c9e37ec26f28a475c6f/crewai_tools-0.73.0-py3-none-any.whl", hash = "sha256:f7be708997ee8ca1a0635dd4a6c769a67d052419159a763c38777a6fc2a9e0c2", size = 739629, upload-time = "2025-09-19T02:43:12.568Z" }, + { url = "https://files.pythonhosted.org/packages/90/39/a5b64857e7f862ff7736d4662f9ec46ae87be024f9d0460a328f5be3fd6f/crewai_tools-0.75.0-py3-none-any.whl", hash = "sha256:92abf7c0ca2650ff10318b553b513e97029e199a10f11ac575a5f9836d495408", size = 739639, upload-time = "2025-09-26T18:19:36.271Z" }, ] [[package]] @@ -1795,11 +1790,13 @@ name = "kagent-crewai" version = "0.1.0" source = { editable = "packages/kagent-crewai" } dependencies = [ - { name = "a2a-sdk" }, + { name = "a2a-sdk", extra = ["http-server"] }, { name = "crewai", extra = ["tools"] }, { name = "fastapi" }, + { name = "google-genai" }, { name = "httpx" }, { name = "kagent-core" }, + { name = "opentelemetry-instrumentation-crewai" }, { name = "pydantic" }, { name = "typing-extensions" }, { name = "uvicorn" }, @@ -1815,12 +1812,14 @@ dev = [ [package.metadata] requires-dist = [ - { name = "a2a-sdk", specifier = ">=0.3.1" }, + { name = "a2a-sdk", extras = ["http-server"], specifier = ">=0.3.1" }, { name = "black", marker = "extra == 'dev'", specifier = ">=23.0.0" }, { name = "crewai", extras = ["tools"], specifier = ">=0.193.2,<1.0.0" }, { name = "fastapi", specifier = ">=0.100.0" }, + { name = "google-genai", specifier = ">=1.21.1" }, { name = "httpx", specifier = ">=0.25.0" }, { name = "kagent-core", editable = "packages/kagent-core" }, + { name = "opentelemetry-instrumentation-crewai", specifier = ">=0.47.3" }, { name = "pydantic", specifier = ">=2.0.0" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" }, { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" }, @@ -2372,15 +2371,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, ] -[[package]] -name = "nodeenv" -version = "1.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, -] - [[package]] name = "numpy" version = "2.3.2" @@ -2616,6 +2606,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9e/07/ab97dd7e8bc680b479203f7d3b2771b7a097468135a669a38da3208f96cb/opentelemetry_instrumentation_asgi-0.57b0-py3-none-any.whl", hash = "sha256:47debbde6af066a7e8e911f7193730d5e40d62effc1ac2e1119908347790a3ea", size = 16599, upload-time = "2025-07-29T15:41:48.332Z" }, ] +[[package]] +name = "opentelemetry-instrumentation-crewai" +version = "0.47.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "opentelemetry-api" }, + { name = "opentelemetry-instrumentation" }, + { name = "opentelemetry-semantic-conventions" }, + { name = "opentelemetry-semantic-conventions-ai" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/c7/64647122476b18cc17c460d137ddd89feca03df852ea8ba4924cbb6eee97/opentelemetry_instrumentation_crewai-0.47.3.tar.gz", hash = "sha256:80af9b6b92e95c729d3fcd5a9778f847538f8b7ce43c71497c20722ddaad3a5f", size = 4620, upload-time = "2025-09-21T12:12:45.993Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/bd/1d8b74abbf1bd3a124e47b4a2ec64e8bb1edf458e5db5cffd222ce413a21/opentelemetry_instrumentation_crewai-0.47.3-py3-none-any.whl", hash = "sha256:56b5283a8b4d6f7332b4aaca7165d14e64c175fccb0af0e671916da4f93a81cc", size = 6193, upload-time = "2025-09-21T12:12:06.595Z" }, +] + [[package]] name = "opentelemetry-instrumentation-fastapi" version = "0.57b0" @@ -2996,7 +3001,7 @@ wheels = [ [[package]] name = "posthog" -version = "6.7.5" +version = "5.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "backoff" }, @@ -3004,11 +3009,10 @@ dependencies = [ { name = "python-dateutil" }, { name = "requests" }, { name = "six" }, - { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/f3/d9055bcd190980730bdc600318b34290e85fcb0afb1e31f83cdc33f92615/posthog-6.7.5.tar.gz", hash = "sha256:f4f32b4a4b0df531ae8f80f255a33a49e8880c8c1b62712e6b640535e33a905f", size = 118558, upload-time = "2025-09-16T12:40:34.431Z" } +sdist = { url = "https://files.pythonhosted.org/packages/48/20/60ae67bb9d82f00427946218d49e2e7e80fb41c15dc5019482289ec9ce8d/posthog-5.4.0.tar.gz", hash = "sha256:701669261b8d07cdde0276e5bc096b87f9e200e3b9589c5ebff14df658c5893c", size = 88076, upload-time = "2025-06-20T23:19:23.485Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/42/b4/b40f8467252b4ff481e54a9767b211b4ff83114e6d0b6f481852d0ef3e46/posthog-6.7.5-py3-none-any.whl", hash = "sha256:95b00f915365939e63fa183635bad1caaf89cf4a24b63c8bb6983f2a22a56cb3", size = 136766, upload-time = "2025-09-16T12:40:32.741Z" }, + { url = "https://files.pythonhosted.org/packages/4f/98/e480cab9a08d1c09b1c59a93dade92c1bb7544826684ff2acbfd10fcfbd4/posthog-5.4.0-py3-none-any.whl", hash = "sha256:284dfa302f64353484420b52d4ad81ff5c2c2d1d607c4e2db602ac72761831bd", size = 105364, upload-time = "2025-06-20T23:19:22.001Z" }, ] [[package]] @@ -3166,6 +3170,98 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] +[[package]] +name = "pybase64" +version = "1.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/04/14/43297a7b7f0c1bf0c00b596f754ee3ac946128c64d21047ccf9c9bbc5165/pybase64-1.4.2.tar.gz", hash = "sha256:46cdefd283ed9643315d952fe44de80dc9b9a811ce6e3ec97fd1827af97692d0", size = 137246, upload-time = "2025-07-27T13:08:57.808Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/56/5337f27a8b8d2d6693f46f7b36bae47895e5820bfa259b0072574a4e1057/pybase64-1.4.2-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:0f331aa59549de21f690b6ccc79360ffed1155c3cfbc852eb5c097c0b8565a2b", size = 33888, upload-time = "2025-07-27T13:03:35.698Z" }, + { url = "https://files.pythonhosted.org/packages/4c/09/f3f4b11fc9beda7e8625e29fb0f549958fcbb34fea3914e1c1d95116e344/pybase64-1.4.2-cp313-cp313-android_21_x86_64.whl", hash = "sha256:9dad20bf1f3ed9e6fe566c4c9d07d9a6c04f5a280daebd2082ffb8620b0a880d", size = 40796, upload-time = "2025-07-27T13:03:36.927Z" }, + { url = "https://files.pythonhosted.org/packages/e3/ff/470768f0fe6de0aa302a8cb1bdf2f9f5cffc3f69e60466153be68bc953aa/pybase64-1.4.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:69d3f0445b0faeef7bb7f93bf8c18d850785e2a77f12835f49e524cc54af04e7", size = 30914, upload-time = "2025-07-27T13:03:38.475Z" }, + { url = "https://files.pythonhosted.org/packages/75/6b/d328736662665e0892409dc410353ebef175b1be5eb6bab1dad579efa6df/pybase64-1.4.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:2372b257b1f4dd512f317fb27e77d313afd137334de64c87de8374027aacd88a", size = 31380, upload-time = "2025-07-27T13:03:39.7Z" }, + { url = "https://files.pythonhosted.org/packages/ca/96/7ff718f87c67f4147c181b73d0928897cefa17dc75d7abc6e37730d5908f/pybase64-1.4.2-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fb794502b4b1ec91c4ca5d283ae71aef65e3de7721057bd9e2b3ec79f7a62d7d", size = 38230, upload-time = "2025-07-27T13:03:41.637Z" }, + { url = "https://files.pythonhosted.org/packages/4d/58/a3307b048d799ff596a3c7c574fcba66f9b6b8c899a3c00a698124ca7ad5/pybase64-1.4.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d5c532b03fd14a5040d6cf6571299a05616f925369c72ddf6fe2fb643eb36fed", size = 38319, upload-time = "2025-07-27T13:03:42.847Z" }, + { url = "https://files.pythonhosted.org/packages/08/a7/0bda06341b0a2c830d348c6e1c4d348caaae86c53dc9a046e943467a05e9/pybase64-1.4.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0f699514dc1d5689ca9cf378139e0214051922732f9adec9404bc680a8bef7c0", size = 31655, upload-time = "2025-07-27T13:03:44.426Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/e1d6e8479e0c5113c2c63c7b44886935ce839c2d99884c7304ca9e86547c/pybase64-1.4.2-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:cd3e8713cbd32c8c6aa935feaf15c7670e2b7e8bfe51c24dc556811ebd293a29", size = 68232, upload-time = "2025-07-27T13:03:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/71/ab/db4dbdfccb9ca874d6ce34a0784761471885d96730de85cee3d300381529/pybase64-1.4.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d377d48acf53abf4b926c2a7a24a19deb092f366a04ffd856bf4b3aa330b025d", size = 71608, upload-time = "2025-07-27T13:03:47.01Z" }, + { url = "https://files.pythonhosted.org/packages/11/e9/508df958563951045d728bbfbd3be77465f9231cf805cb7ccaf6951fc9f1/pybase64-1.4.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d83c076e78d619b9e1dd674e2bf5fb9001aeb3e0b494b80a6c8f6d4120e38cd9", size = 59912, upload-time = "2025-07-27T13:03:48.277Z" }, + { url = "https://files.pythonhosted.org/packages/f2/58/7f2cef1ceccc682088958448d56727369de83fa6b29148478f4d2acd107a/pybase64-1.4.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:ab9cdb6a8176a5cb967f53e6ad60e40c83caaa1ae31c5e1b29e5c8f507f17538", size = 56413, upload-time = "2025-07-27T13:03:49.908Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/7e0af5c5728fa7e2eb082d88eca7c6bd17429be819d58518e74919d42e66/pybase64-1.4.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:adf0c103ad559dbfb9fe69edfd26a15c65d9c991a5ab0a25b04770f9eb0b9484", size = 59311, upload-time = "2025-07-27T13:03:51.238Z" }, + { url = "https://files.pythonhosted.org/packages/03/8b/09825d0f37e45b9a3f546e5f990b6cf2dd838e54ea74122c2464646e0c77/pybase64-1.4.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:0d03ef2f253d97ce0685d3624bf5e552d716b86cacb8a6c971333ba4b827e1fc", size = 60282, upload-time = "2025-07-27T13:03:52.56Z" }, + { url = "https://files.pythonhosted.org/packages/9c/3f/3711d2413f969bfd5b9cc19bc6b24abae361b7673ff37bcb90c43e199316/pybase64-1.4.2-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:e565abf906efee76ae4be1aef5df4aed0fda1639bc0d7732a3dafef76cb6fc35", size = 54845, upload-time = "2025-07-27T13:03:54.167Z" }, + { url = "https://files.pythonhosted.org/packages/c6/3c/4c7ce1ae4d828c2bb56d144322f81bffbaaac8597d35407c3d7cbb0ff98f/pybase64-1.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3c6a5f15fd03f232fc6f295cce3684f7bb08da6c6d5b12cc771f81c9f125cc6", size = 58615, upload-time = "2025-07-27T13:03:55.494Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8f/c2fc03bf4ed038358620065c75968a30184d5d3512d09d3ef9cc3bd48592/pybase64-1.4.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:bad9e3db16f448728138737bbd1af9dc2398efd593a8bdd73748cc02cd33f9c6", size = 52434, upload-time = "2025-07-27T13:03:56.808Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0a/757d6df0a60327c893cfae903e15419914dd792092dc8cc5c9523d40bc9b/pybase64-1.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2683ef271328365c31afee0ed8fa29356fb8fb7c10606794656aa9ffb95e92be", size = 68824, upload-time = "2025-07-27T13:03:58.735Z" }, + { url = "https://files.pythonhosted.org/packages/a0/14/84abe2ed8c29014239be1cfab45dfebe5a5ca779b177b8b6f779bd8b69da/pybase64-1.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:265b20089cd470079114c09bb74b101b3bfc3c94ad6b4231706cf9eff877d570", size = 57898, upload-time = "2025-07-27T13:04:00.379Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c6/d193031f90c864f7b59fa6d1d1b5af41f0f5db35439988a8b9f2d1b32a13/pybase64-1.4.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e53173badead10ef8b839aa5506eecf0067c7b75ad16d9bf39bc7144631f8e67", size = 54319, upload-time = "2025-07-27T13:04:01.742Z" }, + { url = "https://files.pythonhosted.org/packages/cb/37/ec0c7a610ff8f994ee6e0c5d5d66b6b6310388b96ebb347b03ae39870fdf/pybase64-1.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:5823b8dcf74da7da0f761ed60c961e8928a6524e520411ad05fe7f9f47d55b40", size = 56472, upload-time = "2025-07-27T13:04:03.089Z" }, + { url = "https://files.pythonhosted.org/packages/c4/5a/e585b74f85cedd261d271e4c2ef333c5cfce7e80750771808f56fee66b98/pybase64-1.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1237f66c54357d325390da60aa5e21c6918fbcd1bf527acb9c1f4188c62cb7d5", size = 70966, upload-time = "2025-07-27T13:04:04.361Z" }, + { url = "https://files.pythonhosted.org/packages/ad/20/1b2fdd98b4ba36008419668c813025758214c543e362c66c49214ecd1127/pybase64-1.4.2-cp313-cp313-win32.whl", hash = "sha256:b0b851eb4f801d16040047f6889cca5e9dfa102b3e33f68934d12511245cef86", size = 33681, upload-time = "2025-07-27T13:04:06.126Z" }, + { url = "https://files.pythonhosted.org/packages/ff/64/3df4067d169c047054889f34b5a946cbe3785bca43404b93c962a5461a41/pybase64-1.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:19541c6e26d17d9522c02680fe242206ae05df659c82a657aabadf209cd4c6c7", size = 35822, upload-time = "2025-07-27T13:04:07.752Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fd/db505188adf812e60ee923f196f9deddd8a1895b2b29b37f5db94afc3b1c/pybase64-1.4.2-cp313-cp313-win_arm64.whl", hash = "sha256:77a191863d576c0a5dd81f8a568a5ca15597cc980ae809dce62c717c8d42d8aa", size = 30899, upload-time = "2025-07-27T13:04:09.062Z" }, + { url = "https://files.pythonhosted.org/packages/d9/27/5f5fecd206ec1e06e1608a380af18dcb76a6ab08ade6597a3251502dcdb2/pybase64-1.4.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2e194bbabe3fdf9e47ba9f3e157394efe0849eb226df76432126239b3f44992c", size = 38677, upload-time = "2025-07-27T13:04:10.334Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0f/abe4b5a28529ef5f74e8348fa6a9ef27d7d75fbd98103d7664cf485b7d8f/pybase64-1.4.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:39aef1dadf4a004f11dd09e703abaf6528a87c8dbd39c448bb8aebdc0a08c1be", size = 32066, upload-time = "2025-07-27T13:04:11.641Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7e/ea0ce6a7155cada5526017ec588b6d6185adea4bf9331565272f4ef583c2/pybase64-1.4.2-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:91cb920c7143e36ec8217031282c8651da3b2206d70343f068fac0e7f073b7f9", size = 72300, upload-time = "2025-07-27T13:04:12.969Z" }, + { url = "https://files.pythonhosted.org/packages/45/2d/e64c7a056c9ec48dfe130d1295e47a8c2b19c3984488fc08e5eaa1e86c88/pybase64-1.4.2-cp313-cp313t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6958631143fb9e71f9842000da042ec2f6686506b6706e2dfda29e97925f6aa0", size = 75520, upload-time = "2025-07-27T13:04:14.374Z" }, + { url = "https://files.pythonhosted.org/packages/43/e0/e5f93b2e1cb0751a22713c4baa6c6eaf5f307385e369180486c8316ed21e/pybase64-1.4.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:dc35f14141ef3f1ac70d963950a278a2593af66fe5a1c7a208e185ca6278fa25", size = 65384, upload-time = "2025-07-27T13:04:16.204Z" }, + { url = "https://files.pythonhosted.org/packages/ff/23/8c645a1113ad88a1c6a3d0e825e93ef8b74ad3175148767853a0a4d7626e/pybase64-1.4.2-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5d949d2d677859c3a8507e1b21432a039d2b995e0bd3fe307052b6ded80f207a", size = 60471, upload-time = "2025-07-27T13:04:17.947Z" }, + { url = "https://files.pythonhosted.org/packages/8b/81/edd0f7d8b0526b91730a0dd4ce6b4c8be2136cd69d424afe36235d2d2a06/pybase64-1.4.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:09caacdd3e15fe7253a67781edd10a6a918befab0052a2a3c215fe5d1f150269", size = 63945, upload-time = "2025-07-27T13:04:19.383Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a5/edc224cd821fd65100b7af7c7e16b8f699916f8c0226c9c97bbae5a75e71/pybase64-1.4.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:e44b0e793b23f28ea0f15a9754bd0c960102a2ac4bccb8fafdedbd4cc4d235c0", size = 64858, upload-time = "2025-07-27T13:04:20.807Z" }, + { url = "https://files.pythonhosted.org/packages/11/3b/92853f968f1af7e42b7e54d21bdd319097b367e7dffa2ca20787361df74c/pybase64-1.4.2-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:849f274d0bcb90fc6f642c39274082724d108e41b15f3a17864282bd41fc71d5", size = 58557, upload-time = "2025-07-27T13:04:22.229Z" }, + { url = "https://files.pythonhosted.org/packages/76/09/0ec6bd2b2303b0ea5c6da7535edc9a608092075ef8c0cdd96e3e726cd687/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:528dba7ef1357bd7ce1aea143084501f47f5dd0fff7937d3906a68565aa59cfe", size = 63624, upload-time = "2025-07-27T13:04:23.952Z" }, + { url = "https://files.pythonhosted.org/packages/73/6e/52cb1ced2a517a3118b2e739e9417432049013ac7afa15d790103059e8e4/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:1da54be743d9a68671700cfe56c3ab8c26e8f2f5cc34eface905c55bc3a9af94", size = 56174, upload-time = "2025-07-27T13:04:25.419Z" }, + { url = "https://files.pythonhosted.org/packages/5b/9d/820fe79347467e48af985fe46180e1dd28e698ade7317bebd66de8a143f5/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:9b07c0406c3eaa7014499b0aacafb21a6d1146cfaa85d56f0aa02e6d542ee8f3", size = 72640, upload-time = "2025-07-27T13:04:26.824Z" }, + { url = "https://files.pythonhosted.org/packages/53/58/e863e10d08361e694935c815b73faad7e1ab03f99ae154d86c4e2f331896/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:312f2aa4cf5d199a97fbcaee75d2e59ebbaafcd091993eb373b43683498cdacb", size = 62453, upload-time = "2025-07-27T13:04:28.562Z" }, + { url = "https://files.pythonhosted.org/packages/95/f0/c392c4ac8ccb7a34b28377c21faa2395313e3c676d76c382642e19a20703/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ad59362fc267bf15498a318c9e076686e4beeb0dfe09b457fabbc2b32468b97a", size = 58103, upload-time = "2025-07-27T13:04:29.996Z" }, + { url = "https://files.pythonhosted.org/packages/32/30/00ab21316e7df8f526aa3e3dc06f74de6711d51c65b020575d0105a025b2/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:01593bd064e7dcd6c86d04e94e44acfe364049500c20ac68ca1e708fbb2ca970", size = 60779, upload-time = "2025-07-27T13:04:31.549Z" }, + { url = "https://files.pythonhosted.org/packages/a6/65/114ca81839b1805ce4a2b7d58bc16e95634734a2059991f6382fc71caf3e/pybase64-1.4.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5b81547ad8ea271c79fdf10da89a1e9313cb15edcba2a17adf8871735e9c02a0", size = 74684, upload-time = "2025-07-27T13:04:32.976Z" }, + { url = "https://files.pythonhosted.org/packages/54/8f/aa9d445b9bb693b8f6bb1456bd6d8576d79b7a63bf6c69af3a539235b15f/pybase64-1.4.2-cp313-cp313t-win32.whl", hash = "sha256:7edbe70b5654545a37e6e6b02de738303b1bbdfcde67f6cfec374cfb5cc4099e", size = 33961, upload-time = "2025-07-27T13:04:34.806Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e5/da37cfb173c646fd4fc7c6aae2bc41d40de2ee49529854af8f4e6f498b45/pybase64-1.4.2-cp313-cp313t-win_amd64.whl", hash = "sha256:385690addf87c25d6366fab5d8ff512eed8a7ecb18da9e8152af1c789162f208", size = 36199, upload-time = "2025-07-27T13:04:36.223Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/1eb68fb7d00f2cec8bd9838e2a30d183d6724ae06e745fd6e65216f170ff/pybase64-1.4.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c2070d0aa88580f57fe15ca88b09f162e604d19282915a95a3795b5d3c1c05b5", size = 31221, upload-time = "2025-07-27T13:04:37.704Z" }, + { url = "https://files.pythonhosted.org/packages/99/bf/00a87d951473ce96c8c08af22b6983e681bfabdb78dd2dcf7ee58eac0932/pybase64-1.4.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4157ad277a32cf4f02a975dffc62a3c67d73dfa4609b2c1978ef47e722b18b8e", size = 30924, upload-time = "2025-07-27T13:04:39.189Z" }, + { url = "https://files.pythonhosted.org/packages/ae/43/dee58c9d60e60e6fb32dc6da722d84592e22f13c277297eb4ce6baf99a99/pybase64-1.4.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:e113267dc349cf624eb4f4fbf53fd77835e1aa048ac6877399af426aab435757", size = 31390, upload-time = "2025-07-27T13:04:40.995Z" }, + { url = "https://files.pythonhosted.org/packages/e1/11/b28906fc2e330b8b1ab4bc845a7bef808b8506734e90ed79c6062b095112/pybase64-1.4.2-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:cea5aaf218fd9c5c23afacfe86fd4464dfedc1a0316dd3b5b4075b068cc67df0", size = 38212, upload-time = "2025-07-27T13:04:42.729Z" }, + { url = "https://files.pythonhosted.org/packages/24/9e/868d1e104413d14b19feaf934fc7fad4ef5b18946385f8bb79684af40f24/pybase64-1.4.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:41213497abbd770435c7a9c8123fb02b93709ac4cf60155cd5aefc5f3042b600", size = 38303, upload-time = "2025-07-27T13:04:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/a3/73/f7eac96ca505df0600280d6bfc671a9e2e2f947c2b04b12a70e36412f7eb/pybase64-1.4.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c8b522df7ee00f2ac1993ccd5e1f6608ae7482de3907668c2ff96a83ef213925", size = 31669, upload-time = "2025-07-27T13:04:45.845Z" }, + { url = "https://files.pythonhosted.org/packages/c6/43/8e18bea4fd455100112d6a73a83702843f067ef9b9272485b6bdfd9ed2f0/pybase64-1.4.2-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:06725022e540c5b098b978a0418ca979773e2cbdbb76f10bd97536f2ad1c5b49", size = 68452, upload-time = "2025-07-27T13:04:47.788Z" }, + { url = "https://files.pythonhosted.org/packages/e4/2e/851eb51284b97354ee5dfa1309624ab90920696e91a33cd85b13d20cc5c1/pybase64-1.4.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a3e54dcf0d0305ec88473c9d0009f698cabf86f88a8a10090efeff2879c421bb", size = 71674, upload-time = "2025-07-27T13:04:49.294Z" }, + { url = "https://files.pythonhosted.org/packages/57/0d/5cf1e5dc64aec8db43e8dee4e4046856d639a72bcb0fb3e716be42ced5f1/pybase64-1.4.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67675cee727a60dc91173d2790206f01aa3c7b3fbccfa84fd5c1e3d883fe6caa", size = 60027, upload-time = "2025-07-27T13:04:50.769Z" }, + { url = "https://files.pythonhosted.org/packages/a4/8e/3479266bc0e65f6cc48b3938d4a83bff045330649869d950a378f2ddece0/pybase64-1.4.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:753da25d4fd20be7bda2746f545935773beea12d5cb5ec56ec2d2960796477b1", size = 56461, upload-time = "2025-07-27T13:04:52.37Z" }, + { url = "https://files.pythonhosted.org/packages/20/b6/f2b6cf59106dd78bae8717302be5b814cec33293504ad409a2eb752ad60c/pybase64-1.4.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a78c768ce4ca550885246d14babdb8923e0f4a848dfaaeb63c38fc99e7ea4052", size = 59446, upload-time = "2025-07-27T13:04:53.967Z" }, + { url = "https://files.pythonhosted.org/packages/16/70/3417797dfccdfdd0a54e4ad17c15b0624f0fc2d6a362210f229f5c4e8fd0/pybase64-1.4.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:51b17f36d890c92f0618fb1c8db2ccc25e6ed07afa505bab616396fc9b0b0492", size = 60350, upload-time = "2025-07-27T13:04:55.881Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/6e4269dd98d150ae95d321b311a345eae0f7fd459d97901b4a586d7513bb/pybase64-1.4.2-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:f92218d667049ab4f65d54fa043a88ffdb2f07fff1f868789ef705a5221de7ec", size = 54989, upload-time = "2025-07-27T13:04:57.436Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e8/18c1b0c255f964fafd0412b0d5a163aad588aeccb8f84b9bf9c8611d80f6/pybase64-1.4.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3547b3d1499919a06491b3f879a19fbe206af2bd1a424ecbb4e601eb2bd11fea", size = 58724, upload-time = "2025-07-27T13:04:59.406Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/ddfbd2125fc20b94865fb232b2e9105376fa16eee492e4b7786d42a86cbf/pybase64-1.4.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:958af7b0e09ddeb13e8c2330767c47b556b1ade19c35370f6451d139cde9f2a9", size = 52285, upload-time = "2025-07-27T13:05:01.198Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4c/b9d4ec9224add33c84b925a03d1a53cd4106efb449ea8e0ae7795fed7bf7/pybase64-1.4.2-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:4facc57f6671e2229a385a97a618273e7be36a9ea0a9d1c1b9347f14d19ceba8", size = 69036, upload-time = "2025-07-27T13:05:03.109Z" }, + { url = "https://files.pythonhosted.org/packages/92/38/7b96794da77bed3d9b4fea40f14ae563648fba83a696e7602fabe60c0eb7/pybase64-1.4.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a32fc57d05d73a7c9b0ca95e9e265e21cf734195dc6873829a890058c35f5cfd", size = 57938, upload-time = "2025-07-27T13:05:04.744Z" }, + { url = "https://files.pythonhosted.org/packages/eb/c5/ae8bbce3c322d1b074e79f51f5df95961fe90cb8748df66c6bc97616e974/pybase64-1.4.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3dc853243c81ce89cc7318e6946f860df28ddb7cd2a0648b981652d9ad09ee5a", size = 54474, upload-time = "2025-07-27T13:05:06.662Z" }, + { url = "https://files.pythonhosted.org/packages/15/9a/c09887c4bb1b43c03fc352e2671ef20c6686c6942a99106a45270ee5b840/pybase64-1.4.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:0e6d863a86b3e7bc6ac9bd659bebda4501b9da842521111b0b0e54eb51295df5", size = 56533, upload-time = "2025-07-27T13:05:08.368Z" }, + { url = "https://files.pythonhosted.org/packages/4f/0f/d5114d63d35d085639606a880cb06e2322841cd4b213adfc14d545c1186f/pybase64-1.4.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6579475140ff2067903725d8aca47f5747bcb211597a1edd60b58f6d90ada2bd", size = 71030, upload-time = "2025-07-27T13:05:10.3Z" }, + { url = "https://files.pythonhosted.org/packages/40/0e/fe6f1ed22ea52eb99f490a8441815ba21de288f4351aeef4968d71d20d2d/pybase64-1.4.2-cp314-cp314-win32.whl", hash = "sha256:373897f728d7b4f241a1f803ac732c27b6945d26d86b2741ad9b75c802e4e378", size = 34174, upload-time = "2025-07-27T13:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/0e15bea52ffc63e8ae7935e945accbaf635e0aefa26d3e31fdf9bc9dcd01/pybase64-1.4.2-cp314-cp314-win_amd64.whl", hash = "sha256:1afe3361344617d298c1d08bc657ef56d0f702d6b72cb65d968b2771017935aa", size = 36308, upload-time = "2025-07-27T13:05:13.898Z" }, + { url = "https://files.pythonhosted.org/packages/4f/dc/55849fee2577bda77c1e078da04cc9237e8e474a8c8308deb702a26f2511/pybase64-1.4.2-cp314-cp314-win_arm64.whl", hash = "sha256:f131c9360babe522f3d90f34da3f827cba80318125cf18d66f2ee27e3730e8c4", size = 31341, upload-time = "2025-07-27T13:05:15.553Z" }, + { url = "https://files.pythonhosted.org/packages/39/44/c69d088e28b25e70ac742b6789cde038473815b2a69345c4bae82d5e244d/pybase64-1.4.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2583ac304131c1bd6e3120b0179333610f18816000db77c0a2dd6da1364722a8", size = 38678, upload-time = "2025-07-27T13:05:17.544Z" }, + { url = "https://files.pythonhosted.org/packages/00/93/2860ec067497b9cbb06242f96d44caebbd9eed32174e4eb8c1ffef760f94/pybase64-1.4.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:75a8116be4ea4cdd30a5c4f1a6f3b038e0d457eb03c8a2685d8ce2aa00ef8f92", size = 32066, upload-time = "2025-07-27T13:05:19.18Z" }, + { url = "https://files.pythonhosted.org/packages/d3/55/1e96249a38759332e8a01b31c370d88c60ceaf44692eb6ba4f0f451ee496/pybase64-1.4.2-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:217ea776a098d7c08668e5526b9764f5048bbfd28cac86834217ddfe76a4e3c4", size = 72465, upload-time = "2025-07-27T13:05:20.866Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0f468605b899f3e35dbb7423fba3ff98aeed1ec16abb02428468494a58f4/pybase64-1.4.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4ec14683e343c95b14248cdfdfa78c052582be7a3865fd570aa7cffa5ab5cf37", size = 75693, upload-time = "2025-07-27T13:05:22.896Z" }, + { url = "https://files.pythonhosted.org/packages/91/d1/9980a0159b699e2489baba05b71b7c953b29249118ba06fdbb3e9ea1b9b5/pybase64-1.4.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:480ecf21e1e956c5a10d3cf7b3b7e75bce3f9328cf08c101e4aab1925d879f34", size = 65577, upload-time = "2025-07-27T13:05:25Z" }, + { url = "https://files.pythonhosted.org/packages/16/86/b27e7b95f9863d245c0179a7245582eda3d262669d8f822777364d8fd7d5/pybase64-1.4.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:1fe1ebdc55e9447142e2f6658944aadfb5a4fbf03dbd509be34182585515ecc1", size = 60662, upload-time = "2025-07-27T13:05:27.138Z" }, + { url = "https://files.pythonhosted.org/packages/28/87/a7f0dde0abc26bfbee761f1d3558eb4b139f33ddd9fe1f6825ffa7daa22d/pybase64-1.4.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c793a2b06753accdaf5e1a8bbe5d800aab2406919e5008174f989a1ca0081411", size = 64179, upload-time = "2025-07-27T13:05:28.996Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/5d6fa1c60e1363b4cac4c396978f39e9df4689e75225d7d9c0a5998e3a14/pybase64-1.4.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6acae6e1d1f7ebe40165f08076c7a73692b2bf9046fefe673f350536e007f556", size = 64968, upload-time = "2025-07-27T13:05:30.818Z" }, + { url = "https://files.pythonhosted.org/packages/20/6e/2ed585af5b2211040445d9849326dd2445320c9316268794f5453cfbaf30/pybase64-1.4.2-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:88b91cd0949358aadcea75f8de5afbcf3c8c5fb9ec82325bd24285b7119cf56e", size = 58738, upload-time = "2025-07-27T13:05:32.629Z" }, + { url = "https://files.pythonhosted.org/packages/ce/94/e2960b56322eabb3fbf303fc5a72e6444594c1b90035f3975c6fe666db5c/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:53316587e1b1f47a11a5ff068d3cbd4a3911c291f2aec14882734973684871b2", size = 63802, upload-time = "2025-07-27T13:05:34.687Z" }, + { url = "https://files.pythonhosted.org/packages/95/47/312139d764c223f534f751528ce3802887c279125eac64f71cd3b4e05abc/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:caa7f20f43d00602cf9043b5ba758d54f5c41707d3709b2a5fac17361579c53c", size = 56341, upload-time = "2025-07-27T13:05:36.554Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d7/aec9a6ed53b128dac32f8768b646ca5730c88eef80934054d7fa7d02f3ef/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:2d93817e24fdd79c534ed97705df855af6f1d2535ceb8dfa80da9de75482a8d7", size = 72838, upload-time = "2025-07-27T13:05:38.459Z" }, + { url = "https://files.pythonhosted.org/packages/e3/a8/6ccc54c5f1f7c3450ad7c56da10c0f131d85ebe069ea6952b5b42f2e92d9/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:63cd769b51474d8d08f7f2ce73b30380d9b4078ec92ea6b348ea20ed1e1af88a", size = 62633, upload-time = "2025-07-27T13:05:40.624Z" }, + { url = "https://files.pythonhosted.org/packages/34/22/2b9d89f8ff6f2a01d6d6a88664b20a4817049cfc3f2c62caca040706660c/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:cd07e6a9993c392ec8eb03912a43c6a6b21b2deb79ee0d606700fe276e9a576f", size = 58282, upload-time = "2025-07-27T13:05:42.565Z" }, + { url = "https://files.pythonhosted.org/packages/b2/14/dbf6266177532a6a11804ac080ebffcee272f491b92820c39886ee20f201/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:6a8944e8194adff4668350504bc6b7dbde2dab9244c88d99c491657d145b5af5", size = 60948, upload-time = "2025-07-27T13:05:44.48Z" }, + { url = "https://files.pythonhosted.org/packages/fd/7a/b2ae9046a66dd5746cd72836a41386517b1680bea5ce02f2b4f1c9ebc688/pybase64-1.4.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:04ab398ec4b6a212af57f6a21a6336d5a1d754ff4ccb215951366ab9080481b2", size = 74854, upload-time = "2025-07-27T13:05:46.416Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7e/9856f6d6c38a7b730e001123d2d9fa816b8b1a45f0cdee1d509d5947b047/pybase64-1.4.2-cp314-cp314t-win32.whl", hash = "sha256:3b9201ecdcb1c3e23be4caebd6393a4e6615bd0722528f5413b58e22e3792dd3", size = 34490, upload-time = "2025-07-27T13:05:48.304Z" }, + { url = "https://files.pythonhosted.org/packages/c7/38/8523a9dc1ec8704dedbe5ccc95192ae9a7585f7eec85cc62946fe3cacd32/pybase64-1.4.2-cp314-cp314t-win_amd64.whl", hash = "sha256:36e9b0cad8197136d73904ef5a71d843381d063fd528c5ab203fc4990264f682", size = 36680, upload-time = "2025-07-27T13:05:50.264Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/5600104ef7b85f89fb8ec54f73504ead3f6f0294027e08d281f3cafb5c1a/pybase64-1.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:f25140496b02db0e7401567cd869fb13b4c8118bf5c2428592ec339987146d8b", size = 31600, upload-time = "2025-07-27T13:05:52.24Z" }, +] + [[package]] name = "pycparser" version = "2.22" @@ -3177,7 +3273,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.11.7" +version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -3185,37 +3281,54 @@ dependencies = [ { name = "typing-extensions" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" }, ] [[package]] name = "pydantic-core" -version = "2.33.2" +version = "2.41.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, - { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, - { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, - { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, - { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, - { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, - { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, - { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, - { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, - { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, - { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, - { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, - { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf", size = 2105825, upload-time = "2025-10-06T21:10:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb", size = 1910126, upload-time = "2025-10-06T21:10:53.145Z" }, + { url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669", size = 1961472, upload-time = "2025-10-06T21:10:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f", size = 2063230, upload-time = "2025-10-06T21:10:57.179Z" }, + { url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4", size = 2229469, upload-time = "2025-10-06T21:10:59.409Z" }, + { url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62", size = 2347986, upload-time = "2025-10-06T21:11:00.847Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014", size = 2072216, upload-time = "2025-10-06T21:11:02.43Z" }, + { url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d", size = 2193047, upload-time = "2025-10-06T21:11:03.787Z" }, + { url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f", size = 2140613, upload-time = "2025-10-06T21:11:05.607Z" }, + { url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257", size = 2327641, upload-time = "2025-10-06T21:11:07.143Z" }, + { url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32", size = 2318229, upload-time = "2025-10-06T21:11:08.73Z" }, + { url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d", size = 1997911, upload-time = "2025-10-06T21:11:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b", size = 2034301, upload-time = "2025-10-06T21:11:12.113Z" }, + { url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb", size = 1977238, upload-time = "2025-10-06T21:11:14.1Z" }, + { url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc", size = 1875626, upload-time = "2025-10-06T21:11:15.69Z" }, + { url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67", size = 2045708, upload-time = "2025-10-06T21:11:17.258Z" }, + { url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795", size = 1997171, upload-time = "2025-10-06T21:11:18.822Z" }, + { url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b", size = 2107836, upload-time = "2025-10-06T21:11:20.432Z" }, + { url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a", size = 1904449, upload-time = "2025-10-06T21:11:22.185Z" }, + { url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674", size = 1961750, upload-time = "2025-10-06T21:11:24.348Z" }, + { url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4", size = 2063305, upload-time = "2025-10-06T21:11:26.556Z" }, + { url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31", size = 2228959, upload-time = "2025-10-06T21:11:28.426Z" }, + { url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706", size = 2345421, upload-time = "2025-10-06T21:11:30.226Z" }, + { url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b", size = 2065288, upload-time = "2025-10-06T21:11:32.019Z" }, + { url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be", size = 2189759, upload-time = "2025-10-06T21:11:33.753Z" }, + { url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04", size = 2140747, upload-time = "2025-10-06T21:11:35.781Z" }, + { url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4", size = 2327416, upload-time = "2025-10-06T21:11:37.75Z" }, + { url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8", size = 2318138, upload-time = "2025-10-06T21:11:39.463Z" }, + { url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl", hash = "sha256:a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159", size = 1998429, upload-time = "2025-10-06T21:11:41.989Z" }, + { url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl", hash = "sha256:1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae", size = 2028870, upload-time = "2025-10-06T21:11:43.66Z" }, + { url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9", size = 1974275, upload-time = "2025-10-06T21:11:45.476Z" }, + { url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4", size = 1875124, upload-time = "2025-10-06T21:11:47.591Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e", size = 2043075, upload-time = "2025-10-06T21:11:49.542Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762", size = 1995341, upload-time = "2025-10-06T21:11:51.497Z" }, ] [[package]] @@ -3342,19 +3455,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178, upload-time = "2024-09-19T02:40:08.598Z" }, ] -[[package]] -name = "pyright" -version = "1.1.405" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "nodeenv" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fb/6c/ba4bbee22e76af700ea593a1d8701e3225080956753bee9750dcc25e2649/pyright-1.1.405.tar.gz", hash = "sha256:5c2a30e1037af27eb463a1cc0b9f6d65fec48478ccf092c1ac28385a15c55763", size = 4068319, upload-time = "2025-09-04T03:37:06.776Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/1a/524f832e1ff1962a22a1accc775ca7b143ba2e9f5924bb6749dce566784a/pyright-1.1.405-py3-none-any.whl", hash = "sha256:a2cb13700b5508ce8e5d4546034cb7ea4aedb60215c6c33f56cec7f53996035a", size = 5905038, upload-time = "2025-09-04T03:37:04.913Z" }, -] - [[package]] name = "pytest" version = "8.4.1" @@ -3997,14 +4097,14 @@ wheels = [ [[package]] name = "typing-inspection" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, ] [[package]]