Team Ai
Datasetpublic

codekingpro/portable-devtools

sourceHugging Faceupdated 5mo agoView on Hugging Face
1likes14kdownloads
base.py956 linesDownload Raw Back to tracers
1"""Base interfaces for tracing runs."""2 3from __future__ import annotations4 5import asyncio6import logging7from abc import ABC, abstractmethod8from typing import (9    TYPE_CHECKING,10    Any,11)12 13from typing_extensions import override14 15from langchain_core.callbacks.base import AsyncCallbackHandler, BaseCallbackHandler16from langchain_core.exceptions import TracerException  # noqa: F40117from langchain_core.tracers.core import _TracerCore18 19if TYPE_CHECKING:20    from collections.abc import Sequence21    from uuid import UUID22 23    from tenacity import RetryCallState24 25    from langchain_core.documents import Document26    from langchain_core.messages import BaseMessage27    from langchain_core.outputs import ChatGenerationChunk, GenerationChunk, LLMResult28    from langchain_core.tracers.schemas import Run29 30logger = logging.getLogger(__name__)31 32 33class BaseTracer(_TracerCore, BaseCallbackHandler, ABC):34    """Base interface for tracers."""35 36    @abstractmethod37    def _persist_run(self, run: Run) -> None:38        """Persist a run."""39 40    def _start_trace(self, run: Run) -> None:41        """Start a trace for a run."""42        super()._start_trace(run)43        self._on_run_create(run)44 45    def _end_trace(self, run: Run) -> None:46        """End a trace for a run."""47        if not run.parent_run_id:48            self._persist_run(run)49        self.run_map.pop(str(run.id))50        # If this run's parent was injected from an external tracing context51        # (e.g. a langsmith @traceable), decrement its child refcount and52        # remove it from run_map once the last child is done.53        parent_id = str(run.parent_run_id) if run.parent_run_id else None54        if parent_id and parent_id in self._external_run_ids:55            self._external_run_ids[parent_id] -= 156            if self._external_run_ids[parent_id] <= 0:57                self.run_map.pop(parent_id, None)58                del self._external_run_ids[parent_id]59        self._on_run_update(run)60 61    def on_chat_model_start(62        self,63        serialized: dict[str, Any],64        messages: list[list[BaseMessage]],65        *,66        run_id: UUID,67        tags: list[str] | None = None,68        parent_run_id: UUID | None = None,69        metadata: dict[str, Any] | None = None,70        name: str | None = None,71        **kwargs: Any,72    ) -> Run:73        """Start a trace for a chat model run.74 75        Note:76            Naming can be confusing here: there is `on_chat_model_start`, but no77            corresponding `on_chat_model_end` callback. Chat model completion is78            routed through `on_llm_end` / `_on_llm_end`, which are shared with79            text LLM runs.80 81        Args:82            serialized: The serialized model.83            messages: The messages to start the chat with.84            run_id: The run ID.85            tags: The tags for the run.86            parent_run_id: The parent run ID.87            metadata: The metadata for the run.88            name: The name of the run.89            **kwargs: Additional arguments.90 91        Returns:92            The run.93        """94        chat_model_run = self._create_chat_model_run(95            serialized=serialized,96            messages=messages,97            run_id=run_id,98            parent_run_id=parent_run_id,99            tags=tags,100            metadata=metadata,101            name=name,102            **kwargs,103        )104        self._start_trace(chat_model_run)105        self._on_chat_model_start(chat_model_run)106        return chat_model_run107 108    def on_llm_start(109        self,110        serialized: dict[str, Any],111        prompts: list[str],112        *,113        run_id: UUID,114        tags: list[str] | None = None,115        parent_run_id: UUID | None = None,116        metadata: dict[str, Any] | None = None,117        name: str | None = None,118        **kwargs: Any,119    ) -> Run:120        """Start a trace for an LLM run.121 122        Args:123            serialized: The serialized model.124            prompts: The prompts to start the LLM with.125            run_id: The run ID.126            tags: The tags for the run.127            parent_run_id: The parent run ID.128            metadata: The metadata for the run.129            name: The name of the run.130            **kwargs: Additional arguments.131 132        Returns:133            The run.134        """135        llm_run = self._create_llm_run(136            serialized=serialized,137            prompts=prompts,138            run_id=run_id,139            parent_run_id=parent_run_id,140            tags=tags,141            metadata=metadata,142            name=name,143            **kwargs,144        )145        self._start_trace(llm_run)146        self._on_llm_start(llm_run)147        return llm_run148 149    @override150    def on_llm_new_token(151        self,152        token: str,153        *,154        chunk: GenerationChunk | ChatGenerationChunk | None = None,155        run_id: UUID,156        parent_run_id: UUID | None = None,157        **kwargs: Any,158    ) -> Run:159        """Run on new LLM token.160 161        Only available when streaming is enabled.162 163        Args:164            token: The token.165            chunk: The chunk.166            run_id: The run ID.167            parent_run_id: The parent run ID.168            **kwargs: Additional arguments.169 170        Returns:171            The run.172        """173        # "chat_model" is only used for the experimental new streaming_events format.174        # This change should not affect any existing tracers.175        llm_run = self._llm_run_with_token_event(176            token=token,177            run_id=run_id,178            chunk=chunk,179            parent_run_id=parent_run_id,180        )181        self._on_llm_new_token(llm_run, token, chunk)182        return llm_run183 184    @override185    def on_retry(186        self,187        retry_state: RetryCallState,188        *,189        run_id: UUID,190        **kwargs: Any,191    ) -> Run:192        """Run on retry.193 194        Args:195            retry_state: The retry state.196            run_id: The run ID.197            **kwargs: Additional arguments.198 199        Returns:200            The run.201        """202        return self._llm_run_with_retry_event(203            retry_state=retry_state,204            run_id=run_id,205        )206 207    @override208    def on_llm_end(self, response: LLMResult, *, run_id: UUID, **kwargs: Any) -> Run:209        """End a trace for an LLM or chat model run.210 211        Note:212            This is the end callback for both run types. Chat models start with213            `on_chat_model_start`, but there is no `on_chat_model_end`;214            completion is routed here for callback API compatibility.215 216        Args:217            response: The response.218            run_id: The run ID.219            **kwargs: Additional arguments.220 221        Returns:222            The run.223        """224        # "chat_model" is only used for the experimental new streaming_events format.225        # This change should not affect any existing tracers.226        llm_run = self._complete_llm_run(227            response=response,228            run_id=run_id,229        )230        self._end_trace(llm_run)231        self._on_llm_end(llm_run)232        return llm_run233 234    def on_llm_error(235        self,236        error: BaseException,237        *,238        run_id: UUID,239        **kwargs: Any,240    ) -> Run:241        """Handle an error for an LLM run.242 243        Args:244            error: The error.245            run_id: The run ID.246            **kwargs: Additional arguments.247 248        Returns:249            The run.250        """251        # "chat_model" is only used for the experimental new streaming_events format.252        # This change should not affect any existing tracers.253        llm_run = self._errored_llm_run(254            error=error, run_id=run_id, response=kwargs.pop("response", None)255        )256        self._end_trace(llm_run)257        self._on_llm_error(llm_run)258        return llm_run259 260    @override261    def on_chain_start(262        self,263        serialized: dict[str, Any],264        inputs: dict[str, Any],265        *,266        run_id: UUID,267        tags: list[str] | None = None,268        parent_run_id: UUID | None = None,269        metadata: dict[str, Any] | None = None,270        run_type: str | None = None,271        name: str | None = None,272        **kwargs: Any,273    ) -> Run:274        """Start a trace for a chain run.275 276        Args:277            serialized: The serialized chain.278            inputs: The inputs for the chain.279            run_id: The run ID.280            tags: The tags for the run.281            parent_run_id: The parent run ID.282            metadata: The metadata for the run.283            run_type: The type of the run.284            name: The name of the run.285            **kwargs: Additional arguments.286 287        Returns:288            The run.289        """290        chain_run = self._create_chain_run(291            serialized=serialized,292            inputs=inputs,293            run_id=run_id,294            tags=tags,295            parent_run_id=parent_run_id,296            metadata=metadata,297            run_type=run_type,298            name=name,299            **kwargs,300        )301        self._start_trace(chain_run)302        self._on_chain_start(chain_run)303        return chain_run304 305    @override306    def on_chain_end(307        self,308        outputs: dict[str, Any],309        *,310        run_id: UUID,311        inputs: dict[str, Any] | None = None,312        **kwargs: Any,313    ) -> Run:314        """End a trace for a chain run.315 316        Args:317            outputs: The outputs for the chain.318            run_id: The run ID.319            inputs: The inputs for the chain.320            **kwargs: Additional arguments.321 322        Returns:323            The run.324        """325        chain_run = self._complete_chain_run(326            outputs=outputs,327            run_id=run_id,328            inputs=inputs,329        )330        self._end_trace(chain_run)331        self._on_chain_end(chain_run)332        return chain_run333 334    @override335    def on_chain_error(336        self,337        error: BaseException,338        *,339        inputs: dict[str, Any] | None = None,340        run_id: UUID,341        **kwargs: Any,342    ) -> Run:343        """Handle an error for a chain run.344 345        Args:346            error: The error.347            inputs: The inputs for the chain.348            run_id: The run ID.349            **kwargs: Additional arguments.350 351        Returns:352            The run.353        """354        chain_run = self._errored_chain_run(355            error=error,356            run_id=run_id,357            inputs=inputs,358        )359        self._end_trace(chain_run)360        self._on_chain_error(chain_run)361        return chain_run362 363    def on_tool_start(364        self,365        serialized: dict[str, Any],366        input_str: str,367        *,368        run_id: UUID,369        tags: list[str] | None = None,370        parent_run_id: UUID | None = None,371        metadata: dict[str, Any] | None = None,372        name: str | None = None,373        inputs: dict[str, Any] | None = None,374        **kwargs: Any,375    ) -> Run:376        """Start a trace for a tool run.377 378        Args:379            serialized: The serialized tool.380            input_str: The input string.381            run_id: The run ID.382            tags: The tags for the run.383            parent_run_id: The parent run ID.384            metadata: The metadata for the run.385            name: The name of the run.386            inputs: The inputs for the tool.387            **kwargs: Additional arguments.388 389        Returns:390            The run.391        """392        tool_run = self._create_tool_run(393            serialized=serialized,394            input_str=input_str,395            run_id=run_id,396            tags=tags,397            parent_run_id=parent_run_id,398            metadata=metadata,399            name=name,400            inputs=inputs,401            **kwargs,402        )403        self._start_trace(tool_run)404        self._on_tool_start(tool_run)405        return tool_run406 407    @override408    def on_tool_end(self, output: Any, *, run_id: UUID, **kwargs: Any) -> Run:409        """End a trace for a tool run.410 411        Args:412            output: The output for the tool.413            run_id: The run ID.414            **kwargs: Additional arguments.415 416        Returns:417            The run.418        """419        tool_run = self._complete_tool_run(420            output=output,421            run_id=run_id,422        )423        self._end_trace(tool_run)424        self._on_tool_end(tool_run)425        return tool_run426 427    @override428    def on_tool_error(429        self,430        error: BaseException,431        *,432        run_id: UUID,433        **kwargs: Any,434    ) -> Run:435        """Handle an error for a tool run.436 437        Args:438            error: The error.439            run_id: The run ID.440            **kwargs: Additional arguments.441 442        Returns:443            The run.444        """445        tool_run = self._errored_tool_run(446            error=error,447            run_id=run_id,448        )449        self._end_trace(tool_run)450        self._on_tool_error(tool_run)451        return tool_run452 453    def on_retriever_start(454        self,455        serialized: dict[str, Any],456        query: str,457        *,458        run_id: UUID,459        parent_run_id: UUID | None = None,460        tags: list[str] | None = None,461        metadata: dict[str, Any] | None = None,462        name: str | None = None,463        **kwargs: Any,464    ) -> Run:465        """Run when the `Retriever` starts running.466 467        Args:468            serialized: The serialized retriever.469            query: The query.470            run_id: The run ID.471            parent_run_id: The parent run ID.472            tags: The tags for the run.473            metadata: The metadata for the run.474            name: The name of the run.475            **kwargs: Additional arguments.476 477        Returns:478            The run.479        """480        retrieval_run = self._create_retrieval_run(481            serialized=serialized,482            query=query,483            run_id=run_id,484            parent_run_id=parent_run_id,485            tags=tags,486            metadata=metadata,487            name=name,488            **kwargs,489        )490        self._start_trace(retrieval_run)491        self._on_retriever_start(retrieval_run)492        return retrieval_run493 494    @override495    def on_retriever_error(496        self,497        error: BaseException,498        *,499        run_id: UUID,500        **kwargs: Any,501    ) -> Run:502        """Run when `Retriever` errors.503 504        Args:505            error: The error.506            run_id: The run ID.507            **kwargs: Additional arguments.508 509        Returns:510            The run.511        """512        retrieval_run = self._errored_retrieval_run(513            error=error,514            run_id=run_id,515        )516        self._end_trace(retrieval_run)517        self._on_retriever_error(retrieval_run)518        return retrieval_run519 520    @override521    def on_retriever_end(522        self, documents: Sequence[Document], *, run_id: UUID, **kwargs: Any523    ) -> Run:524        """Run when the `Retriever` ends running.525 526        Args:527            documents: The documents.528            run_id: The run ID.529            **kwargs: Additional arguments.530 531        Returns:532            The run.533        """534        retrieval_run = self._complete_retrieval_run(535            documents=documents,536            run_id=run_id,537        )538        self._end_trace(retrieval_run)539        self._on_retriever_end(retrieval_run)540        return retrieval_run541 542    def __deepcopy__(self, memo: dict) -> BaseTracer:543        """Return self."""544        return self545 546    def __copy__(self) -> BaseTracer:547        """Return self."""548        return self549 550 551class AsyncBaseTracer(_TracerCore, AsyncCallbackHandler, ABC):552    """Async base interface for tracers."""553 554    @abstractmethod555    @override556    async def _persist_run(self, run: Run) -> None:557        """Persist a run."""558 559    @override560    async def _start_trace(self, run: Run) -> None:561        """Start a trace for a run.562 563        Starting a trace will run concurrently with each `_on_[run_type]_start` method.564        No `_on_[run_type]_start` callback should depend on operations in565        `_start_trace`.566        """567        super()._start_trace(run)568        await self._on_run_create(run)569 570    @override571    async def _end_trace(self, run: Run) -> None:572        """End a trace for a run.573 574        Ending a trace will run concurrently with each `_on_[run_type]_end` method.575        No `_on_[run_type]_end` callback should depend on operations in `_end_trace`.576        """577        if not run.parent_run_id:578            await self._persist_run(run)579        self.run_map.pop(str(run.id))580        # If this run's parent was injected from an external tracing context581        # (e.g. a langsmith @traceable), decrement its child refcount and582        # remove it from run_map once the last child is done.583        parent_id = str(run.parent_run_id) if run.parent_run_id else None584        if parent_id and parent_id in self._external_run_ids:585            self._external_run_ids[parent_id] -= 1586            if self._external_run_ids[parent_id] <= 0:587                self.run_map.pop(parent_id, None)588                del self._external_run_ids[parent_id]589        await self._on_run_update(run)590 591    @override592    async def on_chat_model_start(593        self,594        serialized: dict[str, Any],595        messages: list[list[BaseMessage]],596        *,597        run_id: UUID,598        parent_run_id: UUID | None = None,599        tags: list[str] | None = None,600        metadata: dict[str, Any] | None = None,601        name: str | None = None,602        **kwargs: Any,603    ) -> Any:604        chat_model_run = self._create_chat_model_run(605            serialized=serialized,606            messages=messages,607            run_id=run_id,608            parent_run_id=parent_run_id,609            tags=tags,610            metadata=metadata,611            name=name,612            **kwargs,613        )614        tasks = [615            self._start_trace(chat_model_run),616            self._on_chat_model_start(chat_model_run),617        ]618        await asyncio.gather(*tasks)619        return chat_model_run620 621    @override622    async def on_llm_start(623        self,624        serialized: dict[str, Any],625        prompts: list[str],626        *,627        run_id: UUID,628        parent_run_id: UUID | None = None,629        tags: list[str] | None = None,630        metadata: dict[str, Any] | None = None,631        **kwargs: Any,632    ) -> None:633        llm_run = self._create_llm_run(634            serialized=serialized,635            prompts=prompts,636            run_id=run_id,637            parent_run_id=parent_run_id,638            tags=tags,639            metadata=metadata,640            **kwargs,641        )642        tasks = [self._start_trace(llm_run), self._on_llm_start(llm_run)]643        await asyncio.gather(*tasks)644 645    @override646    async def on_llm_new_token(647        self,648        token: str,649        *,650        chunk: GenerationChunk | ChatGenerationChunk | None = None,651        run_id: UUID,652        parent_run_id: UUID | None = None,653        **kwargs: Any,654    ) -> None:655        llm_run = self._llm_run_with_token_event(656            token=token,657            run_id=run_id,658            chunk=chunk,659            parent_run_id=parent_run_id,660        )661        await self._on_llm_new_token(llm_run, token, chunk)662 663    @override664    async def on_retry(665        self,666        retry_state: RetryCallState,667        *,668        run_id: UUID,669        **kwargs: Any,670    ) -> None:671        self._llm_run_with_retry_event(672            retry_state=retry_state,673            run_id=run_id,674        )675 676    @override677    async def on_llm_end(678        self,679        response: LLMResult,680        *,681        run_id: UUID,682        parent_run_id: UUID | None = None,683        tags: list[str] | None = None,684        **kwargs: Any,685    ) -> None:686        """End a trace for an LLM or chat model run.687 688        Note:689            This async callback also handles both run types. Async chat models690            start with `on_chat_model_start`, but there is no691            `on_chat_model_end`; completion is routed here for callback API692            compatibility.693        """694        llm_run = self._complete_llm_run(695            response=response,696            run_id=run_id,697        )698        tasks = [self._on_llm_end(llm_run), self._end_trace(llm_run)]699        await asyncio.gather(*tasks)700 701    @override702    async def on_llm_error(703        self,704        error: BaseException,705        *,706        run_id: UUID,707        parent_run_id: UUID | None = None,708        tags: list[str] | None = None,709        **kwargs: Any,710    ) -> None:711        llm_run = self._errored_llm_run(712            error=error,713            run_id=run_id,714        )715        tasks = [self._on_llm_error(llm_run), self._end_trace(llm_run)]716        await asyncio.gather(*tasks)717 718    @override719    async def on_chain_start(720        self,721        serialized: dict[str, Any],722        inputs: dict[str, Any],723        *,724        run_id: UUID,725        tags: list[str] | None = None,726        parent_run_id: UUID | None = None,727        metadata: dict[str, Any] | None = None,728        run_type: str | None = None,729        name: str | None = None,730        **kwargs: Any,731    ) -> None:732        chain_run = self._create_chain_run(733            serialized=serialized,734            inputs=inputs,735            run_id=run_id,736            tags=tags,737            parent_run_id=parent_run_id,738            metadata=metadata,739            run_type=run_type,740            name=name,741            **kwargs,742        )743        tasks = [self._start_trace(chain_run), self._on_chain_start(chain_run)]744        await asyncio.gather(*tasks)745 746    @override747    async def on_chain_end(748        self,749        outputs: dict[str, Any],750        *,751        run_id: UUID,752        inputs: dict[str, Any] | None = None,753        **kwargs: Any,754    ) -> None:755        chain_run = self._complete_chain_run(756            outputs=outputs,757            run_id=run_id,758            inputs=inputs,759        )760        tasks = [self._end_trace(chain_run), self._on_chain_end(chain_run)]761        await asyncio.gather(*tasks)762 763    @override764    async def on_chain_error(765        self,766        error: BaseException,767        *,768        inputs: dict[str, Any] | None = None,769        run_id: UUID,770        **kwargs: Any,771    ) -> None:772        chain_run = self._errored_chain_run(773            error=error,774            inputs=inputs,775            run_id=run_id,776        )777        tasks = [self._end_trace(chain_run), self._on_chain_error(chain_run)]778        await asyncio.gather(*tasks)779 780    @override781    async def on_tool_start(782        self,783        serialized: dict[str, Any],784        input_str: str,785        *,786        run_id: UUID,787        tags: list[str] | None = None,788        parent_run_id: UUID | None = None,789        metadata: dict[str, Any] | None = None,790        name: str | None = None,791        inputs: dict[str, Any] | None = None,792        **kwargs: Any,793    ) -> None:794        tool_run = self._create_tool_run(795            serialized=serialized,796            input_str=input_str,797            run_id=run_id,798            tags=tags,799            parent_run_id=parent_run_id,800            metadata=metadata,801            inputs=inputs,802            **kwargs,803        )804        tasks = [self._start_trace(tool_run), self._on_tool_start(tool_run)]805        await asyncio.gather(*tasks)806 807    @override808    async def on_tool_end(809        self,810        output: Any,811        *,812        run_id: UUID,813        **kwargs: Any,814    ) -> None:815        tool_run = self._complete_tool_run(816            output=output,817            run_id=run_id,818        )819        tasks = [self._end_trace(tool_run), self._on_tool_end(tool_run)]820        await asyncio.gather(*tasks)821 822    @override823    async def on_tool_error(824        self,825        error: BaseException,826        *,827        run_id: UUID,828        parent_run_id: UUID | None = None,829        tags: list[str] | None = None,830        **kwargs: Any,831    ) -> None:832        tool_run = self._errored_tool_run(833            error=error,834            run_id=run_id,835        )836        tasks = [self._end_trace(tool_run), self._on_tool_error(tool_run)]837        await asyncio.gather(*tasks)838 839    @override840    async def on_retriever_start(841        self,842        serialized: dict[str, Any],843        query: str,844        *,845        run_id: UUID,846        parent_run_id: UUID | None = None,847        tags: list[str] | None = None,848        metadata: dict[str, Any] | None = None,849        name: str | None = None,850        **kwargs: Any,851    ) -> None:852        retriever_run = self._create_retrieval_run(853            serialized=serialized,854            query=query,855            run_id=run_id,856            parent_run_id=parent_run_id,857            tags=tags,858            metadata=metadata,859            name=name,860        )861        tasks = [862            self._start_trace(retriever_run),863            self._on_retriever_start(retriever_run),864        ]865        await asyncio.gather(*tasks)866 867    @override868    async def on_retriever_error(869        self,870        error: BaseException,871        *,872        run_id: UUID,873        parent_run_id: UUID | None = None,874        tags: list[str] | None = None,875        **kwargs: Any,876    ) -> None:877        retrieval_run = self._errored_retrieval_run(878            error=error,879            run_id=run_id,880        )881        tasks = [882            self._end_trace(retrieval_run),883            self._on_retriever_error(retrieval_run),884        ]885        await asyncio.gather(*tasks)886 887    @override888    async def on_retriever_end(889        self,890        documents: Sequence[Document],891        *,892        run_id: UUID,893        parent_run_id: UUID | None = None,894        tags: list[str] | None = None,895        **kwargs: Any,896    ) -> None:897        retrieval_run = self._complete_retrieval_run(898            documents=documents,899            run_id=run_id,900        )901        tasks = [self._end_trace(retrieval_run), self._on_retriever_end(retrieval_run)]902        await asyncio.gather(*tasks)903 904    async def _on_run_create(self, run: Run) -> None:905        """Process a run upon creation."""906 907    async def _on_run_update(self, run: Run) -> None:908        """Process a run upon update."""909 910    async def _on_llm_start(self, run: Run) -> None:911        """Process the LLM Run upon start."""912 913    async def _on_llm_end(self, run: Run) -> None:914        """Process LLM/chat model run completion."""915 916    async def _on_llm_error(self, run: Run) -> None:917        """Process the LLM Run upon error."""918 919    async def _on_llm_new_token(920        self,921        run: Run,922        token: str,923        chunk: GenerationChunk | ChatGenerationChunk | None,924    ) -> None:925        """Process new LLM token."""926 927    async def _on_chain_start(self, run: Run) -> None:928        """Process the Chain Run upon start."""929 930    async def _on_chain_end(self, run: Run) -> None:931        """Process the Chain Run."""932 933    async def _on_chain_error(self, run: Run) -> None:934        """Process the Chain Run upon error."""935 936    async def _on_tool_start(self, run: Run) -> None:937        """Process the Tool Run upon start."""938 939    async def _on_tool_end(self, run: Run) -> None:940        """Process the Tool Run."""941 942    async def _on_tool_error(self, run: Run) -> None:943        """Process the Tool Run upon error."""944 945    async def _on_chat_model_start(self, run: Run) -> None:946        """Process the Chat Model Run upon start."""947 948    async def _on_retriever_start(self, run: Run) -> None:949        """Process the Retriever Run upon start."""950 951    async def _on_retriever_end(self, run: Run) -> None:952        """Process the Retriever Run."""953 954    async def _on_retriever_error(self, run: Run) -> None:955        """Process the Retriever Run upon error."""956 
codekingpro/portable-devtools · Team Ai