#
tokens: 47713/50000 6/391 files (page 13/17)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 13 of 17. Use http://codebase.md/stanfordnlp/dspy?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .github
│   ├── .internal_dspyai
│   │   ├── internals
│   │   │   ├── build-and-release.md
│   │   │   └── release-checklist.md
│   │   └── pyproject.toml
│   ├── .tmp
│   │   └── .generated-actions
│   │       └── run-pypi-publish-in-docker-container
│   │           └── action.yml
│   ├── ISSUE_TEMPLATE
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   ├── PULL_REQUEST_TEMPLATE
│   │   └── pull_request_template.md
│   ├── workflow_scripts
│   │   └── install_testpypi_pkg.sh
│   └── workflows
│       ├── build_and_release.yml
│       ├── build_utils
│       │   └── test_version.py
│       ├── docs-push.yml
│       ├── precommits_check.yml
│       └── run_tests.yml
├── .gitignore
├── .pre-commit-config.yaml
├── CONTRIBUTING.md
├── docs
│   ├── .gitignore
│   ├── docs
│   │   ├── api
│   │   │   ├── adapters
│   │   │   │   ├── Adapter.md
│   │   │   │   ├── ChatAdapter.md
│   │   │   │   ├── JSONAdapter.md
│   │   │   │   └── TwoStepAdapter.md
│   │   │   ├── evaluation
│   │   │   │   ├── answer_exact_match.md
│   │   │   │   ├── answer_passage_match.md
│   │   │   │   ├── CompleteAndGrounded.md
│   │   │   │   ├── Evaluate.md
│   │   │   │   ├── EvaluationResult.md
│   │   │   │   └── SemanticF1.md
│   │   │   ├── experimental
│   │   │   │   ├── Citations.md
│   │   │   │   └── Document.md
│   │   │   ├── index.md
│   │   │   ├── models
│   │   │   │   ├── Embedder.md
│   │   │   │   └── LM.md
│   │   │   ├── modules
│   │   │   │   ├── BestOfN.md
│   │   │   │   ├── ChainOfThought.md
│   │   │   │   ├── CodeAct.md
│   │   │   │   ├── Module.md
│   │   │   │   ├── MultiChainComparison.md
│   │   │   │   ├── Parallel.md
│   │   │   │   ├── Predict.md
│   │   │   │   ├── ProgramOfThought.md
│   │   │   │   ├── ReAct.md
│   │   │   │   └── Refine.md
│   │   │   ├── optimizers
│   │   │   │   ├── BetterTogether.md
│   │   │   │   ├── BootstrapFewShot.md
│   │   │   │   ├── BootstrapFewShotWithRandomSearch.md
│   │   │   │   ├── BootstrapFinetune.md
│   │   │   │   ├── BootstrapRS.md
│   │   │   │   ├── COPRO.md
│   │   │   │   ├── Ensemble.md
│   │   │   │   ├── GEPA
│   │   │   │   │   ├── GEPA_Advanced.md
│   │   │   │   │   └── overview.md
│   │   │   │   ├── InferRules.md
│   │   │   │   ├── KNN.md
│   │   │   │   ├── KNNFewShot.md
│   │   │   │   ├── LabeledFewShot.md
│   │   │   │   ├── MIPROv2.md
│   │   │   │   └── SIMBA.md
│   │   │   ├── primitives
│   │   │   │   ├── Audio.md
│   │   │   │   ├── Code.md
│   │   │   │   ├── Example.md
│   │   │   │   ├── History.md
│   │   │   │   ├── Image.md
│   │   │   │   ├── Prediction.md
│   │   │   │   ├── Tool.md
│   │   │   │   └── ToolCalls.md
│   │   │   ├── signatures
│   │   │   │   ├── InputField.md
│   │   │   │   ├── OutputField.md
│   │   │   │   └── Signature.md
│   │   │   ├── tools
│   │   │   │   ├── ColBERTv2.md
│   │   │   │   ├── Embeddings.md
│   │   │   │   └── PythonInterpreter.md
│   │   │   └── utils
│   │   │       ├── asyncify.md
│   │   │       ├── configure_cache.md
│   │   │       ├── disable_litellm_logging.md
│   │   │       ├── disable_logging.md
│   │   │       ├── enable_litellm_logging.md
│   │   │       ├── enable_logging.md
│   │   │       ├── inspect_history.md
│   │   │       ├── load.md
│   │   │       ├── StatusMessage.md
│   │   │       ├── StatusMessageProvider.md
│   │   │       ├── streamify.md
│   │   │       └── StreamListener.md
│   │   ├── cheatsheet.md
│   │   ├── community
│   │   │   ├── community-resources.md
│   │   │   ├── how-to-contribute.md
│   │   │   └── use-cases.md
│   │   ├── deep-dive
│   │   │   └── data-handling
│   │   │       ├── built-in-datasets.md
│   │   │       ├── examples.md
│   │   │       ├── img
│   │   │       │   └── data-loading.png
│   │   │       └── loading-custom-data.md
│   │   ├── faqs.md
│   │   ├── index.md
│   │   ├── js
│   │   │   └── runllm-widget.js
│   │   ├── learn
│   │   │   ├── evaluation
│   │   │   │   ├── data.md
│   │   │   │   ├── metrics.md
│   │   │   │   └── overview.md
│   │   │   ├── figures
│   │   │   │   ├── native_tool_call.png
│   │   │   │   └── teleprompter-classes.png
│   │   │   ├── index.md
│   │   │   ├── optimization
│   │   │   │   ├── optimizers.md
│   │   │   │   └── overview.md
│   │   │   └── programming
│   │   │       ├── 7-assertions.md
│   │   │       ├── adapters.md
│   │   │       ├── language_models.md
│   │   │       ├── mcp.md
│   │   │       ├── modules.md
│   │   │       ├── overview.md
│   │   │       ├── signatures.md
│   │   │       └── tools.md
│   │   ├── production
│   │   │   └── index.md
│   │   ├── roadmap.md
│   │   ├── static
│   │   │   ├── .nojekyll
│   │   │   └── img
│   │   │       ├── dspy_logo.png
│   │   │       ├── logo.png
│   │   │       ├── mlflow-tracing-rag.png
│   │   │       ├── modular.png
│   │   │       ├── optimize.png
│   │   │       ├── undraw_docusaurus_mountain.svg
│   │   │       ├── undraw_docusaurus_react.svg
│   │   │       ├── undraw_docusaurus_tree.svg
│   │   │       └── universal_compatibility.png
│   │   ├── stylesheets
│   │   │   └── extra.css
│   │   └── tutorials
│   │       ├── agents
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-agent.png
│   │       ├── ai_text_game
│   │       │   └── index.md
│   │       ├── async
│   │       │   └── index.md
│   │       ├── audio
│   │       │   └── index.ipynb
│   │       ├── build_ai_program
│   │       │   └── index.md
│   │       ├── cache
│   │       │   └── index.md
│   │       ├── classification
│   │       │   └── index.md
│   │       ├── classification_finetuning
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-classification.png
│   │       ├── conversation_history
│   │       │   └── index.md
│   │       ├── core_development
│   │       │   └── index.md
│   │       ├── custom_module
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-custom-module.png
│   │       ├── customer_service_agent
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-customer-service-agent.png
│   │       ├── deployment
│   │       │   ├── dspy_mlflow_ui.png
│   │       │   └── index.md
│   │       ├── email_extraction
│   │       │   ├── index.md
│   │       │   └── mlflow-tracing-email-extraction.png
│   │       ├── entity_extraction
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-entity-extraction.png
│   │       ├── games
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-agent.png
│   │       ├── gepa_ai_program
│   │       │   └── index.md
│   │       ├── gepa_aime
│   │       │   ├── index.ipynb
│   │       │   ├── mlflow-tracing-gepa-aime.png
│   │       │   └── mlflow-tracking-gepa-aime-optimization.png
│   │       ├── gepa_facilitysupportanalyzer
│   │       │   ├── index.ipynb
│   │       │   ├── mlflow-tracing-gepa-support.png
│   │       │   └── mlflow-tracking-gepa-support-optimization.png
│   │       ├── gepa_papillon
│   │       │   ├── index.ipynb
│   │       │   ├── mlflow-tracing-gepa-papilon.png
│   │       │   └── mlflow-tracking-gepa-papilon-optimization.png
│   │       ├── image_generation_prompting
│   │       │   └── index.ipynb
│   │       ├── index.md
│   │       ├── llms_txt_generation
│   │       │   └── index.md
│   │       ├── math
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-math.png
│   │       ├── mcp
│   │       │   └── index.md
│   │       ├── mem0_react_agent
│   │       │   └── index.md
│   │       ├── multihop_search
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-multi-hop.png
│   │       ├── observability
│   │       │   ├── index.md
│   │       │   ├── mlflow_trace_ui_navigation.gif
│   │       │   ├── mlflow_trace_ui.png
│   │       │   └── mlflow_trace_view.png
│   │       ├── optimize_ai_program
│   │       │   └── index.md
│   │       ├── optimizer_tracking
│   │       │   ├── child_run.png
│   │       │   ├── experiment.png
│   │       │   ├── index.md
│   │       │   └── parent_run.png
│   │       ├── output_refinement
│   │       │   └── best-of-n-and-refine.md
│   │       ├── papillon
│   │       │   └── index.md
│   │       ├── program_of_thought
│   │       │   └── index.ipynb
│   │       ├── rag
│   │       │   ├── index.ipynb
│   │       │   └── mlflow-tracing-rag.png
│   │       ├── real_world_examples
│   │       │   └── index.md
│   │       ├── rl_ai_program
│   │       │   └── index.md
│   │       ├── rl_multihop
│   │       │   └── index.ipynb
│   │       ├── rl_papillon
│   │       │   └── index.ipynb
│   │       ├── sample_code_generation
│   │       │   └── index.md
│   │       ├── saving
│   │       │   └── index.md
│   │       ├── streaming
│   │       │   └── index.md
│   │       ├── tool_use
│   │       │   └── index.ipynb
│   │       └── yahoo_finance_react
│   │           └── index.md
│   ├── mkdocs.yml
│   ├── overrides
│   │   ├── home.html
│   │   ├── main.html
│   │   └── partials
│   │       └── tabs.html
│   ├── Pipfile
│   ├── Pipfile.lock
│   ├── README.md
│   ├── requirements.txt
│   ├── scripts
│   │   ├── generate_api_docs.py
│   │   └── generate_api_summary.py
│   └── vercel.json
├── dspy
│   ├── __init__.py
│   ├── __metadata__.py
│   ├── adapters
│   │   ├── __init__.py
│   │   ├── baml_adapter.py
│   │   ├── base.py
│   │   ├── chat_adapter.py
│   │   ├── json_adapter.py
│   │   ├── two_step_adapter.py
│   │   ├── types
│   │   │   ├── __init__.py
│   │   │   ├── audio.py
│   │   │   ├── base_type.py
│   │   │   ├── citation.py
│   │   │   ├── code.py
│   │   │   ├── document.py
│   │   │   ├── history.py
│   │   │   ├── image.py
│   │   │   └── tool.py
│   │   ├── utils.py
│   │   └── xml_adapter.py
│   ├── clients
│   │   ├── __init__.py
│   │   ├── base_lm.py
│   │   ├── cache.py
│   │   ├── databricks.py
│   │   ├── embedding.py
│   │   ├── lm_local_arbor.py
│   │   ├── lm_local.py
│   │   ├── lm.py
│   │   ├── openai.py
│   │   ├── provider.py
│   │   └── utils_finetune.py
│   ├── datasets
│   │   ├── __init__.py
│   │   ├── alfworld
│   │   │   ├── __init__.py
│   │   │   ├── alfworld.py
│   │   │   └── base_config.yml
│   │   ├── colors.py
│   │   ├── dataloader.py
│   │   ├── dataset.py
│   │   ├── gsm8k.py
│   │   ├── hotpotqa.py
│   │   └── math.py
│   ├── dsp
│   │   ├── __init__.py
│   │   ├── colbertv2.py
│   │   └── utils
│   │       ├── __init__.py
│   │       ├── dpr.py
│   │       ├── settings.py
│   │       └── utils.py
│   ├── evaluate
│   │   ├── __init__.py
│   │   ├── auto_evaluation.py
│   │   ├── evaluate.py
│   │   └── metrics.py
│   ├── experimental
│   │   └── __init__.py
│   ├── predict
│   │   ├── __init__.py
│   │   ├── aggregation.py
│   │   ├── avatar
│   │   │   ├── __init__.py
│   │   │   ├── avatar.py
│   │   │   ├── models.py
│   │   │   └── signatures.py
│   │   ├── best_of_n.py
│   │   ├── chain_of_thought.py
│   │   ├── code_act.py
│   │   ├── knn.py
│   │   ├── multi_chain_comparison.py
│   │   ├── parallel.py
│   │   ├── parameter.py
│   │   ├── predict.py
│   │   ├── program_of_thought.py
│   │   ├── react.py
│   │   ├── refine.py
│   │   └── retry.py
│   ├── primitives
│   │   ├── __init__.py
│   │   ├── base_module.py
│   │   ├── example.py
│   │   ├── module.py
│   │   ├── prediction.py
│   │   ├── python_interpreter.py
│   │   └── runner.js
│   ├── propose
│   │   ├── __init__.py
│   │   ├── dataset_summary_generator.py
│   │   ├── grounded_proposer.py
│   │   ├── propose_base.py
│   │   └── utils.py
│   ├── retrievers
│   │   ├── __init__.py
│   │   ├── databricks_rm.py
│   │   ├── embeddings.py
│   │   ├── retrieve.py
│   │   └── weaviate_rm.py
│   ├── signatures
│   │   ├── __init__.py
│   │   ├── field.py
│   │   ├── signature.py
│   │   └── utils.py
│   ├── streaming
│   │   ├── __init__.py
│   │   ├── messages.py
│   │   ├── streamify.py
│   │   └── streaming_listener.py
│   ├── teleprompt
│   │   ├── __init__.py
│   │   ├── avatar_optimizer.py
│   │   ├── bettertogether.py
│   │   ├── bootstrap_finetune.py
│   │   ├── bootstrap_trace.py
│   │   ├── bootstrap.py
│   │   ├── copro_optimizer.py
│   │   ├── ensemble.py
│   │   ├── gepa
│   │   │   ├── __init__.py
│   │   │   ├── gepa_utils.py
│   │   │   ├── gepa.py
│   │   │   └── instruction_proposal.py
│   │   ├── grpo.py
│   │   ├── infer_rules.py
│   │   ├── knn_fewshot.py
│   │   ├── mipro_optimizer_v2.py
│   │   ├── random_search.py
│   │   ├── signature_opt.py
│   │   ├── simba_utils.py
│   │   ├── simba.py
│   │   ├── teleprompt_optuna.py
│   │   ├── teleprompt.py
│   │   ├── utils.py
│   │   └── vanilla.py
│   └── utils
│       ├── __init__.py
│       ├── annotation.py
│       ├── asyncify.py
│       ├── caching.py
│       ├── callback.py
│       ├── dummies.py
│       ├── exceptions.py
│       ├── hasher.py
│       ├── inspect_history.py
│       ├── langchain_tool.py
│       ├── logging_utils.py
│       ├── mcp.py
│       ├── parallelizer.py
│       ├── saving.py
│       ├── syncify.py
│       ├── unbatchify.py
│       └── usage_tracker.py
├── LICENSE
├── pyproject.toml
├── README.md
├── tests
│   ├── __init__.py
│   ├── adapters
│   │   ├── test_adapter_utils.py
│   │   ├── test_baml_adapter.py
│   │   ├── test_base_type.py
│   │   ├── test_chat_adapter.py
│   │   ├── test_citation.py
│   │   ├── test_code.py
│   │   ├── test_document.py
│   │   ├── test_json_adapter.py
│   │   ├── test_tool.py
│   │   ├── test_two_step_adapter.py
│   │   └── test_xml_adapter.py
│   ├── callback
│   │   └── test_callback.py
│   ├── clients
│   │   ├── test_cache.py
│   │   ├── test_databricks.py
│   │   ├── test_embedding.py
│   │   ├── test_inspect_global_history.py
│   │   └── test_lm.py
│   ├── conftest.py
│   ├── datasets
│   │   └── test_dataset.py
│   ├── docs
│   │   └── test_mkdocs_links.py
│   ├── evaluate
│   │   ├── test_evaluate.py
│   │   └── test_metrics.py
│   ├── examples
│   │   └── test_baleen.py
│   ├── metadata
│   │   └── test_metadata.py
│   ├── predict
│   │   ├── test_aggregation.py
│   │   ├── test_best_of_n.py
│   │   ├── test_chain_of_thought.py
│   │   ├── test_code_act.py
│   │   ├── test_knn.py
│   │   ├── test_multi_chain_comparison.py
│   │   ├── test_parallel.py
│   │   ├── test_predict.py
│   │   ├── test_program_of_thought.py
│   │   ├── test_react.py
│   │   ├── test_refine.py
│   │   └── test_retry.py
│   ├── primitives
│   │   ├── resources
│   │   │   └── saved_program.json
│   │   ├── test_base_module.py
│   │   ├── test_example.py
│   │   ├── test_module.py
│   │   └── test_python_interpreter.py
│   ├── propose
│   │   └── test_grounded_proposer.py
│   ├── README.md
│   ├── reliability
│   │   ├── __init__.py
│   │   ├── complex_types
│   │   │   └── generated
│   │   │       ├── test_many_types_1
│   │   │       │   ├── inputs
│   │   │       │   │   ├── input1.json
│   │   │       │   │   └── input2.json
│   │   │       │   ├── program.py
│   │   │       │   └── schema.json
│   │   │       ├── test_nesting_1
│   │   │       │   ├── inputs
│   │   │       │   │   ├── input1.json
│   │   │       │   │   └── input2.json
│   │   │       │   ├── program.py
│   │   │       │   └── schema.json
│   │   │       └── test_nesting_2
│   │   │           ├── inputs
│   │   │           │   └── input1.json
│   │   │           ├── program.py
│   │   │           └── schema.json
│   │   ├── conftest.py
│   │   ├── generate
│   │   │   ├── __init__.py
│   │   │   ├── __main__.py
│   │   │   └── utils.py
│   │   ├── input_formats
│   │   │   └── generated
│   │   │       └── test_markdown_1
│   │   │           ├── inputs
│   │   │           │   ├── input1.json
│   │   │           │   └── input2.json
│   │   │           ├── program.py
│   │   │           └── schema.json
│   │   ├── README.md
│   │   ├── reliability_conf.yaml
│   │   ├── test_generated.py
│   │   ├── test_pydantic_models.py
│   │   └── utils.py
│   ├── retrievers
│   │   └── test_embeddings.py
│   ├── signatures
│   │   ├── test_adapter_image.py
│   │   ├── test_custom_types.py
│   │   └── test_signature.py
│   ├── streaming
│   │   └── test_streaming.py
│   ├── teleprompt
│   │   ├── gepa_dummy_lm_custom_component_selector_custom_instruction_proposer.json
│   │   ├── gepa_dummy_lm.json
│   │   ├── test_bootstrap_finetune.py
│   │   ├── test_bootstrap_trace.py
│   │   ├── test_bootstrap.py
│   │   ├── test_copro_optimizer.py
│   │   ├── test_ensemble.py
│   │   ├── test_finetune.py
│   │   ├── test_gepa_instruction_proposer.py
│   │   ├── test_gepa.py
│   │   ├── test_grpo.py
│   │   ├── test_knn_fewshot.py
│   │   ├── test_random_search.py
│   │   ├── test_teleprompt.py
│   │   └── test_utils.py
│   ├── test_utils
│   │   ├── __init__.py
│   │   └── server
│   │       ├── __init__.py
│   │       ├── litellm_server_config.yaml
│   │       └── litellm_server.py
│   └── utils
│       ├── __init__.py
│       ├── resources
│       │   └── mcp_server.py
│       ├── test_annotation.py
│       ├── test_asyncify.py
│       ├── test_exceptions.py
│       ├── test_langchain_tool.py
│       ├── test_mcp.py
│       ├── test_parallelizer.py
│       ├── test_saving.py
│       ├── test_settings.py
│       ├── test_syncify.py
│       ├── test_unbatchify.py
│       └── test_usage_tracker.py
└── uv.lock
```

# Files

--------------------------------------------------------------------------------
/tests/signatures/test_signature.py:
--------------------------------------------------------------------------------

```python
  1 | from types import UnionType
  2 | from typing import Any, Optional, Union
  3 | 
  4 | import pydantic
  5 | import pytest
  6 | 
  7 | import dspy
  8 | from dspy import InputField, OutputField, Signature, infer_prefix
  9 | from dspy.utils.dummies import DummyLM
 10 | 
 11 | 
 12 | def test_field_types_and_custom_attributes():
 13 |     class TestSignature(Signature):
 14 |         """Instructions"""
 15 | 
 16 |         input1: str = InputField()
 17 |         input2: int = InputField()
 18 |         output1: list[str] = OutputField()
 19 |         output2 = OutputField()
 20 | 
 21 |     assert TestSignature.instructions == "Instructions"
 22 |     assert TestSignature.input_fields["input1"].annotation == str
 23 |     assert TestSignature.input_fields["input2"].annotation == int
 24 |     assert TestSignature.output_fields["output1"].annotation == list[str]
 25 |     assert TestSignature.output_fields["output2"].annotation == str
 26 | 
 27 | 
 28 | def test_no_input_output():
 29 |     with pytest.raises(TypeError):
 30 | 
 31 |         class TestSignature(Signature):
 32 |             input1: str
 33 | 
 34 | 
 35 | def test_no_input_output2():
 36 |     with pytest.raises(TypeError):
 37 | 
 38 |         class TestSignature(Signature):
 39 |             input1: str = pydantic.Field()
 40 | 
 41 | 
 42 | def test_all_fields_have_prefix():
 43 |     class TestSignature(Signature):
 44 |         input = InputField(prefix="Modified:")
 45 |         output = OutputField()
 46 | 
 47 |     assert TestSignature.input_fields["input"].json_schema_extra["prefix"] == "Modified:"
 48 |     assert TestSignature.output_fields["output"].json_schema_extra["prefix"] == "Output:"
 49 | 
 50 | 
 51 | def test_signature_parsing():
 52 |     signature = Signature("input1, input2 -> output")
 53 |     assert "input1" in signature.input_fields
 54 |     assert "input2" in signature.input_fields
 55 |     assert "output" in signature.output_fields
 56 | 
 57 | 
 58 | def test_with_signature():
 59 |     signature1 = Signature("input1, input2 -> output")
 60 |     signature2 = signature1.with_instructions("This is a test")
 61 |     assert signature2.instructions == "This is a test"
 62 |     assert signature1 is not signature2, "The type should be immutable"
 63 | 
 64 | 
 65 | def test_with_updated_field():
 66 |     signature1 = Signature("input1, input2 -> output")
 67 |     signature2 = signature1.with_updated_fields("input1", prefix="Modified:")
 68 |     assert signature2.input_fields["input1"].json_schema_extra["prefix"] == "Modified:"
 69 |     assert signature1.input_fields["input1"].json_schema_extra["prefix"] == "Input 1:"
 70 |     assert signature1 is not signature2, "The type should be immutable"
 71 |     for key in signature1.fields.keys():
 72 |         if key != "input1":
 73 |             assert signature1.fields[key].json_schema_extra == signature2.fields[key].json_schema_extra
 74 |     assert signature1.instructions == signature2.instructions
 75 | 
 76 | 
 77 | def test_empty_signature():
 78 |     with pytest.raises(ValueError):
 79 |         Signature("")
 80 | 
 81 | 
 82 | def test_instructions_signature():
 83 |     with pytest.raises(ValueError):
 84 |         Signature("")
 85 | 
 86 | 
 87 | def test_signature_instructions():
 88 |     sig1 = Signature("input1 -> output1", instructions="This is a test")
 89 |     assert sig1.instructions == "This is a test"
 90 |     sig2 = Signature("input1 -> output1", "This is a test")
 91 |     assert sig2.instructions == "This is a test"
 92 | 
 93 | 
 94 | def test_signature_instructions_none():
 95 |     sig1 = Signature("a, b -> c")
 96 |     assert sig1.instructions == "Given the fields `a`, `b`, produce the fields `c`."
 97 | 
 98 | 
 99 | def test_signature_from_dict():
100 |     signature = Signature(
101 |         {"input1": InputField(), "input2": InputField(), "output": OutputField()})
102 |     for k in ["input1", "input2", "output"]:
103 |         assert k in signature.fields
104 |         assert signature.fields[k].annotation == str
105 | 
106 | 
107 | def test_signature_equality():
108 |     sig1 = Signature("input1 -> output1")
109 |     sig2 = Signature("input1 -> output1")
110 |     assert sig1.equals(sig2)
111 | 
112 | 
113 | def test_signature_inequality():
114 |     sig1 = Signature("input1 -> output1")
115 |     sig2 = Signature("input2 -> output2")
116 |     assert not sig1.equals(sig2)
117 | 
118 | 
119 | def test_equality_format():
120 |     class TestSignature(Signature):
121 |         input = InputField(format=lambda x: x)
122 |         output = OutputField()
123 | 
124 |     assert TestSignature.equals(TestSignature)
125 | 
126 | 
127 | def test_signature_reverse():
128 |     sig = Signature("input1 -> output1")
129 |     assert sig.signature == "input1 -> output1"
130 | 
131 | 
132 | def test_insert_field_at_various_positions():
133 |     class InitialSignature(Signature):
134 |         input1: str = InputField()
135 |         output1: int = OutputField()
136 | 
137 |     s1 = InitialSignature.prepend("new_input_start", InputField(), str)
138 |     s2 = InitialSignature.append("new_input_end", InputField(), str)
139 |     assert "new_input_start" == list(s1.input_fields.keys())[0]  # noqa: RUF015
140 |     assert "new_input_end" == list(s2.input_fields.keys())[-1]
141 | 
142 |     s3 = InitialSignature.prepend("new_output_start", OutputField(), str)
143 |     s4 = InitialSignature.append("new_output_end", OutputField(), str)
144 |     assert "new_output_start" == list(s3.output_fields.keys())[0]  # noqa: RUF015
145 |     assert "new_output_end" == list(s4.output_fields.keys())[-1]
146 | 
147 | 
148 | def test_order_preserved_with_mixed_annotations():
149 |     class ExampleSignature(dspy.Signature):
150 |         text: str = dspy.InputField()
151 |         output = dspy.OutputField()
152 |         pass_evaluation: bool = dspy.OutputField()
153 | 
154 |     expected_order = ["text", "output", "pass_evaluation"]
155 |     actual_order = list(ExampleSignature.fields.keys())
156 |     assert actual_order == expected_order
157 | 
158 | 
159 | def test_infer_prefix():
160 |     assert infer_prefix(
161 |         "someAttributeName42IsCool") == "Some Attribute Name 42 Is Cool"
162 |     assert infer_prefix("version2Update") == "Version 2 Update"
163 |     assert infer_prefix("modelT45Enhanced") == "Model T 45 Enhanced"
164 |     assert infer_prefix("someAttributeName") == "Some Attribute Name"
165 |     assert infer_prefix("some_attribute_name") == "Some Attribute Name"
166 |     assert infer_prefix("URLAddress") == "URL Address"
167 |     assert infer_prefix("isHTTPSecure") == "Is HTTP Secure"
168 |     assert infer_prefix("isHTTPSSecure123") == "Is HTTPS Secure 123"
169 | 
170 | 
171 | def test_insantiating():
172 |     sig = Signature("input -> output")
173 |     assert issubclass(sig, Signature)
174 |     assert sig.__name__ == "StringSignature"
175 |     value = sig(input="test", output="test")
176 |     assert isinstance(value, sig)
177 | 
178 | 
179 | def test_insantiating2():
180 |     class SubSignature(Signature):
181 |         input = InputField()
182 |         output = OutputField()
183 | 
184 |     assert issubclass(SubSignature, Signature)
185 |     assert SubSignature.__name__ == "SubSignature"
186 |     value = SubSignature(input="test", output="test")
187 |     assert isinstance(value, SubSignature)
188 | 
189 | 
190 | def test_multiline_instructions():
191 |     lm = DummyLM([{"output": "short answer"}])
192 |     dspy.settings.configure(lm=lm)
193 | 
194 |     class MySignature(Signature):
195 |         """First line
196 |         Second line
197 |             Third line"""
198 | 
199 |         output = OutputField()
200 | 
201 |     predictor = dspy.Predict(MySignature)
202 |     assert predictor().output == "short answer"
203 | 
204 | 
205 | def test_dump_and_load_state():
206 |     class CustomSignature(dspy.Signature):
207 |         """I am just an instruction."""
208 | 
209 |         sentence = dspy.InputField(desc="I am an innocent input!")
210 |         sentiment = dspy.OutputField()
211 | 
212 |     state = CustomSignature.dump_state()
213 |     expected = {
214 |         "instructions": "I am just an instruction.",
215 |         "fields": [
216 |             {
217 |                 "prefix": "Sentence:",
218 |                 "description": "I am an innocent input!",
219 |             },
220 |             {
221 |                 "prefix": "Sentiment:",
222 |                 "description": "${sentiment}",
223 |             },
224 |         ],
225 |     }
226 |     assert state == expected
227 | 
228 |     class CustomSignature2(dspy.Signature):
229 |         """I am a malicious instruction."""
230 | 
231 |         sentence = dspy.InputField(desc="I am an malicious input!")
232 |         sentiment = dspy.OutputField()
233 | 
234 |     assert CustomSignature2.dump_state() != expected
235 |     # Overwrite the state with the state of CustomSignature.
236 |     loaded_signature = CustomSignature2.load_state(state)
237 |     assert loaded_signature.instructions == "I am just an instruction."
238 |     # After `load_state`, the state should be the same as CustomSignature.
239 |     assert loaded_signature.dump_state() == expected
240 |     # CustomSignature2 should not have been modified.
241 |     assert CustomSignature2.instructions == "I am a malicious instruction."
242 |     assert CustomSignature2.fields["sentence"].json_schema_extra["desc"] == "I am an malicious input!"
243 |     assert CustomSignature2.fields["sentiment"].json_schema_extra["prefix"] == "Sentiment:"
244 | 
245 | 
246 | def test_typed_signatures_basic_types():
247 |     sig = Signature("input1: int, input2: str -> output: float")
248 |     assert "input1" in sig.input_fields
249 |     assert sig.input_fields["input1"].annotation == int
250 |     assert "input2" in sig.input_fields
251 |     assert sig.input_fields["input2"].annotation == str
252 |     assert "output" in sig.output_fields
253 |     assert sig.output_fields["output"].annotation == float
254 | 
255 | 
256 | def test_typed_signatures_generics():
257 |     sig = Signature(
258 |         "input_list: list[int], input_dict: dict[str, float] -> output_tuple: tuple[str, int]")
259 |     assert "input_list" in sig.input_fields
260 |     assert sig.input_fields["input_list"].annotation == list[int]
261 |     assert "input_dict" in sig.input_fields
262 |     assert sig.input_fields["input_dict"].annotation == dict[str, float]
263 |     assert "output_tuple" in sig.output_fields
264 |     assert sig.output_fields["output_tuple"].annotation == tuple[str, int]
265 | 
266 | 
267 | def test_typed_signatures_unions_and_optionals():
268 |     sig = Signature(
269 |         "input_opt: Optional[str], input_union: Union[int, None] -> output_union: Union[int, str]")
270 |     assert "input_opt" in sig.input_fields
271 |     # Optional[str] is actually Union[str, None]
272 |     # Depending on the environment, it might resolve to Union[str, None] or Optional[str], either is correct.
273 |     # We'll just check for a Union containing str and NoneType:
274 |     input_opt_annotation = sig.input_fields["input_opt"].annotation
275 |     assert input_opt_annotation == Optional[str] or (
276 |         getattr(input_opt_annotation, "__origin__", None) is Union
277 |         and str in input_opt_annotation.__args__
278 |         and type(None) in input_opt_annotation.__args__
279 |     )
280 | 
281 |     assert "input_union" in sig.input_fields
282 |     input_union_annotation = sig.input_fields["input_union"].annotation
283 |     assert (
284 |         getattr(input_union_annotation, "__origin__", None) is Union
285 |         and int in input_union_annotation.__args__
286 |         and type(None) in input_union_annotation.__args__
287 |     )
288 | 
289 |     assert "output_union" in sig.output_fields
290 |     output_union_annotation = sig.output_fields["output_union"].annotation
291 |     assert (
292 |         getattr(output_union_annotation, "__origin__", None) is Union
293 |         and int in output_union_annotation.__args__
294 |         and str in output_union_annotation.__args__
295 |     )
296 | 
297 | 
298 | def test_typed_signatures_any():
299 |     sig = Signature("input_any: Any -> output_any: Any")
300 |     assert "input_any" in sig.input_fields
301 |     assert sig.input_fields["input_any"].annotation == Any
302 |     assert "output_any" in sig.output_fields
303 |     assert sig.output_fields["output_any"].annotation == Any
304 | 
305 | 
306 | def test_typed_signatures_nested():
307 |     sig = Signature(
308 |         "input_nested: list[Union[str, int]] -> output_nested: Tuple[int, Optional[float], list[str]]")
309 |     input_nested_ann = sig.input_fields["input_nested"].annotation
310 |     assert getattr(input_nested_ann, "__origin__", None) is list
311 |     assert len(input_nested_ann.__args__) == 1
312 |     union_arg = input_nested_ann.__args__[0]
313 |     assert getattr(union_arg, "__origin__", None) is Union
314 |     assert str in union_arg.__args__ and int in union_arg.__args__
315 | 
316 |     output_nested_ann = sig.output_fields["output_nested"].annotation
317 |     assert getattr(output_nested_ann, "__origin__", None) is tuple
318 |     assert output_nested_ann.__args__[0] == int
319 |     # The second arg is Optional[float], which is Union[float, None]
320 |     second_arg = output_nested_ann.__args__[1]
321 |     assert getattr(second_arg, "__origin__", None) is Union
322 |     assert float in second_arg.__args__ and type(None) in second_arg.__args__
323 |     # The third arg is list[str]
324 |     third_arg = output_nested_ann.__args__[2]
325 |     assert getattr(third_arg, "__origin__", None) is list
326 |     assert third_arg.__args__[0] == str
327 | 
328 | 
329 | def test_typed_signatures_from_dict():
330 |     fields = {
331 |         "input_str_list": (list[str], InputField()),
332 |         "input_dict_int": (dict[str, int], InputField()),
333 |         "output_tup": (tuple[int, float], OutputField()),
334 |     }
335 |     sig = Signature(fields)
336 |     assert "input_str_list" in sig.input_fields
337 |     assert sig.input_fields["input_str_list"].annotation == list[str]
338 |     assert "input_dict_int" in sig.input_fields
339 |     assert sig.input_fields["input_dict_int"].annotation == dict[str, int]
340 |     assert "output_tup" in sig.output_fields
341 |     assert sig.output_fields["output_tup"].annotation == tuple[int, float]
342 | 
343 | 
344 | def test_typed_signatures_complex_combinations():
345 |     sig = Signature(
346 |         "input_complex: dict[str, list[Optional[Tuple[int, str]]]] -> output_complex: Union[list[str], dict[str, Any]]"
347 |     )
348 |     input_complex_ann = sig.input_fields["input_complex"].annotation
349 |     assert getattr(input_complex_ann, "__origin__", None) is dict
350 |     key_arg, value_arg = input_complex_ann.__args__
351 |     assert key_arg == str
352 |     # value_arg: list[Optional[Tuple[int, str]]]
353 |     assert getattr(value_arg, "__origin__", None) is list
354 |     inner_union = value_arg.__args__[0]
355 |     # inner_union should be Optional[Tuple[int, str]]
356 |     # which is Union[Tuple[int, str], None]
357 |     assert getattr(inner_union, "__origin__", None) is Union
358 |     tuple_type = [t for t in inner_union.__args__ if t != type(None)][0]  # noqa: RUF015
359 |     assert getattr(tuple_type, "__origin__", None) is tuple
360 |     assert tuple_type.__args__ == (int, str)
361 | 
362 |     output_complex_ann = sig.output_fields["output_complex"].annotation
363 |     assert getattr(output_complex_ann, "__origin__", None) is Union
364 |     assert len(output_complex_ann.__args__) == 2
365 |     possible_args = set(output_complex_ann.__args__)
366 |     # Expecting list[str] and dict[str, Any]
367 |     # Because sets don't preserve order, just check membership.
368 |     # Find the list[str] arg
369 |     list_arg = next(a for a in possible_args if getattr(
370 |         a, "__origin__", None) is list)
371 |     dict_arg = next(a for a in possible_args if getattr(
372 |         a, "__origin__", None) is dict)
373 |     assert list_arg.__args__ == (str,)
374 |     k, v = dict_arg.__args__
375 |     assert k == str and v == Any
376 | 
377 | 
378 | def test_make_signature_from_string():
379 |     sig = Signature(
380 |         "input1: int, input2: dict[str, int] -> output1: list[str], output2: Union[int, str]")
381 |     assert "input1" in sig.input_fields
382 |     assert sig.input_fields["input1"].annotation == int
383 |     assert "input2" in sig.input_fields
384 |     assert sig.input_fields["input2"].annotation == dict[str, int]
385 |     assert "output1" in sig.output_fields
386 |     assert sig.output_fields["output1"].annotation == list[str]
387 |     assert "output2" in sig.output_fields
388 |     assert sig.output_fields["output2"].annotation == Union[int, str]
389 | 
390 | 
391 | def test_signature_field_with_constraints():
392 |     class MySignature(Signature):
393 |         inputs: str = InputField()
394 |         outputs1: str = OutputField(min_length=5, max_length=10)
395 |         outputs2: int = OutputField(ge=5, le=10)
396 | 
397 |     assert "outputs1" in MySignature.output_fields
398 |     output1_constraints = MySignature.output_fields["outputs1"].json_schema_extra["constraints"]
399 | 
400 |     assert "minimum length: 5" in output1_constraints
401 |     assert "maximum length: 10" in output1_constraints
402 | 
403 |     assert "outputs2" in MySignature.output_fields
404 |     output2_constraints = MySignature.output_fields["outputs2"].json_schema_extra["constraints"]
405 |     assert "greater than or equal to: 5" in output2_constraints
406 |     assert "less than or equal to: 10" in output2_constraints
407 | 
408 | 
409 | def test_basic_custom_type():
410 |     class CustomType(pydantic.BaseModel):
411 |         value: str
412 | 
413 |     test_signature = dspy.Signature(
414 |         "input: CustomType -> output: str",
415 |         custom_types={"CustomType": CustomType}
416 |     )
417 | 
418 |     assert test_signature.input_fields["input"].annotation == CustomType
419 | 
420 |     lm = DummyLM([{"output": "processed"}])
421 |     dspy.settings.configure(lm=lm)
422 | 
423 |     custom_obj = CustomType(value="test")
424 |     pred = dspy.Predict(test_signature)(input=custom_obj)
425 |     assert pred.output == "processed"
426 | 
427 | 
428 | def test_custom_type_from_different_module():
429 |     from pathlib import Path
430 | 
431 |     test_signature = dspy.Signature("input: Path -> output: str")
432 |     assert test_signature.input_fields["input"].annotation == Path
433 | 
434 |     lm = DummyLM([{"output": "/test/path"}])
435 |     dspy.settings.configure(lm=lm)
436 | 
437 |     path_obj = Path("/test/path")
438 |     pred = dspy.Predict(test_signature)(input=path_obj)
439 |     assert pred.output == "/test/path"
440 | 
441 | def test_pep604_union_type_inline():
442 |     sig = Signature(
443 |         "input1: str | None, input2: None | int -> output_union: int | str"
444 |     )
445 | 
446 |     # input1 and input2 test that both 'T | None' and 'None | T' are interpreted as Optional types,
447 |     # regardless of the order of None in the union expression.
448 | 
449 |     assert "input1" in sig.input_fields
450 |     input1_annotation = sig.input_fields["input1"].annotation
451 |     assert input1_annotation == Optional[str] or (
452 |         getattr(input1_annotation, "__origin__", None) is Union
453 |         and str in input1_annotation.__args__
454 |         and type(None) in input1_annotation.__args__
455 |     )
456 | 
457 |     assert "input2" in sig.input_fields
458 |     input2_annotation = sig.input_fields["input2"].annotation
459 |     assert input2_annotation == Optional[int] or (
460 |         getattr(input2_annotation, "__origin__", None) is Union
461 |         and int in input2_annotation.__args__
462 |         and type(None) in input2_annotation.__args__
463 |     )
464 | 
465 |     assert "output_union" in sig.output_fields
466 |     output_union_annotation = sig.output_fields["output_union"].annotation
467 |     assert (
468 |         getattr(output_union_annotation, "__origin__", None) is Union
469 |         and int in output_union_annotation.__args__
470 |         and str in output_union_annotation.__args__
471 |     )
472 | 
473 | 
474 | def test_pep604_union_type_inline_equivalence():
475 |     sig1 = Signature("input: str | None -> output: int | str")
476 |     sig2 = Signature("input: Optional[str] -> output: Union[int, str]")
477 | 
478 |     # PEP 604 union types in inline signatures should be equivalent to Optional and Union types
479 |     assert sig1.equals(sig2)
480 | 
481 |     # Check that the annotations are equivalent
482 |     assert sig1.input_fields["input"].annotation == sig2.input_fields["input"].annotation
483 |     assert sig1.output_fields["output"].annotation == sig2.output_fields["output"].annotation
484 | 
485 | 
486 | def test_pep604_union_type_inline_nested():
487 |     sig = Signature(
488 |         "input: str | (int | float) | None -> output: str"
489 |     )
490 |     assert "input" in sig.input_fields
491 |     input_annotation = sig.input_fields["input"].annotation
492 | 
493 |     # Check for the correct union: Union[str, int, float, NoneType]
494 |     assert getattr(input_annotation, "__origin__", None) is Union
495 |     assert set(input_annotation.__args__) == {str, int, float, type(None)}
496 | 
497 | 
498 | def test_pep604_union_type_class_nested():
499 |     class Sig1(Signature):
500 |         input: str | (int | float) | None = InputField()
501 |         output: str = OutputField()
502 | 
503 |     assert "input" in Sig1.input_fields
504 |     input_annotation = Sig1.input_fields["input"].annotation
505 | 
506 |     # Check for the correct union: UnionType[str, int, float, NoneType]
507 |     assert isinstance(input_annotation, UnionType)
508 |     assert set(input_annotation.__args__) == {str, int, float, type(None)}
509 | 
510 | 
511 | def test_pep604_union_type_class_equivalence():
512 |     class Sig1(Signature):
513 |         input: str | None = InputField()
514 |         output: int | str = OutputField()
515 | 
516 |     class Sig2(Signature):
517 |         input: str | None = InputField()
518 |         output: Union[int, str] = OutputField()  # noqa: UP007
519 | 
520 |     # PEP 604 union types in class signatures should be equivalent to Optional and Union types
521 |     assert Sig1.equals(Sig2)
522 | 
523 |     # Check that the annotations are equivalent
524 |     assert Sig1.input_fields["input"].annotation == Sig2.input_fields["input"].annotation
525 |     assert Sig1.output_fields["output"].annotation == Sig2.output_fields["output"].annotation
526 | 
527 |     # Check that the pep604 annotations are of type UnionType
528 |     assert isinstance(Sig1.input_fields["input"].annotation, UnionType)
529 |     assert isinstance(Sig1.output_fields["output"].annotation, UnionType)
530 | 
531 | 
532 | def test_pep604_union_type_insert():
533 |     class PEP604Signature(Signature):
534 |         input: str | None = InputField()
535 |         output: int | str = OutputField()
536 | 
537 |     # This test ensures that inserting a field into a signature with a PEP 604 UnionType works
538 | 
539 |     # Insert a new input field at the start
540 |     NewSig = PEP604Signature.prepend("new_input", InputField(), float | int)
541 |     assert "new_input" in NewSig.input_fields
542 | 
543 |     new_input_annotation = NewSig.input_fields["new_input"].annotation
544 |     assert isinstance(new_input_annotation, UnionType)
545 |     assert set(new_input_annotation.__args__) == {float, int}
546 | 
547 |     # The original union type field should still be present and correct
548 |     input_annotation = NewSig.input_fields["input"].annotation
549 |     output_annotation = NewSig.output_fields["output"].annotation
550 | 
551 |     assert isinstance(input_annotation, UnionType)
552 |     assert str in input_annotation.__args__ and type(None) in input_annotation.__args__
553 | 
554 |     assert isinstance(output_annotation, UnionType)
555 |     assert set(output_annotation.__args__) == {int, str}
556 | 
557 | 
558 | def test_pep604_union_type_with_custom_types():
559 |     class CustomType(pydantic.BaseModel):
560 |         value: str
561 | 
562 |     sig = Signature(
563 |         "input: CustomType | None -> output: int | str",
564 |         custom_types={"CustomType": CustomType}
565 |     )
566 | 
567 |     assert sig.input_fields["input"].annotation == Union[CustomType, None]
568 |     assert sig.output_fields["output"].annotation == Union[int, str]
569 | 
570 |     lm = DummyLM([{"output": "processed"}])
571 |     dspy.settings.configure(lm=lm)
572 | 
573 |     custom_obj = CustomType(value="test")
574 |     pred = dspy.Predict(sig)(input=custom_obj)
575 |     assert pred.output == "processed"
576 | 
```

--------------------------------------------------------------------------------
/tests/adapters/test_chat_adapter.py:
--------------------------------------------------------------------------------

```python
  1 | from typing import Literal
  2 | from unittest import mock
  3 | 
  4 | import pydantic
  5 | import pytest
  6 | from litellm.utils import ChatCompletionMessageToolCall, Choices, Function, Message, ModelResponse
  7 | 
  8 | import dspy
  9 | 
 10 | 
 11 | @pytest.mark.parametrize(
 12 |     "input_literal, output_literal, input_value, expected_input_str, expected_output_str",
 13 |     [
 14 |         # Scenario 1: double quotes escaped within strings
 15 |         (
 16 |             Literal["one", "two", 'three"'],
 17 |             Literal["four", "five", 'six"'],
 18 |             "two",
 19 |             "Literal['one', 'two', 'three\"']",
 20 |             "Literal['four', 'five', 'six\"']",
 21 |         ),
 22 |         # Scenario 2: Single quotes inside strings
 23 |         (
 24 |             Literal["she's here", "okay", "test"],
 25 |             Literal["done", "maybe'soon", "later"],
 26 |             "she's here",
 27 |             "Literal[\"she's here\", 'okay', 'test']",
 28 |             "Literal['done', \"maybe'soon\", 'later']",
 29 |         ),
 30 |         # Scenario 3: Strings containing both single and double quotes
 31 |         (
 32 |             Literal["both\"and'", "another"],
 33 |             Literal["yet\"another'", "plain"],
 34 |             "another",
 35 |             "Literal['both\"and\\'', 'another']",
 36 |             "Literal['yet\"another\\'', 'plain']",
 37 |         ),
 38 |         # Scenario 4: No quotes at all (check the default)
 39 |         (
 40 |             Literal["foo", "bar"],
 41 |             Literal["baz", "qux"],
 42 |             "foo",
 43 |             "Literal['foo', 'bar']",
 44 |             "Literal['baz', 'qux']",
 45 |         ),
 46 |         # Scenario 5: Mixed types
 47 |         (
 48 |             Literal[1, "bar"],
 49 |             Literal[True, 3, "foo"],
 50 |             "bar",
 51 |             "Literal[1, 'bar']",
 52 |             "Literal[True, 3, 'foo']",
 53 |         ),
 54 |     ],
 55 | )
 56 | def test_chat_adapter_quotes_literals_as_expected(
 57 |     input_literal, output_literal, input_value, expected_input_str, expected_output_str
 58 | ):
 59 |     """
 60 |     This test verifies that when we declare Literal fields with various mixes
 61 |     of single/double quotes, the generated content string includes those
 62 |     Literals exactly as we want them to appear (like IPython does).
 63 |     """
 64 | 
 65 |     class TestSignature(dspy.Signature):
 66 |         input_text: input_literal = dspy.InputField()
 67 |         output_text: output_literal = dspy.OutputField()
 68 | 
 69 |     program = dspy.Predict(TestSignature)
 70 | 
 71 |     dspy.configure(lm=dspy.LM(model="openai/gpt-4o"), adapter=dspy.ChatAdapter())
 72 | 
 73 |     with mock.patch("litellm.completion") as mock_completion:
 74 |         program(input_text=input_value)
 75 | 
 76 |     mock_completion.assert_called_once()
 77 |     _, call_kwargs = mock_completion.call_args
 78 |     content = call_kwargs["messages"][0]["content"]
 79 | 
 80 |     assert expected_input_str in content
 81 |     assert expected_output_str in content
 82 | 
 83 | 
 84 | def test_chat_adapter_sync_call():
 85 |     signature = dspy.make_signature("question->answer")
 86 |     adapter = dspy.ChatAdapter()
 87 |     lm = dspy.utils.DummyLM([{"answer": "Paris"}])
 88 |     result = adapter(lm, {}, signature, [], {"question": "What is the capital of France?"})
 89 |     assert result == [{"answer": "Paris"}]
 90 | 
 91 | 
 92 | @pytest.mark.asyncio
 93 | async def test_chat_adapter_async_call():
 94 |     signature = dspy.make_signature("question->answer")
 95 |     adapter = dspy.ChatAdapter()
 96 |     lm = dspy.utils.DummyLM([{"answer": "Paris"}])
 97 |     result = await adapter.acall(lm, {}, signature, [], {"question": "What is the capital of France?"})
 98 |     assert result == [{"answer": "Paris"}]
 99 | 
100 | 
101 | def test_chat_adapter_with_pydantic_models():
102 |     """
103 |     This test verifies that ChatAdapter can handle different input and output field types, both basic and nested.
104 |     """
105 | 
106 |     class DogClass(pydantic.BaseModel):
107 |         dog_breeds: list[str] = pydantic.Field(description="List of the breeds of dogs")
108 |         num_dogs: int = pydantic.Field(description="Number of dogs the owner has", ge=0, le=10)
109 | 
110 |     class PetOwner(pydantic.BaseModel):
111 |         name: str = pydantic.Field(description="Name of the owner")
112 |         num_pets: int = pydantic.Field(description="Amount of pets the owner has", ge=0, le=100)
113 |         dogs: DogClass = pydantic.Field(description="Nested Pydantic class with dog specific information ")
114 | 
115 |     class Answer(pydantic.BaseModel):
116 |         result: str
117 |         analysis: str
118 | 
119 |     class TestSignature(dspy.Signature):
120 |         owner: PetOwner = dspy.InputField()
121 |         question: str = dspy.InputField()
122 |         output: Answer = dspy.OutputField()
123 | 
124 |     dspy.configure(lm=dspy.LM(model="openai/gpt-4o"), adapter=dspy.ChatAdapter())
125 |     program = dspy.Predict(TestSignature)
126 | 
127 |     with mock.patch("litellm.completion") as mock_completion:
128 |         program(
129 |             owner=PetOwner(name="John", num_pets=5, dogs=DogClass(dog_breeds=["labrador", "chihuahua"], num_dogs=2)),
130 |             question="How many non-dog pets does John have?",
131 |         )
132 | 
133 |     mock_completion.assert_called_once()
134 |     _, call_kwargs = mock_completion.call_args
135 | 
136 |     system_content = call_kwargs["messages"][0]["content"]
137 |     user_content = call_kwargs["messages"][1]["content"]
138 |     assert "1. `owner` (PetOwner)" in system_content
139 |     assert "2. `question` (str)" in system_content
140 |     assert "1. `output` (Answer)" in system_content
141 | 
142 |     assert "name" in user_content
143 |     assert "num_pets" in user_content
144 |     assert "dogs" in user_content
145 |     assert "dog_breeds" in user_content
146 |     assert "num_dogs" in user_content
147 |     assert "How many non-dog pets does John have?" in user_content
148 | 
149 | 
150 | def test_chat_adapter_signature_information():
151 |     """
152 |     This test ensures that the signature information sent to the LM follows an expected format.
153 |     """
154 | 
155 |     class TestSignature(dspy.Signature):
156 |         input1: str = dspy.InputField(desc="String Input")
157 |         input2: int = dspy.InputField(desc="Integer Input")
158 |         output: str = dspy.OutputField(desc="String Output")
159 | 
160 |     dspy.configure(lm=dspy.LM(model="openai/gpt-4o"), adapter=dspy.ChatAdapter())
161 |     program = dspy.Predict(TestSignature)
162 | 
163 |     with mock.patch("litellm.completion") as mock_completion:
164 |         program(input1="Test", input2=11)
165 | 
166 |     mock_completion.assert_called_once()
167 |     _, call_kwargs = mock_completion.call_args
168 | 
169 |     assert len(call_kwargs["messages"]) == 2
170 |     assert call_kwargs["messages"][0]["role"] == "system"
171 |     assert call_kwargs["messages"][1]["role"] == "user"
172 | 
173 |     system_content = call_kwargs["messages"][0]["content"]
174 |     user_content = call_kwargs["messages"][1]["content"]
175 | 
176 |     assert "1. `input1` (str)" in system_content
177 |     assert "2. `input2` (int)" in system_content
178 |     assert "1. `output` (str)" in system_content
179 |     assert "[[ ## input1 ## ]]\n{input1}" in system_content
180 |     assert "[[ ## input2 ## ]]\n{input2}" in system_content
181 |     assert "[[ ## output ## ]]\n{output}" in system_content
182 |     assert "[[ ## completed ## ]]" in system_content
183 | 
184 |     assert "[[ ## input1 ## ]]" in user_content
185 |     assert "[[ ## input2 ## ]]" in user_content
186 |     assert "[[ ## output ## ]]" in user_content
187 |     assert "[[ ## completed ## ]]" in user_content
188 | 
189 | 
190 | def test_chat_adapter_exception_raised_on_failure():
191 |     """
192 |     This test ensures that on an error, ChatAdapter raises an explicit exception.
193 |     """
194 |     signature = dspy.make_signature("question->answer")
195 |     adapter = dspy.ChatAdapter()
196 |     invalid_completion = "{'output':'mismatched value'}"
197 |     with pytest.raises(dspy.utils.exceptions.AdapterParseError, match="Adapter ChatAdapter failed to parse*"):
198 |         adapter.parse(signature, invalid_completion)
199 | 
200 | 
201 | def test_chat_adapter_formats_image():
202 |     # Test basic image formatting
203 |     image = dspy.Image(url="https://example.com/image.jpg")
204 | 
205 |     class MySignature(dspy.Signature):
206 |         image: dspy.Image = dspy.InputField()
207 |         text: str = dspy.OutputField()
208 | 
209 |     adapter = dspy.ChatAdapter()
210 |     messages = adapter.format(MySignature, [], {"image": image})
211 | 
212 |     assert len(messages) == 2
213 |     user_message_content = messages[1]["content"]
214 |     assert user_message_content is not None
215 | 
216 |     # The message should have 3 chunks of types: text, image_url, text
217 |     assert len(user_message_content) == 3
218 |     assert user_message_content[0]["type"] == "text"
219 |     assert user_message_content[2]["type"] == "text"
220 | 
221 |     # Assert that the image is formatted correctly
222 |     expected_image_content = {"type": "image_url", "image_url": {"url": "https://example.com/image.jpg"}}
223 |     assert expected_image_content in user_message_content
224 | 
225 | 
226 | def test_chat_adapter_formats_image_with_few_shot_examples():
227 |     class MySignature(dspy.Signature):
228 |         image: dspy.Image = dspy.InputField()
229 |         text: str = dspy.OutputField()
230 | 
231 |     adapter = dspy.ChatAdapter()
232 | 
233 |     demos = [
234 |         dspy.Example(
235 |             image=dspy.Image(url="https://example.com/image1.jpg"),
236 |             text="This is a test image",
237 |         ),
238 |         dspy.Example(
239 |             image=dspy.Image(url="https://example.com/image2.jpg"),
240 |             text="This is another test image",
241 |         ),
242 |     ]
243 |     messages = adapter.format(MySignature, demos, {"image": dspy.Image(url="https://example.com/image3.jpg")})
244 | 
245 |     # 1 system message, 2 few shot examples (1 user and assistant message for each example), 1 user message
246 |     assert len(messages) == 6
247 | 
248 |     assert "[[ ## completed ## ]]\n" in messages[2]["content"]
249 |     assert "[[ ## completed ## ]]\n" in messages[4]["content"]
250 | 
251 |     assert {"type": "image_url", "image_url": {"url": "https://example.com/image1.jpg"}} in messages[1]["content"]
252 |     assert {"type": "image_url", "image_url": {"url": "https://example.com/image2.jpg"}} in messages[3]["content"]
253 |     assert {"type": "image_url", "image_url": {"url": "https://example.com/image3.jpg"}} in messages[5]["content"]
254 | 
255 | 
256 | def test_chat_adapter_formats_image_with_nested_images():
257 |     class ImageWrapper(pydantic.BaseModel):
258 |         images: list[dspy.Image]
259 |         tag: list[str]
260 | 
261 |     class MySignature(dspy.Signature):
262 |         image: ImageWrapper = dspy.InputField()
263 |         text: str = dspy.OutputField()
264 | 
265 |     image1 = dspy.Image(url="https://example.com/image1.jpg")
266 |     image2 = dspy.Image(url="https://example.com/image2.jpg")
267 |     image3 = dspy.Image(url="https://example.com/image3.jpg")
268 | 
269 |     image_wrapper = ImageWrapper(images=[image1, image2, image3], tag=["test", "example"])
270 | 
271 |     adapter = dspy.ChatAdapter()
272 |     messages = adapter.format(MySignature, [], {"image": image_wrapper})
273 | 
274 |     expected_image1_content = {"type": "image_url", "image_url": {"url": "https://example.com/image1.jpg"}}
275 |     expected_image2_content = {"type": "image_url", "image_url": {"url": "https://example.com/image2.jpg"}}
276 |     expected_image3_content = {"type": "image_url", "image_url": {"url": "https://example.com/image3.jpg"}}
277 | 
278 |     assert expected_image1_content in messages[1]["content"]
279 |     assert expected_image2_content in messages[1]["content"]
280 |     assert expected_image3_content in messages[1]["content"]
281 | 
282 | 
283 | def test_chat_adapter_formats_image_with_few_shot_examples_with_nested_images():
284 |     class ImageWrapper(pydantic.BaseModel):
285 |         images: list[dspy.Image]
286 |         tag: list[str]
287 | 
288 |     class MySignature(dspy.Signature):
289 |         image: ImageWrapper = dspy.InputField()
290 |         text: str = dspy.OutputField()
291 | 
292 |     image1 = dspy.Image(url="https://example.com/image1.jpg")
293 |     image2 = dspy.Image(url="https://example.com/image2.jpg")
294 |     image3 = dspy.Image(url="https://example.com/image3.jpg")
295 | 
296 |     image_wrapper = ImageWrapper(images=[image1, image2, image3], tag=["test", "example"])
297 |     demos = [
298 |         dspy.Example(
299 |             image=image_wrapper,
300 |             text="This is a test image",
301 |         ),
302 |     ]
303 | 
304 |     image_wrapper_2 = ImageWrapper(images=[dspy.Image(url="https://example.com/image4.jpg")], tag=["test", "example"])
305 |     adapter = dspy.ChatAdapter()
306 |     messages = adapter.format(MySignature, demos, {"image": image_wrapper_2})
307 | 
308 |     assert len(messages) == 4
309 | 
310 |     # Image information in the few-shot example's user message
311 |     expected_image1_content = {"type": "image_url", "image_url": {"url": "https://example.com/image1.jpg"}}
312 |     expected_image2_content = {"type": "image_url", "image_url": {"url": "https://example.com/image2.jpg"}}
313 |     expected_image3_content = {"type": "image_url", "image_url": {"url": "https://example.com/image3.jpg"}}
314 |     assert expected_image1_content in messages[1]["content"]
315 |     assert expected_image2_content in messages[1]["content"]
316 |     assert expected_image3_content in messages[1]["content"]
317 | 
318 |     # The query image is formatted in the last user message
319 |     assert {"type": "image_url", "image_url": {"url": "https://example.com/image4.jpg"}} in messages[-1]["content"]
320 | 
321 | 
322 | def test_chat_adapter_with_tool():
323 |     class MySignature(dspy.Signature):
324 |         """Answer question with the help of the tools"""
325 | 
326 |         question: str = dspy.InputField()
327 |         tools: list[dspy.Tool] = dspy.InputField()
328 |         answer: str = dspy.OutputField()
329 |         tool_calls: dspy.ToolCalls = dspy.OutputField()
330 | 
331 |     def get_weather(city: str) -> str:
332 |         """Get the weather for a city"""
333 |         return f"The weather in {city} is sunny"
334 | 
335 |     def get_population(country: str, year: int) -> str:
336 |         """Get the population for a country"""
337 |         return f"The population of {country} in {year} is 1000000"
338 | 
339 |     tools = [dspy.Tool(get_weather), dspy.Tool(get_population)]
340 | 
341 |     adapter = dspy.ChatAdapter()
342 |     messages = adapter.format(MySignature, [], {"question": "What is the weather in Tokyo?", "tools": tools})
343 | 
344 |     assert len(messages) == 2
345 | 
346 |     # The output field type description should be included in the system message even if the output field is nested
347 |     assert dspy.ToolCalls.description() in messages[0]["content"]
348 | 
349 |     # The user message should include the question and the tools
350 |     assert "What is the weather in Tokyo?" in messages[1]["content"]
351 |     assert "get_weather" in messages[1]["content"]
352 |     assert "get_population" in messages[1]["content"]
353 | 
354 |     # Tool arguments format should be included in the user message
355 |     assert "{'city': {'type': 'string'}}" in messages[1]["content"]
356 |     assert "{'country': {'type': 'string'}, 'year': {'type': 'integer'}}" in messages[1]["content"]
357 | 
358 | 
359 | def test_chat_adapter_with_code():
360 |     # Test with code as input field
361 |     class CodeAnalysis(dspy.Signature):
362 |         """Analyze the time complexity of the code"""
363 | 
364 |         code: dspy.Code = dspy.InputField()
365 |         result: str = dspy.OutputField()
366 | 
367 |     adapter = dspy.ChatAdapter()
368 |     messages = adapter.format(CodeAnalysis, [], {"code": "print('Hello, world!')"})
369 | 
370 |     assert len(messages) == 2
371 | 
372 |     # The output field type description should be included in the system message even if the output field is nested
373 |     assert dspy.Code.description() in messages[0]["content"]
374 | 
375 |     # The user message should include the question and the tools
376 |     assert "print('Hello, world!')" in messages[1]["content"]
377 | 
378 |     # Test with code as output field
379 |     class CodeGeneration(dspy.Signature):
380 |         """Generate code to answer the question"""
381 | 
382 |         question: str = dspy.InputField()
383 |         code: dspy.Code = dspy.OutputField()
384 | 
385 |     adapter = dspy.ChatAdapter()
386 |     with mock.patch("litellm.completion") as mock_completion:
387 |         mock_completion.return_value = ModelResponse(
388 |             choices=[Choices(message=Message(content='[[ ## code ## ]]\nprint("Hello, world!")'))],
389 |             model="openai/gpt-4o-mini",
390 |         )
391 |         result = adapter(
392 |             dspy.LM(model="openai/gpt-4o-mini", cache=False),
393 |             {},
394 |             CodeGeneration,
395 |             [],
396 |             {"question": "Write a python program to print 'Hello, world!'"},
397 |         )
398 |         assert result[0]["code"].code == 'print("Hello, world!")'
399 | 
400 | 
401 | def test_chat_adapter_formats_conversation_history():
402 |     class MySignature(dspy.Signature):
403 |         question: str = dspy.InputField()
404 |         history: dspy.History = dspy.InputField()
405 |         answer: str = dspy.OutputField()
406 | 
407 |     history = dspy.History(
408 |         messages=[
409 |             {"question": "What is the capital of France?", "answer": "Paris"},
410 |             {"question": "What is the capital of Germany?", "answer": "Berlin"},
411 |         ]
412 |     )
413 | 
414 |     adapter = dspy.ChatAdapter()
415 |     messages = adapter.format(MySignature, [], {"question": "What is the capital of France?", "history": history})
416 | 
417 |     assert len(messages) == 6
418 |     assert messages[1]["content"] == "[[ ## question ## ]]\nWhat is the capital of France?"
419 |     assert messages[2]["content"] == "[[ ## answer ## ]]\nParis\n\n[[ ## completed ## ]]\n"
420 |     assert messages[3]["content"] == "[[ ## question ## ]]\nWhat is the capital of Germany?"
421 |     assert messages[4]["content"] == "[[ ## answer ## ]]\nBerlin\n\n[[ ## completed ## ]]\n"
422 | 
423 | 
424 | def test_chat_adapter_fallback_to_json_adapter_on_exception():
425 |     signature = dspy.make_signature("question->answer")
426 |     adapter = dspy.ChatAdapter()
427 | 
428 |     with mock.patch("litellm.completion") as mock_completion:
429 |         # Mock returning a response compatible with JSONAdapter but not ChatAdapter
430 |         mock_completion.return_value = ModelResponse(
431 |             choices=[Choices(message=Message(content="{'answer': 'Paris'}"))],
432 |             model="openai/gpt-4o-mini",
433 |         )
434 | 
435 |         lm = dspy.LM("openai/gpt-4o-mini", cache=False)
436 | 
437 |         with mock.patch("dspy.adapters.json_adapter.JSONAdapter.__call__") as mock_json_adapter_call:
438 |             adapter(lm, {}, signature, [], {"question": "What is the capital of France?"})
439 |             mock_json_adapter_call.assert_called_once()
440 | 
441 |         # The parse should succeed
442 |         result = adapter(lm, {}, signature, [], {"question": "What is the capital of France?"})
443 |         assert result == [{"answer": "Paris"}]
444 | 
445 | 
446 | @pytest.mark.asyncio
447 | async def test_chat_adapter_fallback_to_json_adapter_on_exception_async():
448 |     signature = dspy.make_signature("question->answer")
449 |     adapter = dspy.ChatAdapter()
450 | 
451 |     with mock.patch("litellm.acompletion") as mock_completion:
452 |         # Mock returning a response compatible with JSONAdapter but not ChatAdapter
453 |         mock_completion.return_value = ModelResponse(
454 |             choices=[Choices(message=Message(content="{'answer': 'Paris'}"))],
455 |             model="openai/gpt-4o-mini",
456 |         )
457 | 
458 |         lm = dspy.LM("openai/gpt-4o-mini", cache=False)
459 | 
460 |         with mock.patch("dspy.adapters.json_adapter.JSONAdapter.acall") as mock_json_adapter_acall:
461 |             await adapter.acall(lm, {}, signature, [], {"question": "What is the capital of France?"})
462 |             mock_json_adapter_acall.assert_called_once()
463 | 
464 |         # The parse should succeed
465 |         result = await adapter.acall(lm, {}, signature, [], {"question": "What is the capital of France?"})
466 |         assert result == [{"answer": "Paris"}]
467 | 
468 | 
469 | def test_chat_adapter_toolcalls_native_function_calling():
470 |     class MySignature(dspy.Signature):
471 |         question: str = dspy.InputField()
472 |         tools: list[dspy.Tool] = dspy.InputField()
473 |         answer: str = dspy.OutputField()
474 |         tool_calls: dspy.ToolCalls = dspy.OutputField()
475 | 
476 |     def get_weather(city: str) -> str:
477 |         return f"The weather in {city} is sunny"
478 | 
479 |     tools = [dspy.Tool(get_weather)]
480 | 
481 |     adapter = dspy.JSONAdapter(use_native_function_calling=True)
482 | 
483 |     # Case 1: Tool calls are present in the response, while content is None.
484 |     with mock.patch("litellm.completion") as mock_completion:
485 |         mock_completion.return_value = ModelResponse(
486 |             choices=[
487 |                 Choices(
488 |                     finish_reason="tool_calls",
489 |                     index=0,
490 |                     message=Message(
491 |                         content=None,
492 |                         role="assistant",
493 |                         tool_calls=[
494 |                             ChatCompletionMessageToolCall(
495 |                                 function=Function(arguments='{"city":"Paris"}', name="get_weather"),
496 |                                 id="call_pQm8ajtSMxgA0nrzK2ivFmxG",
497 |                                 type="function",
498 |                             )
499 |                         ],
500 |                     ),
501 |                 ),
502 |             ],
503 |             model="openai/gpt-4o-mini",
504 |         )
505 |         result = adapter(
506 |             dspy.LM(model="openai/gpt-4o-mini", cache=False),
507 |             {},
508 |             MySignature,
509 |             [],
510 |             {"question": "What is the weather in Paris?", "tools": tools},
511 |         )
512 | 
513 |         assert result[0]["tool_calls"] == dspy.ToolCalls(
514 |             tool_calls=[dspy.ToolCalls.ToolCall(name="get_weather", args={"city": "Paris"})]
515 |         )
516 |         # `answer` is not present, so we set it to None
517 |         assert result[0]["answer"] is None
518 | 
519 |     # Case 2: Tool calls are not present in the response, while content is present.
520 |     with mock.patch("litellm.completion") as mock_completion:
521 |         mock_completion.return_value = ModelResponse(
522 |             choices=[Choices(message=Message(content="{'answer': 'Paris'}"))],
523 |             model="openai/gpt-4o-mini",
524 |         )
525 |         result = adapter(
526 |             dspy.LM(model="openai/gpt-4o-mini", cache=False),
527 |             {},
528 |             MySignature,
529 |             [],
530 |             {"question": "What is the weather in Paris?", "tools": tools},
531 |         )
532 |         assert result[0]["answer"] == "Paris"
533 |         assert result[0]["tool_calls"] is None
534 | 
535 | 
536 | def test_chat_adapter_toolcalls_vague_match():
537 |     class MySignature(dspy.Signature):
538 |         question: str = dspy.InputField()
539 |         tools: list[dspy.Tool] = dspy.InputField()
540 |         tool_calls: dspy.ToolCalls = dspy.OutputField()
541 | 
542 |     def get_weather(city: str) -> str:
543 |         return f"The weather in {city} is sunny"
544 | 
545 |     tools = [dspy.Tool(get_weather)]
546 | 
547 |     adapter = dspy.ChatAdapter()
548 | 
549 |     with mock.patch("litellm.completion") as mock_completion:
550 |         # Case 1: tool_calls field is a list of dicts
551 |         mock_completion.return_value = ModelResponse(
552 |             choices=[
553 |                 Choices(
554 |                     message=Message(
555 |                         content="[[ ## tool_calls ## ]]\n[{'name': 'get_weather', 'args': {'city': 'Paris'}]"
556 |                     )
557 |                 )
558 |             ],
559 |             model="openai/gpt-4o-mini",
560 |         )
561 |         result = adapter(
562 |             dspy.LM(model="openai/gpt-4o-mini", cache=False),
563 |             {},
564 |             MySignature,
565 |             [],
566 |             {"question": "What is the weather in Paris?", "tools": tools},
567 |         )
568 |         assert result[0]["tool_calls"] == dspy.ToolCalls(
569 |             tool_calls=[dspy.ToolCalls.ToolCall(name="get_weather", args={"city": "Paris"})]
570 |         )
571 | 
572 |     with mock.patch("litellm.completion") as mock_completion:
573 |         # Case 2: tool_calls field is a single dict with "name" and "args" keys
574 |         mock_completion.return_value = ModelResponse(
575 |             choices=[
576 |                 Choices(
577 |                     message=Message(
578 |                         content="[[ ## tool_calls ## ]]\n{'name': 'get_weather', 'args': {'city': 'Paris'}}"
579 |                     )
580 |                 )
581 |             ],
582 |             model="openai/gpt-4o-mini",
583 |         )
584 |         result = adapter(
585 |             dspy.LM(model="openai/gpt-4o-mini", cache=False),
586 |             {},
587 |             MySignature,
588 |             [],
589 |             {"question": "What is the weather in Paris?", "tools": tools},
590 |         )
591 |         assert result[0]["tool_calls"] == dspy.ToolCalls(
592 |             tool_calls=[dspy.ToolCalls.ToolCall(name="get_weather", args={"city": "Paris"})]
593 |         )
594 | 
```

--------------------------------------------------------------------------------
/docs/docs/tutorials/ai_text_game/index.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Building a Creative Text-Based AI Game with DSPy
  2 | 
  3 | This tutorial demonstrates how to create an interactive text-based adventure game using DSPy's modular programming approach. You'll build a dynamic game where AI handles narrative generation, character interactions, and adaptive gameplay.
  4 | 
  5 | ## What You'll Build
  6 | 
  7 | An intelligent text-based adventure game featuring:
  8 | 
  9 | - Dynamic story generation and branching narratives
 10 | - AI-powered character interactions and dialogue
 11 | - Adaptive gameplay that responds to player choices
 12 | - Inventory and character progression systems
 13 | - Save/load game state functionality
 14 | 
 15 | ## Setup
 16 | 
 17 | ```bash
 18 | pip install dspy rich typer
 19 | ```
 20 | 
 21 | ## Step 1: Core Game Framework
 22 | 
 23 | ```python
 24 | import dspy
 25 | import json
 26 | from typing import Dict, List, Optional, Any
 27 | from dataclasses import dataclass, field
 28 | from enum import Enum
 29 | import random
 30 | from rich.console import Console
 31 | from rich.panel import Panel
 32 | from rich.text import Text
 33 | import typer
 34 | 
 35 | # Configure DSPy
 36 | lm = dspy.LM(model='openai/gpt-4o-mini')
 37 | dspy.configure(lm=lm)
 38 | 
 39 | console = Console()
 40 | 
 41 | class GameState(Enum):
 42 |     MENU = "menu"
 43 |     PLAYING = "playing"
 44 |     INVENTORY = "inventory"
 45 |     CHARACTER = "character"
 46 |     GAME_OVER = "game_over"
 47 | 
 48 | @dataclass
 49 | class Player:
 50 |     name: str
 51 |     health: int = 100
 52 |     level: int = 1
 53 |     experience: int = 0
 54 |     inventory: list[str] = field(default_factory=list)
 55 |     skills: dict[str, int] = field(default_factory=lambda: {
 56 |         "strength": 10,
 57 |         "intelligence": 10,
 58 |         "charisma": 10,
 59 |         "stealth": 10
 60 |     })
 61 |     
 62 |     def add_item(self, item: str):
 63 |         self.inventory.append(item)
 64 |         console.print(f"[green]Added {item} to inventory![/green]")
 65 |     
 66 |     def remove_item(self, item: str) -> bool:
 67 |         if item in self.inventory:
 68 |             self.inventory.remove(item)
 69 |             return True
 70 |         return False
 71 |     
 72 |     def gain_experience(self, amount: int):
 73 |         self.experience += amount
 74 |         old_level = self.level
 75 |         self.level = 1 + (self.experience // 100)
 76 |         if self.level > old_level:
 77 |             console.print(f"[bold yellow]Level up! You are now level {self.level}![/bold yellow]")
 78 | 
 79 | @dataclass
 80 | class GameContext:
 81 |     current_location: str = "Village Square"
 82 |     story_progress: int = 0
 83 |     visited_locations: list[str] = field(default_factory=list)
 84 |     npcs_met: list[str] = field(default_factory=list)
 85 |     completed_quests: list[str] = field(default_factory=list)
 86 |     game_flags: dict[str, bool] = field(default_factory=dict)
 87 |     
 88 |     def add_flag(self, flag: str, value: bool = True):
 89 |         self.game_flags[flag] = value
 90 |     
 91 |     def has_flag(self, flag: str) -> bool:
 92 |         return self.game_flags.get(flag, False)
 93 | 
 94 | class GameEngine:
 95 |     def __init__(self):
 96 |         self.player = None
 97 |         self.context = GameContext()
 98 |         self.state = GameState.MENU
 99 |         self.running = True
100 |         
101 |     def save_game(self, filename: str = "savegame.json"):
102 |         """Save current game state."""
103 |         save_data = {
104 |             "player": {
105 |                 "name": self.player.name,
106 |                 "health": self.player.health,
107 |                 "level": self.player.level,
108 |                 "experience": self.player.experience,
109 |                 "inventory": self.player.inventory,
110 |                 "skills": self.player.skills
111 |             },
112 |             "context": {
113 |                 "current_location": self.context.current_location,
114 |                 "story_progress": self.context.story_progress,
115 |                 "visited_locations": self.context.visited_locations,
116 |                 "npcs_met": self.context.npcs_met,
117 |                 "completed_quests": self.context.completed_quests,
118 |                 "game_flags": self.context.game_flags
119 |             }
120 |         }
121 |         
122 |         with open(filename, 'w') as f:
123 |             json.dump(save_data, f, indent=2)
124 |         console.print(f"[green]Game saved to {filename}![/green]")
125 |     
126 |     def load_game(self, filename: str = "savegame.json") -> bool:
127 |         """Load game state from file."""
128 |         try:
129 |             with open(filename, 'r') as f:
130 |                 save_data = json.load(f)
131 |             
132 |             # Reconstruct player
133 |             player_data = save_data["player"]
134 |             self.player = Player(
135 |                 name=player_data["name"],
136 |                 health=player_data["health"],
137 |                 level=player_data["level"],
138 |                 experience=player_data["experience"],
139 |                 inventory=player_data["inventory"],
140 |                 skills=player_data["skills"]
141 |             )
142 |             
143 |             # Reconstruct context
144 |             context_data = save_data["context"]
145 |             self.context = GameContext(
146 |                 current_location=context_data["current_location"],
147 |                 story_progress=context_data["story_progress"],
148 |                 visited_locations=context_data["visited_locations"],
149 |                 npcs_met=context_data["npcs_met"],
150 |                 completed_quests=context_data["completed_quests"],
151 |                 game_flags=context_data["game_flags"]
152 |             )
153 |             
154 |             console.print(f"[green]Game loaded from {filename}![/green]")
155 |             return True
156 |             
157 |         except FileNotFoundError:
158 |             console.print(f"[red]Save file {filename} not found![/red]")
159 |             return False
160 |         except Exception as e:
161 |             console.print(f"[red]Error loading game: {e}![/red]")
162 |             return False
163 | 
164 | # Initialize game engine
165 | game = GameEngine()
166 | ```
167 | 
168 | ## Step 2: AI-Powered Story Generation
169 | 
170 | ```python
171 | class StoryGenerator(dspy.Signature):
172 |     """Generate dynamic story content based on current game state."""
173 |     location: str = dspy.InputField(desc="Current location")
174 |     player_info: str = dspy.InputField(desc="Player information and stats")
175 |     story_progress: int = dspy.InputField(desc="Current story progress level")
176 |     recent_actions: str = dspy.InputField(desc="Player's recent actions")
177 |     
178 |     scene_description: str = dspy.OutputField(desc="Vivid description of current scene")
179 |     available_actions: list[str] = dspy.OutputField(desc="List of possible player actions")
180 |     npcs_present: list[str] = dspy.OutputField(desc="NPCs present in this location")
181 |     items_available: list[str] = dspy.OutputField(desc="Items that can be found or interacted with")
182 | 
183 | class DialogueGenerator(dspy.Signature):
184 |     """Generate NPC dialogue and responses."""
185 |     npc_name: str = dspy.InputField(desc="Name and type of NPC")
186 |     npc_personality: str = dspy.InputField(desc="NPC personality and background")
187 |     player_input: str = dspy.InputField(desc="What the player said or did")
188 |     context: str = dspy.InputField(desc="Current game context and history")
189 |     
190 |     npc_response: str = dspy.OutputField(desc="NPC's dialogue response")
191 |     mood_change: str = dspy.OutputField(desc="How NPC's mood changed (positive/negative/neutral)")
192 |     quest_offered: bool = dspy.OutputField(desc="Whether NPC offers a quest")
193 |     information_revealed: str = dspy.OutputField(desc="Any important information shared")
194 | 
195 | class ActionResolver(dspy.Signature):
196 |     """Resolve player actions and determine outcomes."""
197 |     action: str = dspy.InputField(desc="Player's chosen action")
198 |     player_stats: str = dspy.InputField(desc="Player's current stats and skills")
199 |     context: str = dspy.InputField(desc="Current game context")
200 |     difficulty: str = dspy.InputField(desc="Difficulty level of the action")
201 |     
202 |     success: bool = dspy.OutputField(desc="Whether the action succeeded")
203 |     outcome_description: str = dspy.OutputField(desc="Description of what happened")
204 |     stat_changes: dict[str, int] = dspy.OutputField(desc="Changes to player stats")
205 |     items_gained: list[str] = dspy.OutputField(desc="Items gained from this action")
206 |     experience_gained: int = dspy.OutputField(desc="Experience points gained")
207 | 
208 | class GameAI(dspy.Module):
209 |     """Main AI module for game logic and narrative."""
210 |     
211 |     def __init__(self):
212 |         super().__init__()
213 |         self.story_gen = dspy.ChainOfThought(StoryGenerator)
214 |         self.dialogue_gen = dspy.ChainOfThought(DialogueGenerator)
215 |         self.action_resolver = dspy.ChainOfThought(ActionResolver)
216 |     
217 |     def generate_scene(self, player: Player, context: GameContext, recent_actions: str = "") -> Dict:
218 |         """Generate current scene description and options."""
219 |         
220 |         player_info = f"Level {player.level} {player.name}, Health: {player.health}, Skills: {player.skills}"
221 |         
222 |         scene = self.story_gen(
223 |             location=context.current_location,
224 |             player_info=player_info,
225 |             story_progress=context.story_progress,
226 |             recent_actions=recent_actions
227 |         )
228 |         
229 |         return {
230 |             "description": scene.scene_description,
231 |             "actions": scene.available_actions,
232 |             "npcs": scene.npcs_present,
233 |             "items": scene.items_available
234 |         }
235 |     
236 |     def handle_dialogue(self, npc_name: str, player_input: str, context: GameContext) -> Dict:
237 |         """Handle conversation with NPCs."""
238 |         
239 |         # Create NPC personality based on name and context
240 |         personality_map = {
241 |             "Village Elder": "Wise, knowledgeable, speaks in riddles, has ancient knowledge",
242 |             "Merchant": "Greedy but fair, loves to bargain, knows about valuable items",
243 |             "Guard": "Dutiful, suspicious of strangers, follows rules strictly",
244 |             "Thief": "Sneaky, untrustworthy, has information about hidden things",
245 |             "Wizard": "Mysterious, powerful, speaks about magic and ancient forces"
246 |         }
247 |         
248 |         personality = personality_map.get(npc_name, "Friendly villager with local knowledge")
249 |         game_context = f"Location: {context.current_location}, Story progress: {context.story_progress}"
250 |         
251 |         response = self.dialogue_gen(
252 |             npc_name=npc_name,
253 |             npc_personality=personality,
254 |             player_input=player_input,
255 |             context=game_context
256 |         )
257 |         
258 |         return {
259 |             "response": response.npc_response,
260 |             "mood": response.mood_change,
261 |             "quest": response.quest_offered,
262 |             "info": response.information_revealed
263 |         }
264 |     
265 |     def resolve_action(self, action: str, player: Player, context: GameContext) -> Dict:
266 |         """Resolve player actions and determine outcomes."""
267 |         
268 |         player_stats = f"Level {player.level}, Health {player.health}, Skills: {player.skills}"
269 |         game_context = f"Location: {context.current_location}, Progress: {context.story_progress}"
270 |         
271 |         # Determine difficulty based on action type
272 |         difficulty = "medium"
273 |         if any(word in action.lower() for word in ["fight", "battle", "attack"]):
274 |             difficulty = "hard"
275 |         elif any(word in action.lower() for word in ["look", "examine", "talk"]):
276 |             difficulty = "easy"
277 |         
278 |         result = self.action_resolver(
279 |             action=action,
280 |             player_stats=player_stats,
281 |             context=game_context,
282 |             difficulty=difficulty
283 |         )
284 |         
285 |         return {
286 |             "success": result.success,
287 |             "description": result.outcome_description,
288 |             "stat_changes": result.stat_changes,
289 |             "items": result.items_gained,
290 |             "experience": result.experience_gained
291 |         }
292 | 
293 | # Initialize AI
294 | ai = GameAI()
295 | ```
296 | 
297 | ## Step 3: Game Interface and Interaction
298 | 
299 | ```python
300 | def display_game_header():
301 |     """Display the game header."""
302 |     header = Text("🏰 MYSTIC REALM ADVENTURE 🏰", style="bold magenta")
303 |     console.print(Panel(header, style="bright_blue"))
304 | 
305 | def display_player_status(player: Player):
306 |     """Display player status panel."""
307 |     status = f"""
308 | [bold]Name:[/bold] {player.name}
309 | [bold]Level:[/bold] {player.level} (XP: {player.experience})
310 | [bold]Health:[/bold] {player.health}/100
311 | [bold]Skills:[/bold]
312 |   • Strength: {player.skills['strength']}
313 |   • Intelligence: {player.skills['intelligence']}
314 |   • Charisma: {player.skills['charisma']}
315 |   • Stealth: {player.skills['stealth']}
316 | [bold]Inventory:[/bold] {len(player.inventory)} items
317 |     """
318 |     console.print(Panel(status.strip(), title="Player Status", style="green"))
319 | 
320 | def display_location(context: GameContext, scene: Dict):
321 |     """Display current location and scene."""
322 |     location_panel = f"""
323 | [bold yellow]{context.current_location}[/bold yellow]
324 | 
325 | {scene['description']}
326 |     """
327 |     
328 |     if scene['npcs']:
329 |         location_panel += f"\n\n[bold]NPCs present:[/bold] {', '.join(scene['npcs'])}"
330 |     
331 |     if scene['items']:
332 |         location_panel += f"\n[bold]Items visible:[/bold] {', '.join(scene['items'])}"
333 |     
334 |     console.print(Panel(location_panel.strip(), title="Current Location", style="cyan"))
335 | 
336 | def display_actions(actions: list[str]):
337 |     """Display available actions."""
338 |     action_text = "\n".join([f"{i+1}. {action}" for i, action in enumerate(actions)])
339 |     console.print(Panel(action_text, title="Available Actions", style="yellow"))
340 | 
341 | def get_player_choice(max_choices: int) -> int:
342 |     """Get player's choice with input validation."""
343 |     while True:
344 |         try:
345 |             choice = typer.prompt("Choose an action (number)")
346 |             choice_num = int(choice)
347 |             if 1 <= choice_num <= max_choices:
348 |                 return choice_num - 1
349 |             else:
350 |                 console.print(f"[red]Please enter a number between 1 and {max_choices}[/red]")
351 |         except ValueError:
352 |             console.print("[red]Please enter a valid number[/red]")
353 | 
354 | def show_inventory(player: Player):
355 |     """Display player inventory."""
356 |     if not player.inventory:
357 |         console.print(Panel("Your inventory is empty.", title="Inventory", style="red"))
358 |     else:
359 |         items = "\n".join([f"• {item}" for item in player.inventory])
360 |         console.print(Panel(items, title="Inventory", style="green"))
361 | 
362 | def main_menu():
363 |     """Display main menu and handle selection."""
364 |     console.clear()
365 |     display_game_header()
366 |     
367 |     menu_options = [
368 |         "1. New Game",
369 |         "2. Load Game", 
370 |         "3. How to Play",
371 |         "4. Exit"
372 |     ]
373 |     
374 |     menu_text = "\n".join(menu_options)
375 |     console.print(Panel(menu_text, title="Main Menu", style="bright_blue"))
376 |     
377 |     choice = typer.prompt("Select an option")
378 |     return choice
379 | 
380 | def show_help():
381 |     """Display help information."""
382 |     help_text = """
383 | [bold]How to Play:[/bold]
384 | 
385 | • This is a text-based adventure game powered by AI
386 | • Make choices by selecting numbered options
387 | • Talk to NPCs to learn about the world and get quests
388 | • Explore different locations to find items and adventures
389 | • Your choices affect the story and character development
390 | • Use 'inventory' to check your items
391 | • Use 'status' to see your character info
392 | • Type 'save' to save your progress
393 | • Type 'quit' to return to main menu
394 | 
395 | [bold]Tips:[/bold]
396 | • Different skills affect your success in various actions
397 | • NPCs remember your previous interactions
398 | • Explore thoroughly - there are hidden secrets!
399 | • Your reputation affects how NPCs treat you
400 |     """
401 |     console.print(Panel(help_text.strip(), title="Game Help", style="blue"))
402 |     typer.prompt("Press Enter to continue")
403 | ```
404 | 
405 | ## Step 4: Main Game Loop
406 | 
407 | ```python
408 | def create_new_character():
409 |     """Create a new player character."""
410 |     console.clear()
411 |     display_game_header()
412 |     
413 |     name = typer.prompt("Enter your character's name")
414 |     
415 |     # Character creation with skill point allocation
416 |     console.print("\n[bold]Character Creation[/bold]")
417 |     console.print("You have 10 extra skill points to distribute among your skills.")
418 |     console.print("Base skills start at 10 each.\n")
419 |     
420 |     skills = {"strength": 10, "intelligence": 10, "charisma": 10, "stealth": 10}
421 |     points_remaining = 10
422 |     
423 |     for skill in skills.keys():
424 |         if points_remaining > 0:
425 |             console.print(f"Points remaining: {points_remaining}")
426 |             while True:
427 |                 try:
428 |                     points = int(typer.prompt(f"Points to add to {skill} (0-{points_remaining})"))
429 |                     if 0 <= points <= points_remaining:
430 |                         skills[skill] += points
431 |                         points_remaining -= points
432 |                         break
433 |                     else:
434 |                         console.print(f"[red]Enter a number between 0 and {points_remaining}[/red]")
435 |                 except ValueError:
436 |                     console.print("[red]Please enter a valid number[/red]")
437 |     
438 |     player = Player(name=name, skills=skills)
439 |     console.print(f"\n[green]Welcome to Mystic Realm, {name}![/green]")
440 |     return player
441 | 
442 | def game_loop():
443 |     """Main game loop."""
444 |     recent_actions = ""
445 |     
446 |     while game.running and game.state == GameState.PLAYING:
447 |         console.clear()
448 |         display_game_header()
449 |         
450 |         # Generate current scene
451 |         scene = ai.generate_scene(game.player, game.context, recent_actions)
452 |         
453 |         # Display game state
454 |         display_player_status(game.player)
455 |         display_location(game.context, scene)
456 |         
457 |         # Add standard actions
458 |         all_actions = scene['actions'] + ["Check inventory", "Character status", "Save game", "Quit to menu"]
459 |         display_actions(all_actions)
460 |         
461 |         # Get player choice
462 |         choice_idx = get_player_choice(len(all_actions))
463 |         chosen_action = all_actions[choice_idx]
464 |         
465 |         # Handle special commands
466 |         if chosen_action == "Check inventory":
467 |             show_inventory(game.player)
468 |             typer.prompt("Press Enter to continue")
469 |             continue
470 |         elif chosen_action == "Character status":
471 |             display_player_status(game.player)
472 |             typer.prompt("Press Enter to continue")
473 |             continue
474 |         elif chosen_action == "Save game":
475 |             game.save_game()
476 |             typer.prompt("Press Enter to continue")
477 |             continue
478 |         elif chosen_action == "Quit to menu":
479 |             game.state = GameState.MENU
480 |             break
481 |         
482 |         # Handle game actions
483 |         if chosen_action in scene['actions']:
484 |             # Check if it's dialogue with an NPC
485 |             npc_target = None
486 |             for npc in scene['npcs']:
487 |                 if npc.lower() in chosen_action.lower():
488 |                     npc_target = npc
489 |                     break
490 |             
491 |             if npc_target:
492 |                 # Handle NPC interaction
493 |                 console.print(f"\n[bold]Talking to {npc_target}...[/bold]")
494 |                 dialogue = ai.handle_dialogue(npc_target, chosen_action, game.context)
495 |                 
496 |                 console.print(f"\n[italic]{npc_target}:[/italic] \"{dialogue['response']}\"")
497 |                 
498 |                 if dialogue['quest']:
499 |                     console.print(f"[yellow]💼 Quest opportunity detected![/yellow]")
500 |                 
501 |                 if dialogue['info']:
502 |                     console.print(f"[blue]ℹ️  {dialogue['info']}[/blue]")
503 |                     
504 |                 # Add NPC to met list
505 |                 if npc_target not in game.context.npcs_met:
506 |                     game.context.npcs_met.append(npc_target)
507 |                 
508 |                 recent_actions = f"Talked to {npc_target}: {chosen_action}"
509 |             else:
510 |                 # Handle general action
511 |                 result = ai.resolve_action(chosen_action, game.player, game.context)
512 |                 
513 |                 console.print(f"\n{result['description']}")
514 |                 
515 |                 # Apply results
516 |                 if result['success']:
517 |                     console.print("[green]✅ Success![/green]")
518 |                     
519 |                     # Apply stat changes
520 |                     for stat, change in result['stat_changes'].items():
521 |                         if stat in game.player.skills:
522 |                             game.player.skills[stat] += change
523 |                             if change > 0:
524 |                                 console.print(f"[green]{stat.title()} increased by {change}![/green]")
525 |                         elif stat == "health":
526 |                             game.player.health = max(0, min(100, game.player.health + change))
527 |                             if change > 0:
528 |                                 console.print(f"[green]Health restored by {change}![/green]")
529 |                             elif change < 0:
530 |                                 console.print(f"[red]Health decreased by {abs(change)}![/red]")
531 |                     
532 |                     # Add items
533 |                     for item in result['items']:
534 |                         game.player.add_item(item)
535 |                     
536 |                     # Give experience
537 |                     if result['experience'] > 0:
538 |                         game.player.gain_experience(result['experience'])
539 |                     
540 |                     # Update story progress
541 |                     game.context.story_progress += 1
542 |                 else:
543 |                     console.print("[red]❌ The action didn't go as planned...[/red]")
544 |                 
545 |                 recent_actions = f"Attempted: {chosen_action}"
546 |             
547 |             # Check for game over conditions
548 |             if game.player.health <= 0:
549 |                 console.print("\n[bold red]💀 You have died! Game Over![/bold red]")
550 |                 game.state = GameState.GAME_OVER
551 |                 break
552 |             
553 |             typer.prompt("\nPress Enter to continue")
554 | 
555 | def main():
556 |     """Main game function."""
557 |     while game.running:
558 |         if game.state == GameState.MENU:
559 |             choice = main_menu()
560 |             
561 |             if choice == "1":
562 |                 game.player = create_new_character()
563 |                 game.context = GameContext()
564 |                 game.state = GameState.PLAYING
565 |                 console.print("\n[italic]Your adventure begins...[/italic]")
566 |                 typer.prompt("Press Enter to start")
567 |                 
568 |             elif choice == "2":
569 |                 if game.load_game():
570 |                     game.state = GameState.PLAYING
571 |                 typer.prompt("Press Enter to continue")
572 |                 
573 |             elif choice == "3":
574 |                 show_help()
575 |                 
576 |             elif choice == "4":
577 |                 game.running = False
578 |                 console.print("[bold]Thanks for playing! Goodbye![/bold]")
579 |             
580 |         elif game.state == GameState.PLAYING:
581 |             game_loop()
582 |             
583 |         elif game.state == GameState.GAME_OVER:
584 |             console.print("\n[bold]Game Over[/bold]")
585 |             restart = typer.confirm("Would you like to return to the main menu?")
586 |             if restart:
587 |                 game.state = GameState.MENU
588 |             else:
589 |                 game.running = False
590 | 
591 | if __name__ == "__main__":
592 |     main()
593 | ```
594 | 
595 | ## Example Gameplay
596 | 
597 | When you run the game, you'll experience:
598 | 
599 | **Character Creation:**
600 | ```
601 | 🏰 MYSTIC REALM ADVENTURE 🏰
602 | 
603 | Enter your character's name: Aria
604 | 
605 | Character Creation
606 | You have 10 extra skill points to distribute among your skills.
607 | Base skills start at 10 each.
608 | 
609 | Points remaining: 10
610 | Points to add to strength (0-10): 2
611 | Points to add to intelligence (0-8): 4
612 | Points to add to charisma (0-4): 3
613 | Points to add to stealth (0-1): 1
614 | 
615 | Welcome to Mystic Realm, Aria!
616 | ```
617 | 
618 | **Dynamic Scene Generation:**
619 | ```
620 | ┌──────────── Current Location ────────────┐
621 | │ Village Square                           │
622 | │                                          │
623 | │ You stand in the bustling heart of       │
624 | │ Willowbrook Village. The ancient stone   │
625 | │ fountain bubbles cheerfully as merchants │
626 | │ hawk their wares and children play. A    │
627 | │ mysterious hooded figure lurks near the  │
628 | │ shadows of the old oak tree.             │
629 | │                                          │
630 | │ NPCs present: Village Elder, Merchant    │
631 | │ Items visible: Strange Medallion, Herbs  │
632 | └──────────────────────────────────────────┘
633 | 
634 | ┌────────── Available Actions ─────────────┐
635 | │ 1. Approach the hooded figure            │
636 | │ 2. Talk to the Village Elder             │
637 | │ 3. Browse the merchant's wares           │
638 | │ 4. Examine the strange medallion         │
639 | │ 5. Gather herbs near the fountain        │
640 | │ 6. Head to the forest path               │
641 | └───────────────────────────────────────────┘
642 | ```
643 | 
644 | **AI-Generated Dialogue:**
645 | ```
646 | Talking to Village Elder...
647 | 
648 | Village Elder: "Ah, young traveler, I sense a great destiny 
649 | surrounds you like morning mist. The ancient prophecy speaks 
650 | of one who would come bearing the mark of courage. Tell me, 
651 | have you noticed anything... unusual in your travels?"
652 | 
653 | 💼 Quest opportunity detected!
654 | ℹ️ The Village Elder knows about an ancient prophecy that might involve you
655 | ```
656 | 
657 | ## Next Steps
658 | 
659 | - **Combat System**: Add turn-based battles with strategy
660 | - **Magic System**: Spellcasting with resource management
661 | - **Multiplayer**: Network support for cooperative adventures
662 | - **Quest System**: Complex multi-step missions with branching outcomes
663 | - **World Building**: Procedurally generated locations and characters
664 | - **Audio**: Add sound effects and background music
665 | 
666 | This tutorial demonstrates how DSPy's modular approach enables complex, interactive systems where AI handles creative content generation while maintaining consistent game logic and player agency.
667 | 
```

--------------------------------------------------------------------------------
/docs/docs/index.md:
--------------------------------------------------------------------------------

```markdown
  1 | ---
  2 | sidebar_position: 1
  3 | hide:
  4 |   - navigation
  5 |   - toc
  6 | 
  7 | ---
  8 | 
  9 | ![DSPy](static/img/dspy_logo.png){ width="200", align=left }
 10 | 
 11 | # _Programming_—not prompting—_LMs_
 12 | 
 13 | [![PyPI Downloads](https://static.pepy.tech/badge/dspy/month)](https://pepy.tech/projects/dspy)
 14 | 
 15 | DSPy is a declarative framework for building modular AI software. It allows you to **iterate fast on structured code**, rather than brittle strings, and offers algorithms that **compile AI programs into effective prompts and weights** for your language models, whether you're building simple classifiers, sophisticated RAG pipelines, or Agent loops.
 16 | 
 17 | Instead of wrangling prompts or training jobs, DSPy (Declarative Self-improving Python) enables you to **build AI software from natural-language modules** and to _generically compose them_ with different models, inference strategies, or learning algorithms. This makes AI software **more reliable, maintainable, and portable** across models and strategies.
 18 | 
 19 | *tl;dr* Think of DSPy as a higher-level language for AI programming ([lecture](https://www.youtube.com/watch?v=JEMYuzrKLUw)), like the shift from assembly to C or pointer arithmetic to SQL. Meet the community, seek help, or start contributing via [GitHub](https://github.com/stanfordnlp/dspy) and [Discord](https://discord.gg/XCGy2WDCQB).
 20 | 
 21 | <!-- Its abstractions make your AI software more reliable and maintainable, and allow it to become more portable as new models and learning techniques emerge. It's also just rather elegant! -->
 22 | 
 23 | !!! info "Getting Started I: Install DSPy and set up your LM"
 24 | 
 25 |     ```bash
 26 |     > pip install -U dspy
 27 |     ```
 28 | 
 29 |     === "OpenAI"
 30 |         You can authenticate by setting the `OPENAI_API_KEY` env variable or passing `api_key` below.
 31 | 
 32 |         ```python linenums="1"
 33 |         import dspy
 34 |         lm = dspy.LM("openai/gpt-4o-mini", api_key="YOUR_OPENAI_API_KEY")
 35 |         dspy.configure(lm=lm)
 36 |         ```
 37 | 
 38 |     === "Anthropic"
 39 |         You can authenticate by setting the `ANTHROPIC_API_KEY` env variable or passing `api_key` below.
 40 | 
 41 |         ```python linenums="1"
 42 |         import dspy
 43 |         lm = dspy.LM("anthropic/claude-3-opus-20240229", api_key="YOUR_ANTHROPIC_API_KEY")
 44 |         dspy.configure(lm=lm)
 45 |         ```
 46 | 
 47 |     === "Databricks"
 48 |         If you're on the Databricks platform, authentication is automatic via their SDK. If not, you can set the env variables `DATABRICKS_API_KEY` and `DATABRICKS_API_BASE`, or pass `api_key` and `api_base` below.
 49 | 
 50 |         ```python linenums="1"
 51 |         import dspy
 52 |         lm = dspy.LM(
 53 |             "databricks/databricks-llama-4-maverick",
 54 |             api_key="YOUR_DATABRICKS_ACCESS_TOKEN",
 55 |             api_base="YOUR_DATABRICKS_WORKSPACE_URL",  # e.g.: https://dbc-64bf4923-e39e.cloud.databricks.com/serving-endpoints
 56 |         )
 57 |         dspy.configure(lm=lm)
 58 |         ```
 59 | 
 60 |     === "Gemini"
 61 |         You can authenticate by setting the `GEMINI_API_KEY` env variable or passing `api_key` below.
 62 | 
 63 |         ```python linenums="1"
 64 |         import dspy
 65 |         lm = dspy.LM("gemini/gemini-2.5-flash", api_key="YOUR_GEMINI_API_KEY")
 66 |         dspy.configure(lm=lm)
 67 |         ```
 68 | 
 69 |     === "Local LMs on your laptop"
 70 |           First, install [Ollama](https://github.com/ollama/ollama) and launch its server with your LM.
 71 | 
 72 |           ```bash
 73 |           > curl -fsSL https://ollama.ai/install.sh | sh
 74 |           > ollama run llama3.2:1b
 75 |           ```
 76 | 
 77 |           Then, connect to it from your DSPy code.
 78 | 
 79 |         ```python linenums="1"
 80 |         import dspy
 81 |         lm = dspy.LM("ollama_chat/llama3.2:1b", api_base="http://localhost:11434", api_key="")
 82 |         dspy.configure(lm=lm)
 83 |         ```
 84 | 
 85 |     === "Local LMs on a GPU server"
 86 |           First, install [SGLang](https://docs.sglang.ai/get_started/install.html) and launch its server with your LM.
 87 | 
 88 |           ```bash
 89 |           > pip install "sglang[all]"
 90 |           > pip install flashinfer -i https://flashinfer.ai/whl/cu121/torch2.4/ 
 91 | 
 92 |           > CUDA_VISIBLE_DEVICES=0 python -m sglang.launch_server --port 7501 --model-path meta-llama/Llama-3.1-8B-Instruct
 93 |           ```
 94 |         
 95 |         If you don't have access from Meta to download `meta-llama/Llama-3.1-8B-Instruct`, use `Qwen/Qwen2.5-7B-Instruct` for example.
 96 | 
 97 |         Next, connect to your local LM from your DSPy code as an `OpenAI`-compatible endpoint.
 98 | 
 99 |           ```python linenums="1"
100 |           lm = dspy.LM("openai/meta-llama/Llama-3.1-8B-Instruct",
101 |                        api_base="http://localhost:7501/v1",  # ensure this points to your port
102 |                        api_key="local", model_type="chat")
103 |           dspy.configure(lm=lm)
104 |           ```
105 | 
106 |     === "Other providers"
107 |         In DSPy, you can use any of the dozens of [LLM providers supported by LiteLLM](https://docs.litellm.ai/docs/providers). Simply follow their instructions for which `{PROVIDER}_API_KEY` to set and how to write pass the `{provider_name}/{model_name}` to the constructor.
108 | 
109 |         Some examples:
110 | 
111 |         - `anyscale/mistralai/Mistral-7B-Instruct-v0.1`, with `ANYSCALE_API_KEY`
112 |         - `together_ai/togethercomputer/llama-2-70b-chat`, with `TOGETHERAI_API_KEY`
113 |         - `sagemaker/<your-endpoint-name>`, with `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, and `AWS_REGION_NAME`
114 |         - `azure/<your_deployment_name>`, with `AZURE_API_KEY`, `AZURE_API_BASE`, `AZURE_API_VERSION`, and the optional `AZURE_AD_TOKEN` and `AZURE_API_TYPE`
115 | 
116 |         
117 |         If your provider offers an OpenAI-compatible endpoint, just add an `openai/` prefix to your full model name.
118 | 
119 |         ```python linenums="1"
120 |         import dspy
121 |         lm = dspy.LM("openai/your-model-name", api_key="PROVIDER_API_KEY", api_base="YOUR_PROVIDER_URL")
122 |         dspy.configure(lm=lm)
123 |         ```
124 | 
125 | ??? "Calling the LM directly."
126 | 
127 |      Idiomatic DSPy involves using _modules_, which we define in the rest of this page. However, it's still easy to call the `lm` you configured above directly. This gives you a unified API and lets you benefit from utilities like automatic caching.
128 | 
129 |      ```python linenums="1"       
130 |      lm("Say this is a test!", temperature=0.7)  # => ['This is a test!']
131 |      lm(messages=[{"role": "user", "content": "Say this is a test!"}])  # => ['This is a test!']
132 |      ``` 
133 | 
134 | 
135 | ## 1) **Modules** help you describe AI behavior as _code_, not strings.
136 | 
137 | To build reliable AI systems, you must iterate fast. But maintaining prompts makes that hard: it forces you to tinker with strings or data _every time you change your LM, metrics, or pipeline_. Having built over a dozen best-in-class compound LM systems since 2020, we learned this the hard way—and so built DSPy to decouple AI system design from messy incidental choices about specific LMs or prompting strategies.
138 | 
139 | DSPy shifts your focus from tinkering with prompt strings to **programming with structured and declarative natural-language modules**. For every AI component in your system, you specify input/output behavior as a _signature_ and select a _module_ to assign a strategy for invoking your LM. DSPy expands your signatures into prompts and parses your typed outputs, so you can compose different modules together into ergonomic, portable, and optimizable AI systems.
140 | 
141 | 
142 | !!! info "Getting Started II: Build DSPy modules for various tasks"
143 |     Try the examples below after configuring your `lm` above. Adjust the fields to explore what tasks your LM can do well out of the box. Each tab below sets up a DSPy module, like `dspy.Predict`, `dspy.ChainOfThought`, or `dspy.ReAct`, with a task-specific _signature_. For example, `question -> answer: float` tells the module to take a question and to produce a `float` answer.
144 | 
145 |     === "Math"
146 | 
147 |         ```python linenums="1"
148 |         math = dspy.ChainOfThought("question -> answer: float")
149 |         math(question="Two dice are tossed. What is the probability that the sum equals two?")
150 |         ```
151 |         
152 |         **Possible Output:**
153 |         ```text
154 |         Prediction(
155 |             reasoning='When two dice are tossed, each die has 6 faces, resulting in a total of 6 x 6 = 36 possible outcomes. The sum of the numbers on the two dice equals two only when both dice show a 1. This is just one specific outcome: (1, 1). Therefore, there is only 1 favorable outcome. The probability of the sum being two is the number of favorable outcomes divided by the total number of possible outcomes, which is 1/36.',
156 |             answer=0.0277776
157 |         )
158 |         ```
159 | 
160 |     === "RAG"
161 | 
162 |         ```python linenums="1"       
163 |         def search_wikipedia(query: str) -> list[str]:
164 |             results = dspy.ColBERTv2(url="http://20.102.90.50:2017/wiki17_abstracts")(query, k=3)
165 |             return [x["text"] for x in results]
166 |         
167 |         rag = dspy.ChainOfThought("context, question -> response")
168 | 
169 |         question = "What's the name of the castle that David Gregory inherited?"
170 |         rag(context=search_wikipedia(question), question=question)
171 |         ```
172 |         
173 |         **Possible Output:**
174 |         ```text
175 |         Prediction(
176 |             reasoning='The context provides information about David Gregory, a Scottish physician and inventor. It specifically mentions that he inherited Kinnairdy Castle in 1664. This detail directly answers the question about the name of the castle that David Gregory inherited.',
177 |             response='Kinnairdy Castle'
178 |         )
179 |         ```
180 | 
181 |     === "Classification"
182 | 
183 |         ```python linenums="1"
184 |         from typing import Literal
185 | 
186 |         class Classify(dspy.Signature):
187 |             """Classify sentiment of a given sentence."""
188 |             
189 |             sentence: str = dspy.InputField()
190 |             sentiment: Literal["positive", "negative", "neutral"] = dspy.OutputField()
191 |             confidence: float = dspy.OutputField()
192 | 
193 |         classify = dspy.Predict(Classify)
194 |         classify(sentence="This book was super fun to read, though not the last chapter.")
195 |         ```
196 |         
197 |         **Possible Output:**
198 | 
199 |         ```text
200 |         Prediction(
201 |             sentiment='positive',
202 |             confidence=0.75
203 |         )
204 |         ```
205 | 
206 |     === "Information Extraction"
207 | 
208 |         ```python linenums="1"        
209 |         class ExtractInfo(dspy.Signature):
210 |             """Extract structured information from text."""
211 |             
212 |             text: str = dspy.InputField()
213 |             title: str = dspy.OutputField()
214 |             headings: list[str] = dspy.OutputField()
215 |             entities: list[dict[str, str]] = dspy.OutputField(desc="a list of entities and their metadata")
216 |         
217 |         module = dspy.Predict(ExtractInfo)
218 | 
219 |         text = "Apple Inc. announced its latest iPhone 14 today." \
220 |             "The CEO, Tim Cook, highlighted its new features in a press release."
221 |         response = module(text=text)
222 | 
223 |         print(response.title)
224 |         print(response.headings)
225 |         print(response.entities)
226 |         ```
227 |         
228 |         **Possible Output:**
229 |         ```text
230 |         Apple Inc. Announces iPhone 14
231 |         ['Introduction', "CEO's Statement", 'New Features']
232 |         [{'name': 'Apple Inc.', 'type': 'Organization'}, {'name': 'iPhone 14', 'type': 'Product'}, {'name': 'Tim Cook', 'type': 'Person'}]
233 |         ```
234 | 
235 |     === "Agents"
236 | 
237 |         ```python linenums="1"       
238 |         def evaluate_math(expression: str):
239 |             return dspy.PythonInterpreter({}).execute(expression)
240 | 
241 |         def search_wikipedia(query: str):
242 |             results = dspy.ColBERTv2(url="http://20.102.90.50:2017/wiki17_abstracts")(query, k=3)
243 |             return [x["text"] for x in results]
244 | 
245 |         react = dspy.ReAct("question -> answer: float", tools=[evaluate_math, search_wikipedia])
246 | 
247 |         pred = react(question="What is 9362158 divided by the year of birth of David Gregory of Kinnairdy castle?")
248 |         print(pred.answer)
249 |         ```
250 |         
251 |         **Possible Output:**
252 | 
253 |         ```text
254 |         5761.328
255 |         ```
256 |     
257 |     === "Multi-Stage Pipelines"
258 | 
259 |         ```python linenums="1"       
260 |         class Outline(dspy.Signature):
261 |             """Outline a thorough overview of a topic."""
262 |             
263 |             topic: str = dspy.InputField()
264 |             title: str = dspy.OutputField()
265 |             sections: list[str] = dspy.OutputField()
266 |             section_subheadings: dict[str, list[str]] = dspy.OutputField(desc="mapping from section headings to subheadings")
267 | 
268 |         class DraftSection(dspy.Signature):
269 |             """Draft a top-level section of an article."""
270 |             
271 |             topic: str = dspy.InputField()
272 |             section_heading: str = dspy.InputField()
273 |             section_subheadings: list[str] = dspy.InputField()
274 |             content: str = dspy.OutputField(desc="markdown-formatted section")
275 | 
276 |         class DraftArticle(dspy.Module):
277 |             def __init__(self):
278 |                 self.build_outline = dspy.ChainOfThought(Outline)
279 |                 self.draft_section = dspy.ChainOfThought(DraftSection)
280 | 
281 |             def forward(self, topic):
282 |                 outline = self.build_outline(topic=topic)
283 |                 sections = []
284 |                 for heading, subheadings in outline.section_subheadings.items():
285 |                     section, subheadings = f"## {heading}", [f"### {subheading}" for subheading in subheadings]
286 |                     section = self.draft_section(topic=outline.title, section_heading=section, section_subheadings=subheadings)
287 |                     sections.append(section.content)
288 |                 return dspy.Prediction(title=outline.title, sections=sections)
289 | 
290 |         draft_article = DraftArticle()
291 |         article = draft_article(topic="World Cup 2002")
292 |         ```
293 |         
294 |         **Possible Output:**
295 | 
296 |         A 1500-word article on the topic, e.g.
297 | 
298 |         ```text
299 |         ## Qualification Process
300 | 
301 |         The qualification process for the 2002 FIFA World Cup involved a series of..... [shortened here for presentation].
302 | 
303 |         ### UEFA Qualifiers
304 | 
305 |         The UEFA qualifiers involved 50 teams competing for 13..... [shortened here for presentation].
306 | 
307 |         .... [rest of the article]
308 |         ```
309 | 
310 |         Note that DSPy makes it straightforward to optimize multi-stage modules like this. As long as you can evaluate the _final_ output of the system, every DSPy optimizer can tune all of the intermediate modules.
311 | 
312 | ??? "Using DSPy in practice: from quick scripting to building sophisticated systems."
313 | 
314 |     Standard prompts conflate interface ("what should the LM do?") with implementation ("how do we tell it to do that?"). DSPy isolates the former as _signatures_ so we can infer the latter or learn it from data — in the context of a bigger program.
315 |     
316 |     Even before you start using optimizers, DSPy's modules allow you to script effective LM systems as ergonomic, portable _code_. Across many tasks and LMs, we maintain _signature test suites_ that assess the reliability of the built-in DSPy adapters. Adapters are the components that map signatures to prompts prior to optimization. If you find a task where a simple prompt consistently outperforms idiomatic DSPy for your LM, consider that a bug and [file an issue](https://github.com/stanfordnlp/dspy/issues). We'll use this to improve the built-in adapters.
317 | 
318 | 
319 | ## 2) **Optimizers** tune the prompts and weights of your AI modules.
320 | 
321 | DSPy provides you with the tools to compile high-level code with natural language annotations into the low-level computations, prompts, or weight updates that align your LM with your program's structure and metrics. If you change your code or your metrics, you can simply re-compile accordingly.
322 | 
323 | Given a few tens or hundreds of representative _inputs_ of your task and a _metric_ that can measure the quality of your system's outputs, you can use a DSPy optimizer. Different optimizers in DSPy work by **synthesizing good few-shot examples** for every module, like `dspy.BootstrapRS`,<sup>[1](https://arxiv.org/abs/2310.03714)</sup> **proposing and intelligently exploring better natural-language instructions** for every prompt, like [`dspy.GEPA`](https://dspy.ai/tutorials/gepa_ai_program/)<sup>[2](https://arxiv.org/abs/2507.19457)</sup>, `dspy.MIPROv2`,<sup>[3](https://arxiv.org/abs/2406.11695)</sup> and **building datasets for your modules and using them to finetune the LM weights** in your system, like `dspy.BootstrapFinetune`.<sup>[4](https://arxiv.org/abs/2407.10930)</sup> For detailed tutorials on running `dspy.GEPA`, please take a look at [dspy.GEPA tutorials](https://dspy.ai/tutorials/gepa_ai_program/).
324 | 
325 | 
326 | !!! info "Getting Started III: Optimizing the LM prompts or weights in DSPy programs"
327 |     A typical simple optimization run costs on the order of $2 USD and takes around 20 minutes, but be careful when running optimizers with very large LMs or very large datasets.
328 |     Optimization can cost as little as a few cents or up to tens of dollars, depending on your LM, dataset, and configuration.
329 | 
330 |     Examples below rely on HuggingFace/datasets, you can install it by the command below.
331 | 
332 |     ```bash
333 |     > pip install -U datasets
334 |     ```
335 | 
336 |     === "Optimizing prompts for a ReAct agent"
337 |         This is a minimal but fully runnable example of setting up a `dspy.ReAct` agent that answers questions via
338 |         search from Wikipedia and then optimizing it using `dspy.MIPROv2` in the cheap `light` mode on 500
339 |         question-answer pairs sampled from the `HotPotQA` dataset.
340 | 
341 |         ```python linenums="1"
342 |         import dspy
343 |         from dspy.datasets import HotPotQA
344 | 
345 |         dspy.configure(lm=dspy.LM("openai/gpt-4o-mini"))
346 | 
347 |         def search_wikipedia(query: str) -> list[str]:
348 |             results = dspy.ColBERTv2(url="http://20.102.90.50:2017/wiki17_abstracts")(query, k=3)
349 |             return [x["text"] for x in results]
350 | 
351 |         trainset = [x.with_inputs('question') for x in HotPotQA(train_seed=2024, train_size=500).train]
352 |         react = dspy.ReAct("question -> answer", tools=[search_wikipedia])
353 | 
354 |         tp = dspy.MIPROv2(metric=dspy.evaluate.answer_exact_match, auto="light", num_threads=24)
355 |         optimized_react = tp.compile(react, trainset=trainset)
356 |         ```
357 | 
358 |         An informal run like this raises ReAct's score from 24% to 51%, by teaching `gpt-4o-mini` more about the specifics of the task.
359 | 
360 |     === "Optimizing prompts for RAG"
361 |         Given a retrieval index to `search`, your favorite `dspy.LM`, and a small `trainset` of questions and ground-truth responses, the following code snippet can optimize your RAG system with long outputs against the built-in `SemanticF1` metric, which is implemented as a DSPy module.
362 | 
363 |         ```python linenums="1"
364 |         class RAG(dspy.Module):
365 |             def __init__(self, num_docs=5):
366 |                 self.num_docs = num_docs
367 |                 self.respond = dspy.ChainOfThought("context, question -> response")
368 | 
369 |             def forward(self, question):
370 |                 context = search(question, k=self.num_docs)   # defined in tutorial linked below
371 |                 return self.respond(context=context, question=question)
372 | 
373 |         tp = dspy.MIPROv2(metric=dspy.evaluate.SemanticF1(decompositional=True), auto="medium", num_threads=24)
374 |         optimized_rag = tp.compile(RAG(), trainset=trainset, max_bootstrapped_demos=2, max_labeled_demos=2)
375 |         ```
376 | 
377 |         For a complete RAG example that you can run, start this [tutorial](tutorials/rag/index.ipynb). It improves the quality of a RAG system over a subset of StackExchange communities by 10% relative gain.
378 | 
379 |     === "Optimizing weights for Classification"
380 |         <details><summary>Click to show dataset setup code.</summary>
381 | 
382 |         ```python linenums="1"
383 |         import random
384 |         from typing import Literal
385 | 
386 |         from datasets import load_dataset
387 | 
388 |         import dspy
389 |         from dspy.datasets import DataLoader
390 | 
391 |         # Load the Banking77 dataset.
392 |         CLASSES = load_dataset("PolyAI/banking77", split="train", trust_remote_code=True).features["label"].names
393 |         kwargs = {"fields": ("text", "label"), "input_keys": ("text",), "split": "train", "trust_remote_code": True}
394 | 
395 |         # Load the first 2000 examples from the dataset, and assign a hint to each *training* example.
396 |         trainset = [
397 |             dspy.Example(x, hint=CLASSES[x.label], label=CLASSES[x.label]).with_inputs("text", "hint")
398 |             for x in DataLoader().from_huggingface(dataset_name="PolyAI/banking77", **kwargs)[:2000]
399 |         ]
400 |         random.Random(0).shuffle(trainset)
401 |         ```
402 |         </details>
403 | 
404 |         ```python linenums="1"
405 |         import dspy
406 |         lm=dspy.LM('openai/gpt-4o-mini-2024-07-18')
407 | 
408 |         # Define the DSPy module for classification. It will use the hint at training time, if available.
409 |         signature = dspy.Signature("text, hint -> label").with_updated_fields("label", type_=Literal[tuple(CLASSES)])
410 |         classify = dspy.ChainOfThought(signature)
411 |         classify.set_lm(lm)
412 | 
413 |         # Optimize via BootstrapFinetune.
414 |         optimizer = dspy.BootstrapFinetune(metric=(lambda x, y, trace=None: x.label == y.label), num_threads=24)
415 |         optimized = optimizer.compile(classify, trainset=trainset)
416 | 
417 |         optimized(text="What does a pending cash withdrawal mean?")
418 |         
419 |         # For a complete fine-tuning tutorial, see: https://dspy.ai/tutorials/classification_finetuning/
420 |         ```
421 | 
422 |         **Possible Output (from the last line):**
423 |         ```text
424 |         Prediction(
425 |             reasoning='A pending cash withdrawal indicates that a request to withdraw cash has been initiated but has not yet been completed or processed. This status means that the transaction is still in progress and the funds have not yet been deducted from the account or made available to the user.',
426 |             label='pending_cash_withdrawal'
427 |         )
428 |         ```
429 | 
430 |         An informal run similar to this on DSPy 2.5.29 raises GPT-4o-mini's score 66% to 87%.
431 | 
432 | 
433 | ??? "What's an example of a DSPy optimizer? How do different optimizers work?"
434 | 
435 |     Take the `dspy.MIPROv2` optimizer as an example. First, MIPRO starts with the **bootstrapping stage**. It takes your program, which may be unoptimized at this point, and runs it many times across different inputs to collect traces of input/output behavior for each one of your modules. It filters these traces to keep only those that appear in trajectories scored highly by your metric. Second, MIPRO enters its **grounded proposal stage**. It previews your DSPy program's code, your data, and traces from running your program, and uses them to draft many potential instructions for every prompt in your program. Third, MIPRO launches the **discrete search stage**. It samples mini-batches from your training set, proposes a combination of instructions and traces to use for constructing every prompt in the pipeline, and evaluates the candidate program on the mini-batch. Using the resulting score, MIPRO updates a surrogate model that helps the proposals get better over time.
436 | 
437 |     One thing that makes DSPy optimizers so powerful is that they can be composed. You can run `dspy.MIPROv2` and use the produced program as an input to `dspy.MIPROv2` again or, say, to `dspy.BootstrapFinetune` to get better results. This is partly the essence of `dspy.BetterTogether`. Alternatively, you can run the optimizer and then extract the top-5 candidate programs and build a `dspy.Ensemble` of them. This allows you to scale _inference-time compute_ (e.g., ensembles) as well as DSPy's unique _pre-inference time compute_ (i.e., optimization budget) in highly systematic ways.
438 | 
439 | 
440 | 
441 | <!-- Future:
442 | BootstrapRS or MIPRO on ??? with a local SGLang LM
443 | BootstrapFS on MATH with a tiny LM like Llama-3.2 with Ollama (maybe with a big teacher) -->
444 | 
445 | 
446 | 
447 | ## 3) **DSPy's Ecosystem** advances open-source AI research.
448 | 
449 | Compared to monolithic LMs, DSPy's modular paradigm enables a large community to improve the compositional architectures, inference-time strategies, and optimizers for LM programs in an open, distributed way. This gives DSPy users more control, helps them iterate much faster, and allows their programs to get better over time by applying the latest optimizers or modules.
450 | 
451 | The DSPy research effort started at Stanford NLP in Feb 2022, building on what we had learned from developing early [compound LM systems](https://bair.berkeley.edu/blog/2024/02/18/compound-ai-systems/) like [ColBERT-QA](https://arxiv.org/abs/2007.00814), [Baleen](https://arxiv.org/abs/2101.00436), and [Hindsight](https://arxiv.org/abs/2110.07752). The first version was released as [DSP](https://arxiv.org/abs/2212.14024) in Dec 2022 and evolved by Oct 2023 into [DSPy](https://arxiv.org/abs/2310.03714). Thanks to [250 contributors](https://github.com/stanfordnlp/dspy/graphs/contributors), DSPy has introduced tens of thousands of people to building and optimizing modular LM programs.
452 | 
453 | Since then, DSPy's community has produced a large body of work on optimizers, like [MIPROv2](https://arxiv.org/abs/2406.11695), [BetterTogether](https://arxiv.org/abs/2407.10930), and [LeReT](https://arxiv.org/abs/2410.23214), on program architectures, like [STORM](https://arxiv.org/abs/2402.14207), [IReRa](https://arxiv.org/abs/2401.12178), and [DSPy Assertions](https://arxiv.org/abs/2312.13382), and on successful applications to new problems, like [PAPILLON](https://arxiv.org/abs/2410.17127), [PATH](https://arxiv.org/abs/2406.11706), [WangLab@MEDIQA](https://arxiv.org/abs/2404.14544), [UMD's Prompting Case Study](https://arxiv.org/abs/2406.06608), and [Haize's Red-Teaming Program](https://blog.haizelabs.com/posts/dspy/), in addition to many open-source projects, production applications, and other [use cases](community/use-cases.md).
454 | 
```

--------------------------------------------------------------------------------
/docs/docs/tutorials/sample_code_generation/index.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Automated Code Generation from Documentation with DSPy
  2 | 
  3 | This tutorial demonstrates how to use DSPy to automatically fetch documentation from URLs and generate working code examples for any library. The system can analyze documentation websites, extract key concepts, and produce tailored code examples.
  4 | 
  5 | ## What You'll Build
  6 | 
  7 | A documentation-powered code generation system that:
  8 | 
  9 | - Fetches and parses documentation from multiple URLs
 10 | - Extracts API patterns, methods, and usage examples  
 11 | - Generates working code for specific use cases
 12 | - Provides explanations and best practices
 13 | - Works with any library's documentation
 14 | 
 15 | ## Setup
 16 | 
 17 | ```bash
 18 | pip install dspy requests beautifulsoup4 html2text
 19 | ```
 20 | 
 21 | ## Step 1: Documentation Fetching and Processing
 22 | 
 23 | ```python
 24 | import dspy
 25 | import requests
 26 | from bs4 import BeautifulSoup
 27 | import html2text
 28 | from typing import List, Dict, Any
 29 | import json
 30 | from urllib.parse import urljoin, urlparse
 31 | import time
 32 | 
 33 | # Configure DSPy
 34 | lm = dspy.LM(model='openai/gpt-4o-mini')
 35 | dspy.configure(lm=lm)
 36 | 
 37 | class DocumentationFetcher:
 38 |     """Fetches and processes documentation from URLs."""
 39 |     
 40 |     def __init__(self, max_retries=3, delay=1):
 41 |         self.session = requests.Session()
 42 |         self.session.headers.update({
 43 |             'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
 44 |         })
 45 |         self.max_retries = max_retries
 46 |         self.delay = delay
 47 |         self.html_converter = html2text.HTML2Text()
 48 |         self.html_converter.ignore_links = False
 49 |         self.html_converter.ignore_images = True
 50 |     
 51 |     def fetch_url(self, url: str) -> dict[str, str]:
 52 |         """Fetch content from a single URL."""
 53 |         for attempt in range(self.max_retries):
 54 |             try:
 55 |                 print(f"📡 Fetching: {url} (attempt {attempt + 1})")
 56 |                 response = self.session.get(url, timeout=10)
 57 |                 response.raise_for_status()
 58 |                 
 59 |                 soup = BeautifulSoup(response.content, 'html.parser')
 60 |                 
 61 |                 # Remove script and style elements
 62 |                 for script in soup(["script", "style", "nav", "footer", "header"]):
 63 |                     script.decompose()
 64 |                 
 65 |                 # Convert to markdown for better LLM processing
 66 |                 markdown_content = self.html_converter.handle(str(soup))
 67 |                 
 68 |                 return {
 69 |                     "url": url,
 70 |                     "title": soup.title.string if soup.title else "No title",
 71 |                     "content": markdown_content,
 72 |                     "success": True
 73 |                 }
 74 |                 
 75 |             except Exception as e:
 76 |                 print(f"❌ Error fetching {url}: {e}")
 77 |                 if attempt < self.max_retries - 1:
 78 |                     time.sleep(self.delay)
 79 |                 else:
 80 |                     return {
 81 |                         "url": url,
 82 |                         "title": "Failed to fetch",
 83 |                         "content": f"Error: {str(e)}",
 84 |                         "success": False
 85 |                     }
 86 |         
 87 |         return {"url": url, "title": "Failed", "content": "", "success": False}
 88 |     
 89 |     def fetch_documentation(self, urls: list[str]) -> list[dict[str, str]]:
 90 |         """Fetch documentation from multiple URLs."""
 91 |         results = []
 92 |         
 93 |         for url in urls:
 94 |             result = self.fetch_url(url)
 95 |             results.append(result)
 96 |             time.sleep(self.delay)  # Be respectful to servers
 97 |         
 98 |         return results
 99 | 
100 | class LibraryAnalyzer(dspy.Signature):
101 |     """Analyze library documentation to understand core concepts and patterns."""
102 |     library_name: str = dspy.InputField(desc="Name of the library to analyze")
103 |     documentation_content: str = dspy.InputField(desc="Combined documentation content")
104 |     
105 |     core_concepts: list[str] = dspy.OutputField(desc="Main concepts and components")
106 |     common_patterns: list[str] = dspy.OutputField(desc="Common usage patterns")
107 |     key_methods: list[str] = dspy.OutputField(desc="Important methods and functions")
108 |     installation_info: str = dspy.OutputField(desc="Installation and setup information")
109 |     code_examples: list[str] = dspy.OutputField(desc="Example code snippets found")
110 | 
111 | class CodeGenerator(dspy.Signature):
112 |     """Generate code examples for specific use cases using the target library."""
113 |     library_info: str = dspy.InputField(desc="Library concepts and patterns")
114 |     use_case: str = dspy.InputField(desc="Specific use case to implement")
115 |     requirements: str = dspy.InputField(desc="Additional requirements or constraints")
116 |     
117 |     code_example: str = dspy.OutputField(desc="Complete, working code example")
118 |     explanation: str = dspy.OutputField(desc="Step-by-step explanation of the code")
119 |     best_practices: list[str] = dspy.OutputField(desc="Best practices and tips")
120 |     imports_needed: list[str] = dspy.OutputField(desc="Required imports and dependencies")
121 | 
122 | class DocumentationLearningAgent(dspy.Module):
123 |     """Agent that learns from documentation URLs and generates code examples."""
124 |     
125 |     def __init__(self):
126 |         super().__init__()
127 |         self.fetcher = DocumentationFetcher()
128 |         self.analyze_docs = dspy.ChainOfThought(LibraryAnalyzer)
129 |         self.generate_code = dspy.ChainOfThought(CodeGenerator)
130 |         self.refine_code = dspy.ChainOfThought(
131 |             "code, feedback -> improved_code: str, changes_made: list[str]"
132 |         )
133 |     
134 |     def learn_from_urls(self, library_name: str, doc_urls: list[str]) -> Dict:
135 |         """Learn about a library from its documentation URLs."""
136 |         
137 |         print(f"📚 Learning about {library_name} from {len(doc_urls)} URLs...")
138 |         
139 |         # Fetch all documentation
140 |         docs = self.fetcher.fetch_documentation(doc_urls)
141 |         
142 |         # Combine successful fetches
143 |         combined_content = "\n\n---\n\n".join([
144 |             f"URL: {doc['url']}\nTitle: {doc['title']}\n\n{doc['content']}"
145 |             for doc in docs if doc['success']
146 |         ])
147 |         
148 |         if not combined_content:
149 |             raise ValueError("No documentation could be fetched successfully")
150 |         
151 |         # Analyze combined documentation
152 |         analysis = self.analyze_docs(
153 |             library_name=library_name,
154 |             documentation_content=combined_content
155 |         )
156 |         
157 |         return {
158 |             "library": library_name,
159 |             "source_urls": [doc['url'] for doc in docs if doc['success']],
160 |             "core_concepts": analysis.core_concepts,
161 |             "patterns": analysis.common_patterns,
162 |             "methods": analysis.key_methods,
163 |             "installation": analysis.installation_info,
164 |             "examples": analysis.code_examples,
165 |             "fetched_docs": docs
166 |         }
167 |     
168 |     def generate_example(self, library_info: Dict, use_case: str, requirements: str = "") -> Dict:
169 |         """Generate a code example for a specific use case."""
170 |         
171 |         # Format library information for the generator
172 |         info_text = f"""
173 |         Library: {library_info['library']}
174 |         Core Concepts: {', '.join(library_info['core_concepts'])}
175 |         Common Patterns: {', '.join(library_info['patterns'])}
176 |         Key Methods: {', '.join(library_info['methods'])}
177 |         Installation: {library_info['installation']}
178 |         Example Code Snippets: {'; '.join(library_info['examples'][:3])}  # First 3 examples
179 |         """
180 |         
181 |         code_result = self.generate_code(
182 |             library_info=info_text,
183 |             use_case=use_case,
184 |             requirements=requirements
185 |         )
186 |         
187 |         return {
188 |             "code": code_result.code_example,
189 |             "explanation": code_result.explanation,
190 |             "best_practices": code_result.best_practices,
191 |             "imports": code_result.imports_needed
192 |         }
193 | 
194 | # Initialize the learning agent
195 | agent = DocumentationLearningAgent()
196 | ```
197 | 
198 | ## Step 2: Learning from Documentation URLs
199 | 
200 | ```python
201 | def learn_library_from_urls(library_name: str, documentation_urls: list[str]) -> Dict:
202 |     """Learn about any library from its documentation URLs."""
203 |     
204 |     try:
205 |         library_info = agent.learn_from_urls(library_name, documentation_urls)
206 |         
207 |         print(f"\n🔍 Library Analysis Results for {library_name}:")
208 |         print(f"Sources: {len(library_info['source_urls'])} successful fetches")
209 |         print(f"Core Concepts: {library_info['core_concepts']}")
210 |         print(f"Common Patterns: {library_info['patterns']}")
211 |         print(f"Key Methods: {library_info['methods']}")
212 |         print(f"Installation: {library_info['installation']}")
213 |         print(f"Found {len(library_info['examples'])} code examples")
214 |         
215 |         return library_info
216 |         
217 |     except Exception as e:
218 |         print(f"❌ Error learning library: {e}")
219 |         raise
220 | 
221 | # Example 1: Learn FastAPI from official documentation
222 | fastapi_urls = [
223 |     "https://fastapi.tiangolo.com/",
224 |     "https://fastapi.tiangolo.com/tutorial/first-steps/",
225 |     "https://fastapi.tiangolo.com/tutorial/path-params/",
226 |     "https://fastapi.tiangolo.com/tutorial/query-params/"
227 | ]
228 | 
229 | print("🚀 Learning FastAPI from official documentation...")
230 | fastapi_info = learn_library_from_urls("FastAPI", fastapi_urls)
231 | 
232 | # Example 2: Learn a different library (you can replace with any library)
233 | streamlit_urls = [
234 |     "https://docs.streamlit.io/",
235 |     "https://docs.streamlit.io/get-started",
236 |     "https://docs.streamlit.io/develop/api-reference"
237 | ]
238 | 
239 | print("\n\n📊 Learning Streamlit from official documentation...")
240 | streamlit_info = learn_library_from_urls("Streamlit", streamlit_urls)
241 | ```
242 | 
243 | ## Step 3: Generating Code Examples
244 | 
245 | ```python
246 | def generate_examples_for_library(library_info: Dict, library_name: str):
247 |     """Generate code examples for any library based on its documentation."""
248 |     
249 |     # Define generic use cases that can apply to most libraries
250 |     use_cases = [
251 |         {
252 |             "name": "Basic Setup and Hello World",
253 |             "description": f"Create a minimal working example with {library_name}",
254 |             "requirements": "Include installation, imports, and basic usage"
255 |         },
256 |         {
257 |             "name": "Common Operations",
258 |             "description": f"Demonstrate the most common {library_name} operations",
259 |             "requirements": "Show typical workflow and best practices"
260 |         },
261 |         {
262 |             "name": "Advanced Usage",
263 |             "description": f"Create a more complex example showcasing {library_name} capabilities",
264 |             "requirements": "Include error handling and optimization"
265 |         }
266 |     ]
267 |     
268 |     generated_examples = []
269 |     
270 |     print(f"\n🔧 Generating examples for {library_name}...")
271 |     
272 |     for use_case in use_cases:
273 |         print(f"\n📝 {use_case['name']}")
274 |         print(f"Description: {use_case['description']}")
275 |         
276 |         example = agent.generate_example(
277 |             library_info=library_info,
278 |             use_case=use_case['description'],
279 |             requirements=use_case['requirements']
280 |         )
281 |         
282 |         print("\n💻 Generated Code:")
283 |         print("```python")
284 |         print(example['code'])
285 |         print("```")
286 |         
287 |         print("\n📦 Required Imports:")
288 |         for imp in example['imports']:
289 |             print(f"  • {imp}")
290 |         
291 |         print("\n📝 Explanation:")
292 |         print(example['explanation'])
293 |         
294 |         print("\n✅ Best Practices:")
295 |         for practice in example['best_practices']:
296 |             print(f"  • {practice}")
297 |         
298 |         generated_examples.append({
299 |             "use_case": use_case['name'],
300 |             "code": example['code'],
301 |             "imports": example['imports'],
302 |             "explanation": example['explanation'],
303 |             "best_practices": example['best_practices']
304 |         })
305 |         
306 |         print("-" * 80)
307 |     
308 |     return generated_examples
309 | 
310 | # Generate examples for both libraries
311 | print("🎯 Generating FastAPI Examples:")
312 | fastapi_examples = generate_examples_for_library(fastapi_info, "FastAPI")
313 | 
314 | print("\n\n🎯 Generating Streamlit Examples:")
315 | streamlit_examples = generate_examples_for_library(streamlit_info, "Streamlit")
316 | ```
317 | 
318 | ## Step 4: Interactive Library Learning Function
319 | 
320 | ```python
321 | def learn_any_library(library_name: str, documentation_urls: list[str], use_cases: list[str] = None):
322 |     """Learn any library from its documentation and generate examples."""
323 |     
324 |     if use_cases is None:
325 |         use_cases = [
326 |             "Basic setup and hello world example",
327 |             "Common operations and workflows",
328 |             "Advanced usage with best practices"
329 |         ]
330 |     
331 |     print(f"🚀 Starting automated learning for {library_name}...")
332 |     print(f"Documentation sources: {len(documentation_urls)} URLs")
333 |     
334 |     try:
335 |         # Step 1: Learn from documentation
336 |         library_info = agent.learn_from_urls(library_name, documentation_urls)
337 |         
338 |         # Step 2: Generate examples for each use case
339 |         all_examples = []
340 |         
341 |         for i, use_case in enumerate(use_cases, 1):
342 |             print(f"\n📝 Generating example {i}/{len(use_cases)}: {use_case}")
343 |             
344 |             example = agent.generate_example(
345 |                 library_info=library_info,
346 |                 use_case=use_case,
347 |                 requirements="Include error handling, comments, and follow best practices"
348 |             )
349 |             
350 |             all_examples.append({
351 |                 "use_case": use_case,
352 |                 "code": example['code'],
353 |                 "imports": example['imports'],
354 |                 "explanation": example['explanation'],
355 |                 "best_practices": example['best_practices']
356 |             })
357 |         
358 |         return {
359 |             "library_info": library_info,
360 |             "examples": all_examples
361 |         }
362 |     
363 |     except Exception as e:
364 |         print(f"❌ Error learning {library_name}: {e}")
365 |         return None
366 | 
367 | def interactive_learning_session():
368 |     """Interactive session for learning libraries with user input."""
369 |     
370 |     print("🎯 Welcome to the Interactive Library Learning System!")
371 |     print("This system will help you learn any Python library from its documentation.\n")
372 |     
373 |     learned_libraries = {}
374 |     
375 |     while True:
376 |         print("\n" + "="*60)
377 |         print("🚀 LIBRARY LEARNING SESSION")
378 |         print("="*60)
379 |         
380 |         # Get library name from user
381 |         library_name = input("\n📚 Enter the library name you want to learn (or 'quit' to exit): ").strip()
382 |         
383 |         if library_name.lower() in ['quit', 'exit', 'q']:
384 |             print("\n👋 Thanks for using the Interactive Library Learning System!")
385 |             break
386 |         
387 |         if not library_name:
388 |             print("❌ Please enter a valid library name.")
389 |             continue
390 |         
391 |         # Get documentation URLs
392 |         print(f"\n🔗 Enter documentation URLs for {library_name} (one per line, empty line to finish):")
393 |         urls = []
394 |         while True:
395 |             url = input("  URL: ").strip()
396 |             if not url:
397 |                 break
398 |             if not url.startswith(('http://', 'https://')):
399 |                 print("    ⚠️  Please enter a valid URL starting with http:// or https://")
400 |                 continue
401 |             urls.append(url)
402 |         
403 |         if not urls:
404 |             print("❌ No valid URLs provided. Skipping this library.")
405 |             continue
406 |         
407 |         # Get custom use cases from user
408 |         print(f"\n🎯 Define use cases for {library_name} (optional, press Enter for defaults):")
409 |         print("   Default use cases will be: Basic setup, Common operations, Advanced usage")
410 |         
411 |         user_wants_custom = input("   Do you want to define custom use cases? (y/n): ").strip().lower()
412 |         
413 |         use_cases = None
414 |         if user_wants_custom in ['y', 'yes']:
415 |             print("   Enter your use cases (one per line, empty line to finish):")
416 |             use_cases = []
417 |             while True:
418 |                 use_case = input("     Use case: ").strip()
419 |                 if not use_case:
420 |                     break
421 |                 use_cases.append(use_case)
422 |             
423 |             if not use_cases:
424 |                 print("   No custom use cases provided, using defaults.")
425 |                 use_cases = None
426 |         
427 |         # Learn the library
428 |         print(f"\n🚀 Starting learning process for {library_name}...")
429 |         result = learn_any_library(library_name, urls, use_cases)
430 |         
431 |         if result:
432 |             learned_libraries[library_name] = result
433 |             print(f"\n✅ Successfully learned {library_name}!")
434 |             
435 |             # Show summary
436 |             print(f"\n📊 Learning Summary for {library_name}:")
437 |             print(f"   • Core concepts: {len(result['library_info']['core_concepts'])} identified")
438 |             print(f"   • Common patterns: {len(result['library_info']['patterns'])} found")
439 |             print(f"   • Examples generated: {len(result['examples'])}")
440 |             
441 |             # Ask if user wants to see examples
442 |             show_examples = input(f"\n👀 Do you want to see the generated examples for {library_name}? (y/n): ").strip().lower()
443 |             
444 |             if show_examples in ['y', 'yes']:
445 |                 for i, example in enumerate(result['examples'], 1):
446 |                     print(f"\n{'─'*50}")
447 |                     print(f"📝 Example {i}: {example['use_case']}")
448 |                     print(f"{'─'*50}")
449 |                     
450 |                     print("\n💻 Generated Code:")
451 |                     print("```python")
452 |                     print(example['code'])
453 |                     print("```")
454 |                     
455 |                     print(f"\n📦 Required Imports:")
456 |                     for imp in example['imports']:
457 |                         print(f"  • {imp}")
458 |                     
459 |                     print(f"\n📝 Explanation:")
460 |                     print(example['explanation'])
461 |                     
462 |                     print(f"\n✅ Best Practices:")
463 |                     for practice in example['best_practices']:
464 |                         print(f"  • {practice}")
465 |                     
466 |                     # Ask if user wants to see the next example
467 |                     if i < len(result['examples']):
468 |                         continue_viewing = input(f"\nContinue to next example? (y/n): ").strip().lower()
469 |                         if continue_viewing not in ['y', 'yes']:
470 |                             break
471 |             
472 |             # Offer to save results
473 |             save_results = input(f"\n💾 Save learning results for {library_name} to file? (y/n): ").strip().lower()
474 |             
475 |             if save_results in ['y', 'yes']:
476 |                 filename = input(f"   Enter filename (default: {library_name.lower()}_learning.json): ").strip()
477 |                 if not filename:
478 |                     filename = f"{library_name.lower()}_learning.json"
479 |                 
480 |                 try:
481 |                     import json
482 |                     with open(filename, 'w') as f:
483 |                         json.dump(result, f, indent=2, default=str)
484 |                     print(f"   ✅ Results saved to {filename}")
485 |                 except Exception as e:
486 |                     print(f"   ❌ Error saving file: {e}")
487 |         
488 |         else:
489 |             print(f"❌ Failed to learn {library_name}")
490 |         
491 |         # Ask if user wants to learn another library
492 |         print(f"\n📚 Libraries learned so far: {list(learned_libraries.keys())}")
493 |         continue_learning = input("\n🔄 Do you want to learn another library? (y/n): ").strip().lower()
494 |         
495 |         if continue_learning not in ['y', 'yes']:
496 |             break
497 |     
498 |     # Final summary
499 |     if learned_libraries:
500 |         print(f"\n🎉 Session Summary:")
501 |         print(f"Successfully learned {len(learned_libraries)} libraries:")
502 |         for lib_name, info in learned_libraries.items():
503 |             print(f"  • {lib_name}: {len(info['examples'])} examples generated")
504 |     
505 |     return learned_libraries
506 | 
507 | # Example: Run interactive learning session
508 | if __name__ == "__main__":
509 |     # Run interactive session
510 |     learned_libraries = interactive_learning_session()
511 | ```
512 | 
513 | ## Example Output
514 | 
515 | When you run the interactive learning system, you'll see:
516 | 
517 | **Interactive Session Start:**
518 | ```
519 | 🎯 Welcome to the Interactive Library Learning System!
520 | This system will help you learn any Python library from its documentation.
521 | 
522 | ============================================================
523 | 🚀 LIBRARY LEARNING SESSION
524 | ============================================================
525 | 
526 | 📚 Enter the library name you want to learn (or 'quit' to exit): FastAPI
527 | 
528 | 🔗 Enter documentation URLs for FastAPI (one per line, empty line to finish):
529 |   URL: https://fastapi.tiangolo.com/
530 |   URL: https://fastapi.tiangolo.com/tutorial/first-steps/
531 |   URL: https://fastapi.tiangolo.com/tutorial/path-params/
532 |   URL: 
533 | 
534 | 🎯 Define use cases for FastAPI (optional, press Enter for defaults):
535 |    Default use cases will be: Basic setup, Common operations, Advanced usage
536 |    Do you want to define custom use cases? (y/n): y
537 |    Enter your use cases (one per line, empty line to finish):
538 |      Use case: Create a REST API with authentication
539 |      Use case: Build a file upload endpoint
540 |      Use case: Add database integration with SQLAlchemy
541 |      Use case: 
542 | ```
543 | 
544 | **Documentation Processing:**
545 | ```
546 | 🚀 Starting learning process for FastAPI...
547 | 🚀 Starting automated learning for FastAPI...
548 | Documentation sources: 3 URLs
549 | 📡 Fetching: https://fastapi.tiangolo.com/ (attempt 1)
550 | 📡 Fetching: https://fastapi.tiangolo.com/tutorial/first-steps/ (attempt 1)
551 | 📡 Fetching: https://fastapi.tiangolo.com/tutorial/path-params/ (attempt 1)
552 | 📚 Learning about FastAPI from 3 URLs...
553 | 
554 | 🔍 Library Analysis Results for FastAPI:
555 | Sources: 3 successful fetches
556 | Core Concepts: ['FastAPI app', 'path operations', 'dependencies', 'request/response models']
557 | Common Patterns: ['app = FastAPI()', 'decorator-based routing', 'Pydantic models']
558 | Key Methods: ['FastAPI()', '@app.get()', '@app.post()', 'uvicorn.run()']
559 | Installation: pip install fastapi uvicorn
560 | ```
561 | 
562 | **Code Generation:**
563 | ```
564 | 📝 Generating example 1/3: Create a REST API with authentication
565 | 
566 | ✅ Successfully learned FastAPI!
567 | 
568 | 📊 Learning Summary for FastAPI:
569 |    • Core concepts: 4 identified
570 |    • Common patterns: 3 found
571 |    • Examples generated: 3
572 | 
573 | 👀 Do you want to see the generated examples for FastAPI? (y/n): y
574 | 
575 | ──────────────────────────────────────────────────
576 | 📝 Example 1: Create a REST API with authentication
577 | ──────────────────────────────────────────────────
578 | 
579 | 💻 Generated Code:
580 | from fastapi import FastAPI, Depends, HTTPException, status
581 | from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
582 | import uvicorn
583 | from typing import Dict
584 | import jwt
585 | from datetime import datetime, timedelta
586 | 
587 | app = FastAPI(title="Authenticated API", version="1.0.0")
588 | security = HTTPBearer()
589 | 
590 | # Secret key for JWT (use environment variable in production)
591 | SECRET_KEY = "your-secret-key-here"
592 | ALGORITHM = "HS256"
593 | 
594 | def verify_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
595 |     try:
596 |         payload = jwt.decode(credentials.credentials, SECRET_KEY, algorithms=[ALGORITHM])
597 |         username: str = payload.get("sub")
598 |         if username is None:
599 |             raise HTTPException(status_code=401, detail="Invalid token")
600 |         return username
601 |     except jwt.PyJWTError:
602 |         raise HTTPException(status_code=401, detail="Invalid token")
603 | 
604 | @app.post("/login")
605 | async def login(username: str, password: str) -> dict[str, str]:
606 |     # In production, verify against database
607 |     if username == "admin" and password == "secret":
608 |         token_data = {"sub": username, "exp": datetime.utcnow() + timedelta(hours=24)}
609 |         token = jwt.encode(token_data, SECRET_KEY, algorithm=ALGORITHM)
610 |         return {"access_token": token, "token_type": "bearer"}
611 |     raise HTTPException(status_code=401, detail="Invalid credentials")
612 | 
613 | @app.get("/protected")
614 | async def protected_route(current_user: str = Depends(verify_token)) -> dict[str, str]:
615 |     return {"message": f"Hello {current_user}! This is a protected route."}
616 | 
617 | if __name__ == "__main__":
618 |     uvicorn.run(app, host="0.0.0.0", port=8000)
619 | 
620 | 📦 Required Imports:
621 |   • pip install fastapi uvicorn python-jose[cryptography]
622 |   • from fastapi import FastAPI, Depends, HTTPException, status
623 |   • from fastapi.security import HTTPBearer
624 |   • import jwt
625 | 
626 | 📝 Explanation:
627 | This example creates a FastAPI application with JWT-based authentication. It includes a login endpoint that returns a JWT token and a protected route that requires authentication...
628 | 
629 | ✅ Best Practices:
630 |   • Use environment variables for secret keys
631 |   • Implement proper password hashing in production
632 |   • Add token expiration and refresh logic
633 |   • Include proper error handling
634 | 
635 | Continue to next example? (y/n): n
636 | 
637 | 💾 Save learning results for FastAPI to file? (y/n): y
638 |    Enter filename (default: fastapi_learning.json): 
639 |    ✅ Results saved to fastapi_learning.json
640 | 
641 | 📚 Libraries learned so far: ['FastAPI']
642 | 
643 | 🔄 Do you want to learn another library? (y/n): n
644 | 
645 | 🎉 Session Summary:
646 | Successfully learned 1 libraries:
647 |   • FastAPI: 3 examples generated
648 | ```
649 | 
650 | 
651 | ## Next Steps
652 | 
653 | - **GitHub Integration**: Learn from README files and example repositories
654 | - **Video Tutorial Processing**: Extract information from video documentation
655 | - **Community Examples**: Aggregate examples from Stack Overflow and forums
656 | - **Version Comparison**: Track API changes across library versions
657 | - **Testing Generation**: Automatically create unit tests for generated code
658 | - **Page Crawling**: Automatically crawl documentation pages to actively understand the usage
659 | 
660 | This tutorial demonstrates how DSPy can automate the entire process of learning unfamiliar libraries from their documentation, making it valuable for rapid technology adoption and exploration.
661 | 
```

--------------------------------------------------------------------------------
/dspy/signatures/signature.py:
--------------------------------------------------------------------------------

```python
  1 | """Signature class for DSPy.
  2 | 
  3 | You typically subclass the Signature class, like this:
  4 |     class MySignature(dspy.Signature):
  5 |         input: str = InputField(desc="...")
  6 |         output: int = OutputField(desc="...")
  7 | 
  8 | You can call Signature("input1, input2 -> output1, output2") to create a new signature type.
  9 | You can also include instructions, Signature("input -> output", "This is a test").
 10 | But it's generally better to use the make_signature function.
 11 | 
 12 | If you are not sure if your input is a string representation, (like "input1, input2 -> output1, output2"),
 13 | or a signature, you can use the ensure_signature function.
 14 | 
 15 | For compatibility with the legacy dsp format, you can use the signature_to_template function.
 16 | """
 17 | 
 18 | import ast
 19 | import importlib
 20 | import inspect
 21 | import re
 22 | import sys
 23 | import types
 24 | import typing
 25 | from copy import deepcopy
 26 | from typing import Any
 27 | 
 28 | from pydantic import BaseModel, Field, create_model
 29 | from pydantic.fields import FieldInfo
 30 | 
 31 | from dspy.signatures.field import InputField, OutputField
 32 | 
 33 | 
 34 | def _default_instructions(cls) -> str:
 35 |     inputs_ = ", ".join([f"`{field}`" for field in cls.input_fields])
 36 |     outputs_ = ", ".join([f"`{field}`" for field in cls.output_fields])
 37 |     return f"Given the fields {inputs_}, produce the fields {outputs_}."
 38 | 
 39 | 
 40 | class SignatureMeta(type(BaseModel)):
 41 |     def __call__(cls, *args, **kwargs):
 42 |         if cls is Signature:
 43 |             # We don't create an actual Signature instance, instead, we create a new Signature class.
 44 |             custom_types = kwargs.pop("custom_types", None)
 45 | 
 46 |             if custom_types is None and args and isinstance(args[0], str):
 47 |                 custom_types = cls._detect_custom_types_from_caller(args[0])
 48 | 
 49 |             return make_signature(*args, custom_types=custom_types, **kwargs)
 50 |         return super().__call__(*args, **kwargs)
 51 | 
 52 |     @staticmethod
 53 |     def _detect_custom_types_from_caller(signature_str):
 54 |         """Detect custom types from the caller's frame based on the signature string.
 55 | 
 56 |         Note: This method relies on Python's frame introspection which has some limitations:
 57 |         1. May not work in all Python implementations (e.g., compiled with optimizations)
 58 |         2. Looks up a limited number of frames in the call stack
 59 |         3. Cannot find types that are imported but not in the caller's namespace
 60 | 
 61 |         For more reliable custom type resolution, explicitly provide types using the
 62 |         `custom_types` parameter when creating a Signature.
 63 |         """
 64 | 
 65 |         # Extract potential type names from the signature string, including dotted names
 66 |         # Match both simple types like 'MyType' and dotted names like 'Module.Type'
 67 |         type_pattern = r":\s*([A-Za-z_][A-Za-z0-9_]*(?:\.[A-Za-z_][A-Za-z0-9_]*)*)"
 68 |         type_names = re.findall(type_pattern, signature_str)
 69 |         if not type_names:
 70 |             return None
 71 | 
 72 |         # Get type references from caller frames by walking the stack
 73 |         found_types = {}
 74 | 
 75 |         needed_types = set()
 76 |         dotted_types = {}
 77 | 
 78 |         for type_name in type_names:
 79 |             parts = type_name.split(".")
 80 |             base_name = parts[0]
 81 | 
 82 |             if base_name not in typing.__dict__ and base_name not in __builtins__:
 83 |                 if len(parts) > 1:
 84 |                     dotted_types[type_name] = base_name
 85 |                     needed_types.add(base_name)
 86 |                 else:
 87 |                     needed_types.add(type_name)
 88 | 
 89 |         if not needed_types:
 90 |             return None
 91 | 
 92 |         frame = None
 93 |         try:
 94 |             frame = sys._getframe(1)  # Start one level up (skip this function)
 95 | 
 96 |             max_frames = 100
 97 |             frame_count = 0
 98 | 
 99 |             while frame and needed_types and frame_count < max_frames:
100 |                 frame_count += 1
101 | 
102 |                 for type_name in list(needed_types):
103 |                     if type_name in frame.f_locals:
104 |                         found_types[type_name] = frame.f_locals[type_name]
105 |                         needed_types.remove(type_name)
106 |                     elif frame.f_globals and type_name in frame.f_globals:
107 |                         found_types[type_name] = frame.f_globals[type_name]
108 |                         needed_types.remove(type_name)
109 | 
110 |                 # If we found all needed types, stop looking
111 |                 if not needed_types:
112 |                     break
113 | 
114 |                 frame = frame.f_back
115 | 
116 |             if needed_types and frame_count >= max_frames:
117 |                 import logging
118 | 
119 |                 logging.getLogger("dspy").warning(
120 |                     f"Reached maximum frame search depth ({max_frames}) while looking for types: {needed_types}. "
121 |                     "Consider providing custom_types explicitly to Signature."
122 |                 )
123 |         except (AttributeError, ValueError):
124 |             # Handle environments where frame introspection is not available
125 |             import logging
126 | 
127 |             logging.getLogger("dspy").debug(
128 |                 "Frame introspection failed while trying to resolve custom types. "
129 |                 "Consider providing custom_types explicitly to Signature."
130 |             )
131 |         finally:
132 |             if frame:
133 |                 del frame
134 | 
135 |         return found_types or None
136 | 
137 |     def __new__(mcs, signature_name, bases, namespace, **kwargs):
138 |         # At this point, the orders have been swapped already.
139 |         field_order = [name for name, value in namespace.items() if isinstance(value, FieldInfo)]
140 |         # Set `str` as the default type for all fields
141 |         raw_annotations = namespace.get("__annotations__", {})
142 |         for name, field in namespace.items():
143 |             if not isinstance(field, FieldInfo):
144 |                 continue  # Don't add types to non-field attributes
145 |             if not name.startswith("__") and name not in raw_annotations:
146 |                 raw_annotations[name] = str
147 |         # Create ordered annotations dictionary that preserves field order
148 |         ordered_annotations = {name: raw_annotations[name] for name in field_order if name in raw_annotations}
149 |         # Add any remaining annotations that weren't in field_order
150 |         ordered_annotations.update({k: v for k, v in raw_annotations.items() if k not in ordered_annotations})
151 |         namespace["__annotations__"] = ordered_annotations
152 | 
153 |         # Let Pydantic do its thing
154 |         cls = super().__new__(mcs, signature_name, bases, namespace, **kwargs)
155 | 
156 |         # If we don't have instructions, it might be because we are a derived generic type.
157 |         # In that case, we should inherit the instructions from the base class.
158 |         if cls.__doc__ is None:
159 |             for base in bases:
160 |                 if isinstance(base, SignatureMeta):
161 |                     doc = getattr(base, "__doc__", "")
162 |                     if doc != "":
163 |                         cls.__doc__ = doc
164 | 
165 |         # The more likely case is that the user has just not given us a type.
166 |         # In that case, we should default to the input/output format.
167 |         if cls.__doc__ is None:
168 |             cls.__doc__ = _default_instructions(cls)
169 | 
170 |         # Ensure all fields are declared with InputField or OutputField
171 |         cls._validate_fields()
172 | 
173 |         # Ensure all fields have a prefix
174 |         for name, field in cls.model_fields.items():
175 |             if "prefix" not in field.json_schema_extra:
176 |                 field.json_schema_extra["prefix"] = infer_prefix(name) + ":"
177 |             if "desc" not in field.json_schema_extra:
178 |                 field.json_schema_extra["desc"] = f"${{{name}}}"
179 | 
180 |         return cls
181 | 
182 |     def _validate_fields(cls):
183 |         for name, field in cls.model_fields.items():
184 |             extra = field.json_schema_extra or {}
185 |             field_type = extra.get("__dspy_field_type")
186 |             if field_type not in ["input", "output"]:
187 |                 raise TypeError(
188 |                     f"Field `{name}` in `{cls.__name__}` must be declared with InputField or OutputField, but "
189 |                     f"field `{name}` has `field.json_schema_extra={field.json_schema_extra}`",
190 |                 )
191 | 
192 |     @property
193 |     def instructions(cls) -> str:
194 |         return inspect.cleandoc(getattr(cls, "__doc__", ""))
195 | 
196 |     @instructions.setter
197 |     def instructions(cls, instructions: str) -> None:
198 |         cls.__doc__ = instructions
199 | 
200 |     @property
201 |     def input_fields(cls) -> dict[str, FieldInfo]:
202 |         return cls._get_fields_with_type("input")
203 | 
204 |     @property
205 |     def output_fields(cls) -> dict[str, FieldInfo]:
206 |         return cls._get_fields_with_type("output")
207 | 
208 |     @property
209 |     def fields(cls) -> dict[str, FieldInfo]:
210 |         # Make sure to give input fields before output fields
211 |         return {**cls.input_fields, **cls.output_fields}
212 | 
213 |     @property
214 |     def signature(cls) -> str:
215 |         """The string representation of the signature."""
216 |         input_fields = ", ".join(cls.input_fields.keys())
217 |         output_fields = ", ".join(cls.output_fields.keys())
218 |         return f"{input_fields} -> {output_fields}"
219 | 
220 |     def _get_fields_with_type(cls, field_type) -> dict[str, FieldInfo]:
221 |         return {k: v for k, v in cls.model_fields.items() if v.json_schema_extra["__dspy_field_type"] == field_type}
222 | 
223 |     def __repr__(cls):
224 |         """Output a representation of the signature.
225 | 
226 |         Uses the form:
227 |         Signature(question, context -> answer
228 |             question: str = InputField(desc="..."),
229 |             context: list[str] = InputField(desc="..."),
230 |             answer: int = OutputField(desc="..."),
231 |         ).
232 |         """
233 |         field_reprs = []
234 |         for name, field in cls.fields.items():
235 |             field_reprs.append(f"{name} = Field({field})")
236 |         field_repr = "\n    ".join(field_reprs)
237 |         return f"{cls.__name__}({cls.signature}\n    instructions={cls.instructions!r}\n    {field_repr}\n)"
238 | 
239 | 
240 | class Signature(BaseModel, metaclass=SignatureMeta):
241 |     ""
242 | 
243 |     # Note: Don't put a docstring here, as it will become the default instructions
244 |     # for any signature that doesn't define it's own instructions.
245 | 
246 |     @classmethod
247 |     def with_instructions(cls, instructions: str) -> type["Signature"]:
248 |         return Signature(cls.fields, instructions)
249 | 
250 |     @classmethod
251 |     def with_updated_fields(cls, name: str, type_: type | None = None, **kwargs: dict[str, Any]) -> type["Signature"]:
252 |         """Create a new Signature class with the updated field information.
253 | 
254 |         Returns a new Signature class with the field, name, updated
255 |         with fields[name].json_schema_extra[key] = value.
256 | 
257 |         Args:
258 |             name: The name of the field to update.
259 |             type_: The new type of the field.
260 |             kwargs: The new values for the field.
261 | 
262 |         Returns:
263 |             A new Signature class (not an instance) with the updated field information.
264 |         """
265 |         fields_copy = deepcopy(cls.fields)
266 |         # Update `fields_copy[name].json_schema_extra` with the new kwargs, on conflicts
267 |         # we use the new value in kwargs.
268 |         fields_copy[name].json_schema_extra = {
269 |             **fields_copy[name].json_schema_extra,
270 |             **kwargs,
271 |         }
272 |         if type_ is not None:
273 |             fields_copy[name].annotation = type_
274 |         return Signature(fields_copy, cls.instructions)
275 | 
276 |     @classmethod
277 |     def prepend(cls, name, field, type_=None) -> type["Signature"]:
278 |         return cls.insert(0, name, field, type_)
279 | 
280 |     @classmethod
281 |     def append(cls, name, field, type_=None) -> type["Signature"]:
282 |         return cls.insert(-1, name, field, type_)
283 | 
284 |     @classmethod
285 |     def delete(cls, name) -> type["Signature"]:
286 |         fields = dict(cls.fields)
287 | 
288 |         fields.pop(name, None)
289 | 
290 |         return Signature(fields, cls.instructions)
291 | 
292 |     @classmethod
293 |     def insert(cls, index: int, name: str, field, type_: type | None = None) -> type["Signature"]:
294 |         # It's possible to set the type as annotation=type in pydantic.Field(...)
295 |         # But this may be annoying for users, so we allow them to pass the type
296 |         if type_ is None:
297 |             type_ = field.annotation
298 |         if type_ is None:
299 |             type_ = str
300 | 
301 |         input_fields = list(cls.input_fields.items())
302 |         output_fields = list(cls.output_fields.items())
303 | 
304 |         # Choose the list to insert into based on the field type
305 |         lst = input_fields if field.json_schema_extra["__dspy_field_type"] == "input" else output_fields
306 |         # We support negative insert indices
307 |         if index < 0:
308 |             index += len(lst) + 1
309 |         if index < 0 or index > len(lst):
310 |             raise ValueError(
311 |                 f"Invalid index to insert: {index}, index must be in the range of [{len(lst) - 1}, {len(lst)}] for "
312 |                 f"{field.json_schema_extra['__dspy_field_type']} fields, but received: {index}.",
313 |             )
314 |         lst.insert(index, (name, (type_, field)))
315 | 
316 |         new_fields = dict(input_fields + output_fields)
317 |         return Signature(new_fields, cls.instructions)
318 | 
319 |     @classmethod
320 |     def equals(cls, other) -> bool:
321 |         """Compare the JSON schema of two Signature classes."""
322 |         if not isinstance(other, type) or not issubclass(other, BaseModel):
323 |             return False
324 |         if cls.instructions != other.instructions:
325 |             return False
326 |         for name in cls.fields.keys() | other.fields.keys():
327 |             if name not in other.fields or name not in cls.fields:
328 |                 return False
329 |             if cls.fields[name].json_schema_extra != other.fields[name].json_schema_extra:
330 |                 return False
331 |         return True
332 | 
333 |     @classmethod
334 |     def dump_state(cls):
335 |         state = {"instructions": cls.instructions, "fields": []}
336 |         for field in cls.fields:
337 |             state["fields"].append(
338 |                 {
339 |                     "prefix": cls.fields[field].json_schema_extra["prefix"],
340 |                     "description": cls.fields[field].json_schema_extra["desc"],
341 |                 }
342 |             )
343 | 
344 |         return state
345 | 
346 |     @classmethod
347 |     def load_state(cls, state):
348 |         signature_copy = Signature(deepcopy(cls.fields), cls.instructions)
349 | 
350 |         signature_copy.instructions = state["instructions"]
351 |         for field, saved_field in zip(signature_copy.fields.values(), state["fields"], strict=False):
352 |             field.json_schema_extra["prefix"] = saved_field["prefix"]
353 |             field.json_schema_extra["desc"] = saved_field["description"]
354 | 
355 |         return signature_copy
356 | 
357 | 
358 | def ensure_signature(signature: str | type[Signature], instructions=None) -> type[Signature]:
359 |     if signature is None:
360 |         return None
361 |     if isinstance(signature, str):
362 |         return Signature(signature, instructions)
363 |     if instructions is not None:
364 |         raise ValueError("Don't specify instructions when initializing with a Signature")
365 |     return signature
366 | 
367 | 
368 | def make_signature(
369 |     signature: str | dict[str, tuple[type, FieldInfo]],
370 |     instructions: str | None = None,
371 |     signature_name: str = "StringSignature",
372 |     custom_types: dict[str, type] | None = None,
373 | ) -> type[Signature]:
374 |     """Create a new Signature subclass with the specified fields and instructions.
375 | 
376 |     Args:
377 |         signature: Either a string in the format "input1, input2 -> output1, output2"
378 |             or a dictionary mapping field names to tuples of (type, FieldInfo).
379 |         instructions: Optional string containing instructions/prompt for the signature.
380 |             If not provided, defaults to a basic description of inputs and outputs.
381 |         signature_name: Optional string to name the generated Signature subclass.
382 |             Defaults to "StringSignature".
383 |         custom_types: Optional dictionary mapping type names to their actual type objects.
384 |             Useful for resolving custom types that aren't built-ins or in the typing module.
385 | 
386 |     Returns:
387 |         A new signature class with the specified fields and instructions.
388 | 
389 |     Examples:
390 | 
391 |     ```
392 |     # Using string format
393 |     sig1 = make_signature("question, context -> answer")
394 | 
395 |     # Using dictionary format
396 |     sig2 = make_signature({
397 |         "question": (str, InputField()),
398 |         "answer": (str, OutputField())
399 |     })
400 | 
401 |     # Using custom types
402 |     class MyType:
403 |         pass
404 | 
405 |     sig3 = make_signature("input: MyType -> output", custom_types={"MyType": MyType})
406 |     ```
407 |     """
408 |     # Prepare the names dictionary for type resolution
409 |     names = None
410 |     if custom_types:
411 |         names = dict(typing.__dict__)
412 |         names.update(custom_types)
413 | 
414 |     fields = _parse_signature(signature, names) if isinstance(signature, str) else signature
415 | 
416 |     # Validate the fields, this is important because we sometimes forget the
417 |     # slightly unintuitive syntax with tuples of (type, Field)
418 |     fixed_fields = {}
419 |     for name, type_field in fields.items():
420 |         if not isinstance(name, str):
421 |             raise ValueError(f"Field names must be strings, but received: {name}.")
422 |         if isinstance(type_field, FieldInfo):
423 |             type_ = type_field.annotation
424 |             field = type_field
425 |         else:
426 |             if not isinstance(type_field, tuple):
427 |                 raise ValueError(f"Field values must be tuples, but received: {type_field}.")
428 |             type_, field = type_field
429 |         # It might be better to be explicit about the type, but it currently would break
430 |         # program of thought and teleprompters, so we just silently default to string.
431 |         if type_ is None:
432 |             type_ = str
433 |         if not isinstance(type_, (type, typing._GenericAlias, types.GenericAlias, typing._SpecialForm, types.UnionType)):
434 |             raise ValueError(f"Field types must be types, but received: {type_} of type {type(type_)}.")
435 |         if not isinstance(field, FieldInfo):
436 |             raise ValueError(f"Field values must be Field instances, but received: {field}.")
437 |         fixed_fields[name] = (type_, field)
438 | 
439 |     # Default prompt when no instructions are provided
440 |     if instructions is None:
441 |         sig = Signature(signature, "")  # Simple way to parse input/output fields
442 |         instructions = _default_instructions(sig)
443 | 
444 |     return create_model(
445 |         signature_name,
446 |         __base__=Signature,
447 |         __doc__=instructions,
448 |         **fixed_fields,
449 |     )
450 | 
451 | 
452 | def _parse_signature(signature: str, names=None) -> dict[str, tuple[type, Field]]:
453 |     if signature.count("->") != 1:
454 |         raise ValueError(f"Invalid signature format: '{signature}', must contain exactly one '->'.")
455 | 
456 |     inputs_str, outputs_str = signature.split("->")
457 | 
458 |     fields = {}
459 |     for field_name, field_type in _parse_field_string(inputs_str, names):
460 |         fields[field_name] = (field_type, InputField())
461 |     for field_name, field_type in _parse_field_string(outputs_str, names):
462 |         fields[field_name] = (field_type, OutputField())
463 | 
464 |     return fields
465 | 
466 | 
467 | def _parse_field_string(field_string: str, names=None) -> dict[str, str]:
468 |     """Extract the field name and type from field string in the string-based Signature.
469 | 
470 |     It takes a string like "x: int, y: str" and returns a dictionary mapping field names to their types.
471 |     For example, "x: int, y: str" -> [("x", int), ("y", str)]. This function utitlizes the Python AST to parse the
472 |     fields and types.
473 |     """
474 | 
475 |     args = ast.parse(f"def f({field_string}): pass").body[0].args.args
476 |     field_names = [arg.arg for arg in args]
477 |     types = [str if arg.annotation is None else _parse_type_node(arg.annotation, names) for arg in args]
478 |     return zip(field_names, types, strict=False)
479 | 
480 | 
481 | def _parse_type_node(node, names=None) -> Any:
482 |     """Recursively parse an AST node representing a type annotation.
483 | 
484 |     This function converts Python's Abstract Syntax Tree (AST) nodes into actual Python types.
485 |     It's used to parse type annotations in signature strings like "x: list[int] -> y: str".
486 | 
487 |     Examples:
488 |         - For "x: int", the AST node represents 'int' and returns the int type
489 |         - For "x: list[str]", it processes a subscript node to return typing.list[str]
490 |         - For "x: Optional[int]", it handles the Union type to return Optional[int]
491 |         - For "x: MyModule.CustomType", it processes attribute access to return the actual type
492 | 
493 |     Args:
494 |         node: An AST node from Python's ast module, representing a type annotation.
495 |             Common node types include:
496 |             - ast.Name: Simple types like 'int', 'str'
497 |             - ast.Attribute: Nested types like 'typing.List'
498 |             - ast.Subscript: Generic types like 'list[int]'
499 |         names: Optional dictionary mapping type names to their actual type objects.
500 |             Defaults to Python's typing module contents plus NoneType.
501 | 
502 |     Returns:
503 |         The actual Python type represented by the AST node.
504 | 
505 |     Raises:
506 |         ValueError: If the AST node represents an unknown or invalid type annotation.
507 |     """
508 | 
509 |     if names is None:
510 |         names = dict(typing.__dict__)
511 |         names["NoneType"] = type(None)
512 | 
513 |     def resolve_name(type_name: str):
514 |         # Check if it's a built-in known type or in the provided names
515 |         if type_name in names:
516 |             return names[type_name]
517 |         # Common built-in types
518 |         builtin_types = [int, str, float, bool, list, tuple, dict, set, frozenset, complex, bytes, bytearray]
519 | 
520 |         # Check if it matches any known built-in type by name
521 |         for t in builtin_types:
522 |             if t.__name__ == type_name:
523 |                 return t
524 | 
525 |         # Attempt to import a module with this name dynamically
526 |         # This allows handling of module-based annotations like `dspy.Image`.
527 |         try:
528 |             mod = importlib.import_module(type_name)
529 |             names[type_name] = mod
530 |             return mod
531 |         except ImportError:
532 |             pass
533 | 
534 |         # If we don't know the type or module, raise an error
535 |         raise ValueError(f"Unknown name: {type_name}")
536 | 
537 |     if isinstance(node, ast.Module):
538 |         if len(node.body) != 1:
539 |             raise ValueError(f"Code is not syntactically valid: {ast.dump(node)}")
540 |         return _parse_type_node(node.body[0], names)
541 | 
542 |     if isinstance(node, ast.Expr):
543 |         return _parse_type_node(node.value, names)
544 | 
545 |     if isinstance(node, ast.Name):
546 |         return resolve_name(node.id)
547 | 
548 |     if isinstance(node, ast.Attribute):
549 |         base = _parse_type_node(node.value, names)
550 |         attr_name = node.attr
551 | 
552 |         if hasattr(base, attr_name):
553 |             return getattr(base, attr_name)
554 | 
555 |         if isinstance(node.value, ast.Name):
556 |             full_name = f"{node.value.id}.{attr_name}"
557 |             if full_name in names:
558 |                 return names[full_name]
559 | 
560 |         raise ValueError(f"Unknown attribute: {attr_name} on {base}")
561 | 
562 |     if isinstance(node, ast.Subscript):
563 |         base_type = _parse_type_node(node.value, names)
564 |         slice_node = node.slice
565 |         if isinstance(slice_node, ast.Index):  # For older Python versions
566 |             slice_node = slice_node.value
567 | 
568 |         if isinstance(slice_node, ast.Tuple):
569 |             arg_types = tuple(_parse_type_node(elt, names) for elt in slice_node.elts)
570 |         else:
571 |             arg_types = (_parse_type_node(slice_node, names),)
572 | 
573 |         # Special handling for Union, Optional
574 |         if base_type is typing.Union:
575 |             return typing.Union[arg_types]
576 |         if base_type is typing.Optional:
577 |             if len(arg_types) != 1:
578 |                 raise ValueError("Optional must have exactly one type argument")
579 |             return typing.Optional[arg_types[0]]
580 | 
581 |         return base_type[arg_types]
582 | 
583 |     if isinstance(node, ast.BinOp) and isinstance(node.op, ast.BitOr):
584 |         # Handle PEP 604: int | None, str | float, etc.
585 |         left = _parse_type_node(node.left, names)
586 |         right = _parse_type_node(node.right, names)
587 | 
588 |         # Optional[X] is Union[X, NoneType]
589 |         if right is type(None):
590 |             return typing.Optional[left]
591 |         if left is type(None):
592 |             return typing.Optional[right]
593 |         return typing.Union[left, right]
594 | 
595 |     if isinstance(node, ast.Tuple):
596 |         return tuple(_parse_type_node(elt, names) for elt in node.elts)
597 | 
598 |     if isinstance(node, ast.Constant):
599 |         return node.value
600 | 
601 |     if isinstance(node, ast.Call) and isinstance(node.func, ast.Name) and node.func.id == "Field":
602 |         keys = [kw.arg for kw in node.keywords]
603 |         values = []
604 |         for kw in node.keywords:
605 |             if isinstance(kw.value, ast.Constant):
606 |                 values.append(kw.value.value)
607 |             else:
608 |                 values.append(_parse_type_node(kw.value, names))
609 |         return Field(**dict(zip(keys, values, strict=False)))
610 | 
611 |     raise ValueError(
612 |         f"Failed to parse string-base Signature due to unhandled AST node type in annotation: {ast.dump(node)}. "
613 |         "Please consider using class-based DSPy Signatures instead."
614 |     )
615 | 
616 | 
617 | def infer_prefix(attribute_name: str) -> str:
618 |     """Infer a prefix from an attribute name by converting it to a human-readable format.
619 | 
620 |     Examples:
621 |         "camelCaseText" -> "Camel Case Text"
622 |         "snake_case_text" -> "Snake Case Text"
623 |         "text2number" -> "Text 2 Number"
624 |         "HTMLParser" -> "HTML Parser"
625 |     """
626 |     # Step 1: Convert camelCase to snake_case
627 |     # Example: "camelCase" -> "camel_Case"
628 |     s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", attribute_name)
629 | 
630 |     # Handle consecutive capitals
631 |     # Example: "camel_Case" -> "camel_case"
632 |     intermediate_name = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1)
633 | 
634 |     # Step 2: Handle numbers by adding underscores around them
635 |     # Example: "text2number" -> "text_2_number"
636 |     with_underscores_around_numbers = re.sub(
637 |         r"([a-zA-Z])(\d)",  # Match letter followed by number
638 |         r"\1_\2",  # Add underscore between them
639 |         intermediate_name,
640 |     )
641 |     # Example: "2text" -> "2_text"
642 |     with_underscores_around_numbers = re.sub(
643 |         r"(\d)([a-zA-Z])",  # Match number followed by letter
644 |         r"\1_\2",  # Add underscore between them
645 |         with_underscores_around_numbers,
646 |     )
647 | 
648 |     # Step 3: Convert to Title Case while preserving acronyms
649 |     words = with_underscores_around_numbers.split("_")
650 |     title_cased_words = []
651 |     for word in words:
652 |         if word.isupper():
653 |             # Preserve acronyms like 'HTML', 'API' as-is
654 |             title_cased_words.append(word)
655 |         else:
656 |             # Capitalize first letter: 'text' -> 'Text'
657 |             title_cased_words.append(word.capitalize())
658 | 
659 |     # Join words with spaces
660 |     # Example: ["Text", "2", "Number"] -> "Text 2 Number"
661 |     return " ".join(title_cased_words)
662 | 
```
Page 13/17FirstPrevNextLast